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.

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 sudo privileges 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 integrationCard data touches your server?Typical SAQServer requirements
Redirect to the provider's hosted payment page, or a provider iframe for all card fieldsNoSAQ ASmall set, mainly protecting the page that loads the payment form
Your page loads the provider's JavaScript, which sends card data directly to the providerNo, but your page could be tampered withSAQ A-EPMost of the technical requirements in this guide
Card numbers are posted to your server or stored in your databaseYesSAQ DEverything, 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:

RequirementControlEvidence
ScopeHosted payment page or iframe, no stored card dataIntegration docs, grep results
1UFW default deny, SSH only from your_admin_ipufw status verbose, rule review record
2Unneeded services disabled, SSH hardened, defaults changedss -tlnup, sshd -T output
4TLS 1.2+ only, strong ciphers, HSTSnmap ssl-enum-ciphers report
5ClamAV with daily scan (or documented risk assessment)Scan logs
6Automatic security updates, WAF, payment page script controlUpdate logs, WAF config, CSP header
7, 8Personal accounts, 12+ character passwords, SSH MFAUser list, pwquality.conf, PAM config
10auditd rules, central log retention of 12 months, Chronyauditctl -l, log server config, chronyc tracking
11AIDE daily check, ASV scans, penetration testAIDE reports, ASV reports, test report
12Security policy, incident response plan, staff trainingDocuments

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.