SOC 2 is an attestation report issued by an independent CPA firm about how an organization protects customer data, based on the AICPA Trust Services Criteria. It does not prescribe specific settings: you define controls, and the auditor checks that they are designed well (Type I) and that they operated consistently over a period, usually 3 to 12 months (Type II). For the servers, that means two jobs: configure the controls, and keep evidence that they were in place the whole time. In this tutorial you will implement the most commonly audited Linux controls on Ubuntu 24.04 and automate the evidence collection.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, and a non-root user with sudo privileges who logs in with an SSH key.
  • A central log destination that server administrators cannot modify, such as a dedicated syslog server or a SIEM. This guide uses logs.your_domain on TCP port 514 as a placeholder.
  • The scope of your audit and your written policies (access control, change management, incident response). The commands below implement policies; they do not replace them.

How the criteria map to a Linux server

Most SOC 2 reports cover the Security category (the Common Criteria, CC), often with Availability. These are the criteria that auditors test directly on servers:

CriterionWhat the auditor looks forLinux control in this guide
CC6.1Access restricted to authorized, identifiable usersIndividual accounts, SSH keys, no root login
CC6.2 / CC6.3Access granted and removed through a processGroup-based access, review of accounts
CC7.2Monitoring of system activity and anomaliesauditd rules, central logging
CC7.1 / CC8.1Vulnerabilities and changes trackedAutomatic security updates, package history
A1.2Backups and recoveryBackups with tested restores

Step 1 - Enforcing individual accounts and key-based SSH

Every action on a server must be traceable to one person, so shared accounts and direct root logins are not acceptable. Create a group that controls who may log in over SSH:

sudo groupadd sshusers

Create one account per administrator, replacing alice with the real user name, and add it to the SSH group and to sudo:

sudo adduser alice
sudo usermod -aG sshusers,sudo alice

Add the user's public key to /home/alice/.ssh/authorized_keys with permissions 600, owned by the user. Also add your own account to the group before the next change, or you will lock yourself out:

sudo usermod -aG sshusers "$USER"

Now restrict SSH. Create a drop-in file; on Ubuntu 24.04 files in sshd_config.d are read first and the first value wins, so a low number takes precedence:

sudo nano /etc/ssh/sshd_config.d/10-soc2.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowGroups sshusers
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
LogLevel VERBOSE

LogLevel VERBOSE records the fingerprint of the key used for each login, which lets you attribute a session to a specific key. Test and apply:

sudo sshd -t && sudo systemctl restart ssh

Keep your current session open and confirm in a new terminal that you can still log in. Then check the effective configuration:

sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowgroups)'
permitrootlogin no
passwordauthentication no
allowgroups sshusers

For access reviews, this command lists who can log in and who has sudo, which is exactly what an auditor asks for each quarter:

getent group sshusers sudo

When someone leaves, lock the account and expire it the same day rather than deleting it, so their audit trail keeps a valid owner:

sudo usermod --lock --expiredate 1 alice

Step 2 - Recording privileged activity with auditd

The Linux audit framework records security events in the kernel, including every command run as root by a real user. Install the daemon and its plugins (you will need the syslog plugin in Step 3):

sudo apt install auditd audispd-plugins

Create a rules file for the events auditors ask about most: changes to identity and privilege files, SSH and cron configuration, and commands executed as root:

sudo nano /etc/audit/rules.d/50-soc2.rules
## Identity and privilege changes
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k privilege
-w /etc/sudoers.d/ -p wa -k privilege

## Remote access and scheduled tasks
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config
-w /etc/crontab -p wa -k cron
-w /etc/cron.d/ -p wa -k cron
-w /var/spool/cron/ -p wa -k cron

## Commands run as root by logged-in users
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k root_commands
-a always,exit -F arch=b32 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k root_commands

The auid filter keeps the log focused: it matches commands whose login user is a real person (UID 1000 and above), even after sudo, and skips background services. Load the rules and list them:

sudo augenrules --load
sudo auditctl -l

Test that a change is recorded. Creating and deleting a group writes to /etc/group, so it triggers the identity key:

sudo groupadd audittest && sudo groupdel audittest
sudo ausearch -k identity -i --start recent | tail -n 5

The output includes the file, the command and auid= with the name of the person who ran it. For a summary of recent activity by user, use:

sudo aureport -x --summary -i

Step 3 - Shipping logs to a central server

Logs stored only on the server can be altered by anyone who compromises it, and they vanish if the server is rebuilt. Ship them to the central destination with rsyslog, which is installed on Ubuntu by default. Create a forwarding rule with a disk-assisted queue, so messages are kept if the log server is unreachable:

sudo nano /etc/rsyslog.d/60-forward.conf
*.* action(
  type="omfwd"
  target="logs.your_domain"
  port="514"
  protocol="tcp"
  queue.type="LinkedList"
  queue.filename="fwd_central"
  queue.saveOnShutdown="on"
  queue.maxDiskSpace="1g"
  action.resumeRetryCount="-1"
)

auditd writes to its own file, so also send audit records to syslog. Enable the audit syslog plugin:

sudo sed -i 's/^active = no/active = yes/' /etc/audit/plugins.d/syslog.conf

Check the syntax and restart both services:

sudo rsyslogd -N1
sudo systemctl restart rsyslog auditd

Send a test message and confirm it arrives on the central server:

logger -t soc2-test "central logging test from $(hostname)"

Step 4 - Patching and change history

