The Payment Card Industry Data Security Standard (PCI DSS) applies to every business that accepts card payments, including small online stores. Version 4.0.1 is the current version, and the requirements that were "future-dated" became mandatory on March 31, 2025. In this tutorial you will first reduce how much of your infrastructure falls under PCI DSS, then work through the technical requirements that apply to the Ubuntu 24.04 server running your store: network controls, secure configuration, TLS, patching, multi-factor authentication, anti-malware, audit logging and file integrity monitoring.
NoteYour acquiring bank or payment provider decides which self-assessment questionnaire (SAQ) you must complete and whether you need a Qualified Security Assessor. Use this guide for the server work, and the official PCI SSC documents for the full list of requirements.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS that hosts your store, for example a CubePath VPS.
- A non-root user with
sudoprivileges and SSH key authentication already working. - Nginx serving the site over HTTPS with a valid certificate.
- The static public IP address of the network you administer the server from, referred to as
your_admin_ip. - An authenticator app (such as any TOTP app) on your phone for Step 6.
Step 1 - Reducing your PCI DSS scope
The most effective PCI DSS control is not to handle card data at all. Everything that stores, processes or transmits cardholder data, or can affect its security, is in scope. How your checkout collects card numbers determines how much work you have:
| Checkout integration | Card data touches your server? | Typical SAQ | Server requirements |
|---|---|---|---|
| Redirect to the provider's hosted payment page, or a provider iframe for all card fields | No | SAQ A | Small set, mainly protecting the page that loads the payment form |
| Your page loads the provider's JavaScript, which sends card data directly to the provider | No, but your page could be tampered with | SAQ A-EP | Most of the technical requirements in this guide |
| Card numbers are posted to your server or stored in your database | Yes | SAQ D | Everything, plus encryption and key management for stored data |
If your store is in the last row, move to a hosted payment page or iframe integration from your payment provider. Tokenization means your database only stores a token that is useless outside the provider.
Whatever your integration, two rules are absolute:
- Never store sensitive authentication data (the CVV/CVC code, full magnetic stripe or PIN data) after authorization, not even encrypted (Requirement 3.3.1).
- Never log card numbers. Check that your application, web server and error logs do not capture checkout form fields.
To look for card numbers that may have leaked into logs, search for 13 to 16 digit sequences:
sudo grep -rEo '\b[0-9]{13,16}\b' /var/log /var/www 2>/dev/null | head
This finds candidates, including order IDs and timestamps, so review each match manually. Any real card number you find must be removed and its source fixed.
Step 2 - Restricting network access (Requirement 1)
Only the ports the store needs should be reachable from the Internet, and SSH should only be reachable from your administration network. Configure UFW, allowing SSH from your IP first so you do not lock yourself out:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from your_admin_ip to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Check the result:
sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ---- ----
22/tcp ALLOW IN your_admin_ip
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
Port 80 is only needed to redirect visitors to HTTPS and for Let's Encrypt renewals. Keep the database listening on 127.0.0.1 or on a private network, never on a public address. Requirement 1 also asks you to restrict outbound traffic from systems in the cardholder data environment to what is necessary; if your server handles card data (SAQ D), replace the allow outgoing default with explicit rules for your payment provider, package mirrors, DNS and NTP.
Document every rule and its business reason, and review the rule set at least every six months (Requirement 1.2.7).
Step 3 - Removing unnecessary services and hardening SSH (Requirement 2)
Every running service is attack surface. List what is listening:
sudo ss -tlnup
Stop and disable anything the store does not need. For example, if a leftover FTP server is running:
sudo systemctl disable --now vsftpd
Next, harden SSH. Create a drop-in file; files in sshd_config.d are read before the main configuration, so their values take precedence:
sudo nano /etc/ssh/sshd_config.d/10-pci.conf
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 4
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 3
X11Forwarding no
ClientAliveInterval and ClientAliveCountMax close idle sessions after about 15 minutes, matching Requirement 8.2.8. Validate the configuration and restart SSH, keeping your current session open:
sudo sshd -t
sudo systemctl restart ssh
Open a second terminal and confirm you can still log in before closing the first one.
Also change every vendor default: database root passwords, admin accounts of your e-commerce platform and any sample accounts they create (Requirement 2.2.2).
Step 4 - Enforcing strong TLS (Requirement 4)
Card data and the pages that collect it must only travel over strong cryptography. SSL and TLS 1.0/1.1 are not acceptable. On Ubuntu, the default nginx.conf still lists the old protocols, so edit it:
sudo nano /etc/nginx/nginx.conf
In the http block, replace the existing ssl_protocols line and add a cipher list with forward secrecy and authenticated encryption only:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
In the server block of your store in /etc/nginx/sites-available/, add HSTS so browsers always use HTTPS:
add_header Strict-Transport-Security "max-age=63072000" always;
Test and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Verify which protocols and ciphers are offered with Nmap. Run it from your workstation or the server, replacing your_domain:
sudo apt install nmap
nmap --script ssl-enum-ciphers -p 443 your_domain
PORT STATE SERVICE
443/tcp open https
| ssl-enum-ciphers:
| TLSv1.2:
| ciphers:
| TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
| TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
| TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (secp256r1) - A
| TLSv1.3:
| ciphers:
| TLS_AKE_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A
| TLS_AKE_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A
| TLS_AKE_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A
|_ least strength: A
Only TLSv1.2 and TLSv1.3 should appear, all rated A.
Step 5 - Patching and protecting the payment page (Requirement 6)
Critical security patches must be installed within one month of release (Requirement 6.3.3). Enable automatic security updates:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Answer Yes to enable automatic updates. Confirm that a dry run selects the security origin:
sudo unattended-upgrade --dry-run --debug 2>&1 | grep -i "allowed origins"
Allowed origins are: o=Ubuntu,a=noble, o=Ubuntu,a=noble-security, o=UbuntuESMApps,a=noble-apps-security, o=UbuntuESM,a=noble-infra-security
Keep your e-commerce platform and its plugins updated too; they are the most common entry point for card-skimming attacks.
Two newer requirements target those attacks directly:
- Requirement 6.4.2: public-facing web applications must be protected by an automated solution that detects and blocks web attacks, such as a web application firewall (WAF) in front of the store.
- Requirement 6.4.3 and 11.6.1: every script loaded on the payment page must be authorized and inventoried, and you must detect unauthorized changes to the page and its security headers.
A Content Security Policy on the checkout page limits which scripts the browser runs. Add it to the Nginx location that serves checkout, replacing payments.example.com with your payment provider's domain from its documentation:
location /checkout {
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://payments.example.com; frame-src https://payments.example.com; connect-src 'self' https://payments.example.com" always;
add_header Strict-Transport-Security "max-age=63072000" always;
try_files $uri $uri/ /index.php?$args;
}
Nginx does not inherit add_header directives into a location that defines its own, which is why HSTS is repeated. Adapt try_files to how your platform routes requests, test in a staging copy first, and check the browser console for blocked scripts before deploying.
Step 6 - Individual accounts, passwords and MFA (Requirements 7 and 8)
Every administrator needs a personal account with the minimum privileges required; shared accounts are not allowed. List users who can log in and members of the sudo group:
getent passwd | awk -F: '$7 !~ /(nologin|false)$/ {print $1}'
getent group sudo
Lock accounts of people who no longer need access:
sudo usermod -L -e 1 former_user
Password policy
Requirement 8.3.6 requires passwords of at least 12 characters with both letters and numbers. Install pwquality, which Ubuntu adds to the PAM password stack automatically:
sudo apt install libpam-pwquality
sudo nano /etc/security/pwquality.conf
Set these values:
minlen = 12
dcredit = -1
lcredit = -1
dcredit = -1 requires at least one digit and lcredit = -1 at least one lowercase letter. Test it by changing your password with passwd and entering a short one; it is rejected with BAD PASSWORD: The password is shorter than 12 characters.
Multi-factor authentication for SSH
Requirement 8.4.2 requires MFA for all access into the cardholder data environment, and 8.4.1 for all administrative access. Combine your SSH key with a time-based one-time password. Install the PAM module:
sudo apt install libpam-google-authenticator
As your own user (not with sudo), create your TOTP secret and scan the QR code with your authenticator app:
google-authenticator -t -d -f -r 3 -R 30 -w 3
The flags create time-based codes, forbid code reuse, write the file without asking, limit logins to three every 30 seconds and accept one code before and after the current one to tolerate clock drift. Save the emergency scratch codes it prints in your password manager. Every administrator must do this before you enable MFA.
Edit the SSH PAM configuration:
sudo nano /etc/pam.d/sshd
Comment out the line that asks for the Unix password and add the TOTP module below it:
# @include common-auth
auth required pam_google_authenticator.so
Then add the MFA settings to your SSH drop-in:
sudo nano /etc/ssh/sshd_config.d/10-pci.conf
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
Validate and restart SSH, keeping your current session open:
sudo sshd -t
sudo systemctl restart ssh
From a new terminal, log in. After your key is accepted, SSH asks for the code:
(sammy@your_server_ip) Verification code:
Step 7 - Anti-malware (Requirement 5)
Requirement 5 applies to all systems unless a documented risk assessment shows they are not at risk from malware. For a web server that accepts uploads or runs a PHP platform, a scanner is a reasonable control. Install ClamAV; the clamav-freshclam service updates signatures automatically:
sudo apt install clamav clamav-freshclam
systemctl status clamav-freshclam --no-pager
The first signature download takes a minute or two after installation. Then run a first scan of the web root, printing only infected files:
sudo clamscan -r -i /var/www
----------- SCAN SUMMARY -----------
Known viruses: 8702354
Scanned directories: 1204
Scanned files: 9811
Infected files: 0
Schedule a daily scan with a cron file:
sudo nano /etc/cron.d/clamscan
30 3 * * * root clamscan -r -i --log=/var/log/clamav/daily-scan.log /var/www
clamscan loads the whole signature database into memory and needs about 1.5 GB of free RAM, so run it at a quiet hour.
Step 8 - Audit logging and time synchronization (Requirement 10)
You must log all administrative actions, access to audit logs, invalid access attempts and changes to accounts, and keep logs for at least 12 months with the last three months immediately available (Requirement 10.5.1). Install the Linux audit daemon:
sudo apt install auditd
sudo nano /etc/audit/rules.d/pci.rules
Add these rules:
-w /var/log/audit/ -p wa -k audit_logs
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config
-w /etc/nginx/ -p wa -k web_config
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=unset -k admin_commands
-a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -k access_denied
-a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -k access_denied
The -w rules watch files for changes. The execve rule records every command a logged-in user runs as root, and the openat rules record denied file access. Load the rules and check them:
sudo augenrules --load
sudo auditctl -l | head -n 3
-w /var/log/audit/ -p wa -k audit_logs
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
Run any command with sudo and look it up:
sudo ausearch -k admin_commands -i | tail -n 4
Local logs can be deleted by an attacker who gains root, and a single VPS disk is not a 12-month archive. Forward audit, authentication and web logs to a separate log server or SIEM, and review them daily, which Requirement 10.4.1 allows you to automate.
Log timestamps are only useful if the clock is correct (Requirement 10.6). Replace the default systemd-timesyncd with Chrony:
sudo apt install chrony
chronyc tracking
Reference ID : 5DB3A1C2 (ntp.ubuntu.com)
Stratum : 3
System time : 0.000012841 seconds fast of NTP time
Leap status : Normal
Step 9 - File integrity monitoring and scanning (Requirement 11)
Requirement 11.5.2 requires a change-detection mechanism that alerts on unauthorized changes to critical files, at least weekly. AIDE builds a database of file hashes and reports differences. Install it and build the initial database, which takes a few minutes:
sudo apt install aide
sudo aideinit -y -f
Run a check to confirm it works. On an unchanged system, the summary reports no differences:
sudo aide --config /etc/aide/aide.conf --check
The Ubuntu package schedules a daily check that emails its report to root. Confirm the schedule with systemctl list-timers | grep -i aide, or look for an AIDE script in /etc/cron.daily/, and make sure mail for root reaches a monitored address. After a legitimate change, such as a package upgrade, update the baseline with sudo aideinit -y -f so that real changes stand out.
Finally, the testing requirements you cannot do from the server itself:
- Quarterly external vulnerability scans by a PCI SSC Approved Scanning Vendor (ASV), plus a rescan after significant changes (Requirement 11.3.2).
- Quarterly internal vulnerability scans, authenticated where possible (Requirement 11.3.1).
- Annual penetration testing of the application and network, and after significant changes (Requirement 11.4).
PCI DSS server checklist
Use this table to track progress and collect evidence for your SAQ:
| Requirement | Control | Evidence |
|---|---|---|
| Scope | Hosted payment page or iframe, no stored card data | Integration docs, grep results |
| 1 | UFW default deny, SSH only from your_admin_ip | ufw status verbose, rule review record |
| 2 | Unneeded services disabled, SSH hardened, defaults changed | ss -tlnup, sshd -T output |
| 4 | TLS 1.2+ only, strong ciphers, HSTS | nmap ssl-enum-ciphers report |
| 5 | ClamAV with daily scan (or documented risk assessment) | Scan logs |
| 6 | Automatic security updates, WAF, payment page script control | Update logs, WAF config, CSP header |
| 7, 8 | Personal accounts, 12+ character passwords, SSH MFA | User list, pwquality.conf, PAM config |
| 10 | auditd rules, central log retention of 12 months, Chrony | auditctl -l, log server config, chronyc tracking |
| 11 | AIDE daily check, ASV scans, penetration test | AIDE reports, ASV reports, test report |
| 12 | Security policy, incident response plan, staff training | Documents |
Conclusion
You reduced your PCI DSS scope by keeping card data away from the server, then applied the key technical controls on Ubuntu 24.04: a restrictive firewall, hardened SSH with MFA, TLS 1.2 and 1.3 only, automatic patching, a Content Security Policy on the checkout, malware scanning, audit logging with synchronized time and file integrity monitoring. As next steps, set up central log collection with 12-month retention, schedule your first ASV scan, and write the incident response plan required by Requirement 12.10.