Auditors test that security updates are applied within the timeframe your policy states. Ubuntu 24.04 server installs unattended-upgrades with security updates enabled; confirm it is installed and active:

sudo apt install unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

If the file is missing or the values are 0, enable it:

sudo dpkg-reconfigure --priority=low unattended-upgrades

The evidence of patching and of every manual package change is already on disk, so you do not need extra tooling:

  • /var/log/unattended-upgrades/unattended-upgrades.log: every automatic update run.
  • /var/log/apt/history.log: every apt operation, with the date and the Requested-By user.
  • /var/log/dpkg.log: every package installed, upgraded or removed.

Show the most recent changes with the user who made them:

grep -E '^(Start-Date|Commandline|Requested-By)' /var/log/apt/history.log | tail -n 9

Application and configuration changes should go through your version control and review process (pull requests, CI pipelines, tickets); that is where CC8.1 evidence for them comes from. On the server, the auditd rules from Step 2 catch changes made outside that process.

Step 5 - Checking encryption in transit and at rest

CC6.7 asks that data is protected in transit. Check that each public service offers only modern TLS versions. Replace your_domain:

openssl s_client -connect your_domain:443 -servername your_domain -tls1_1 </dev/null

A handshake failure is the expected result, because TLS 1.1 must be rejected. Then check that TLS 1.2 works and read the certificate expiry date:

openssl s_client -connect your_domain:443 -servername your_domain -tls1_2 </dev/null 2>/dev/null | openssl x509 -noout -enddate
notAfter=Dec 14 08:12:45 2026 GMT

For data at rest, list block devices and look for crypt devices if your policy requires disk encryption on the server itself:

lsblk -o NAME,TYPE,FSTYPE,MOUNTPOINTS

If your provider encrypts storage at the infrastructure level, that is documented in the provider's own SOC 2 or ISO 27001 report, which you reference as a subservice organization instead of proving it from inside the VM.

Step 6 - Collecting evidence automatically

For a Type II audit you need proof that controls were in place during the whole period, not only on the day of the audit. A monthly snapshot of the configuration answers most auditor requests. Create the collection script:

sudo nano /usr/local/sbin/soc2-evidence
#!/usr/bin/env bash
set -euo pipefail

outdir="/var/lib/soc2-evidence/$(date +%Y-%m)"
install -d -m 0700 "$outdir"
cd "$outdir"

getent passwd > users.txt
getent group sshusers sudo > privileged-groups.txt
cat /etc/sudoers.d/* > sudoers-d.txt 2>/dev/null || true
sshd -T > sshd-effective.txt
auditctl -l > audit-rules.txt
last -F -n 500 > logins.txt || true
journalctl -u ssh --since "-31 days" --no-pager | grep -E 'Accepted|Failed' > ssh-auth.txt || true
dpkg-query -W -f='${Package} ${Version}\n' > packages.txt
cp /var/log/apt/history.log apt-history.txt
systemctl list-units --type=service --state=running --no-pager > services.txt
ss -tulpn > listening-ports.txt
ufw status verbose > firewall.txt || true

sha256sum -- *.txt > SHA256SUMS
echo "Evidence written to $outdir"

The SHA256SUMS file lets you show later that the snapshot was not edited. Make the script executable and run it once:

sudo chmod 750 /usr/local/sbin/soc2-evidence
sudo /usr/local/sbin/soc2-evidence
sudo ls /var/lib/soc2-evidence/"$(date +%Y-%m)"
SHA256SUMS  apt-history.txt  audit-rules.txt  firewall.txt  listening-ports.txt  logins.txt  packages.txt
privileged-groups.txt  services.txt  ssh-auth.txt  sshd-effective.txt  sudoers-d.txt  users.txt

Schedule it on the first day of each month with a systemd timer. Create the service:

sudo nano /etc/systemd/system/soc2-evidence.service
[Unit]
Description=Collect monthly SOC 2 evidence

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/soc2-evidence

Then the timer:

sudo nano /etc/systemd/system/soc2-evidence.timer
[Unit]
Description=Run SOC 2 evidence collection monthly

[Timer]
OnCalendar=monthly
Persistent=true

[Install]
WantedBy=timers.target

Enable it and confirm the next run:

sudo systemctl daemon-reload
sudo systemctl enable --now soc2-evidence.timer
systemctl list-timers soc2-evidence.timer

Copy each month's folder to the same write-protected storage as your logs, so the evidence survives a server rebuild.

Troubleshooting

auditctl -l shows "No rules". augenrules did not load the file, usually because of a syntax error. Run sudo augenrules --check and sudo augenrules --load and read the error. Also check that the rule set is not locked with -e 2 from an earlier configuration, which requires a reboot to change.

No logs arrive at the central server. Confirm the server can reach it with nc -zv logs.your_domain 514, check sudo journalctl -u rsyslog -n 50 for connection errors, and make sure the firewall on the log server allows the connection.

A user cannot log in after Step 1. They are probably not in sshusers. Check with id their_user and add them with sudo usermod -aG sshusers their_user. sudo journalctl -u ssh -n 50 shows the reason for each rejection.

Conclusion

Your Ubuntu 24.04 server now enforces individual key-based access, records privileged activity with auditd, ships its logs off the host, applies security updates automatically, and produces a monthly, checksummed evidence snapshot. Next, apply the same configuration to every in-scope server through configuration management, schedule quarterly access reviews using the privileged groups report, and add a documented, tested backup restore to cover the Availability criteria.