A practical SOC 2 compliance guide for engineers, not auditors. Covers the Trust Service Criteria, what evidence auditors actually want, how to automate compliance controls, and how to pass your audit without losing your mind.
SOC 2 compliance isn't a product you install; it's a set of controls that engineering teams must instrument into their systems, processes, and data flows. This page covers the concrete things you need to build or configure to satisfy the Trust Services Criteria—specifically Security, Availability, and Confidentiality—when your platform handles user data or provides services to other organizations. You'll need this when a customer asks for a SOC 2 report, when you're preparing for an audit, or when your legal team mandates documented controls. The following sections walk through what to implement, how to instrument it, and how to produce evidence without inventing tooling or attributing mechanisms to specific vendors.
Produce a tamper‑evident, time‑stamped record of every security‑relevant event—authentications, configuration changes, and data accesses—in a format an auditor can dump, verify, and retain for the required retention period.
On a typical Linux host, authentication and authorization events are recorded in the system log at /var/log/auth.log. Verify the pipe with:
grep "Accepted password" /var/log/auth.log | tail -3
If recent logins occurred, the output includes lines such as username sshd: ... accept. If the output is empty, either no recent logins took place or the service isn’t writing to the default path.
Many applications replace the system auth log with their own internal audit file (e.g., /var/log/app/audit.json). If your process writes there instead of the system log, the grep above returns nothing and you have a blind spot. You only discover this when an auditor asks for login history and the trail ends at your application’s custom file rather than the expected system log.
Demonstrate that every principal (human or service) operates with the minimum permissions needed, and that you can prove it at audit time by showing the exact decision boundary.
Use sudo -l to list the commands a given user can run from the current shell:
sudo -l -U app_user
If the user is authorized, the output lists the allowed runas and commands. If not, you see the exact error:
Sorry, user app_user may not run sudo on localhost.
sudo -l checks the cached credential, which expires after the sudo timeout (default five minutes). If you run the check minutes after the last sudo invocation, the cache is stale and the command reports “Sorry” even though the user’s policy permits it. Always pair the check with a fresh sudo -k to invalidate the cache, or run sudo -v to refresh it before the test.
Show that data is encrypted while moving and while stored, with key management that can be inspected and proven during an audit.
Verify TLS configuration on any public endpoint with openssl s_client:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com < /dev/null 2>/dev/null | openssl x509 -noout -dates -subject -issuer
The output returns lines such as notBefore: Mar 15 12:00:00 2024 GMT and notAfter: Sep 15 12:00:00 2024 GMT. If notBefore is in the past and notAfter is within the next 30 days, you have an expiring certificate that an auditor will flag.
openssl s_client connects to the server’s public endpoint, but some architectures terminate TLS at an internal load balancer or reverse proxy. The command may report a valid certificate from the proxy, while the origin server presents a different or expired cert. You only learn this when you run the same command against the origin’s IP or hostname and get a mismatch, or when the auditor requests the full certificate chain and you can only provide the proxy’s leaf cert.
Gather the outputs from the previous steps, format them as a machine‑readable package, and keep it refreshed so an auditor can see the latest state at any time.
Wire the checks into a script that emits a single JSON artifact:
#!/usr/bin/env bash
set -euo pipefail
evidence=()
last=$(grep "Accepted password" /var/log/auth.log 2>/dev/null | tail -1 || echo "missing")
evidence+=("audit_last_login=$last")
# Access control check
sudo_out=$(sudo -l -U app_user 2>&1 || true)
evidence+=("sudo_check=$sudo_out")
# TLS expiry
expiry=$(echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
evidence+=("tls_expiry=$expiry")
printf '{"generated":"%s","checks":{%s}}\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$(IFS==; echo "${evidence[*]}")" > /tmp/soc2-evidence.json
Run this from a cron job or CI pipeline and commit /tmp/soc2-evidence.json to a version‑controlled directory. An auditor can run jq . /path/to/soc2-evidence.json and see at a glance whether any check returned missing or an imminent expiry date.
The script assumes /var/log/auth.log exists and is readable by the process running it. In containerized or minimal‑host environments, that file may be absent or empty, causing audit_last_login=missing to appear in the evidence even though the system is logging to a different path. You only notice the gap when the JSON artifact consistently shows missing for a service that clearly produces audit data—because the script is looking at the wrong log pipe.
Detect when a SOC 2–relevant control drops out of compliance in real time, so you can remediate before an auditor sees a stale snapshot.
Deploy a daily systemd timer that reruns the same checks and exits non‑zero on any failure. Unit file:
[Unit]
Description=SOC 2 control watcher
After=network.target
[Timer]
OnCalendar=daily
Persistent=true
[Service]
Type=oneshot
ExecStart=/usr/local/bin/soc2-watcher.sh
RemainAfterExit=yes
The accompanying script exits with code 1 if grep "Accepted password" returns empty, if sudo -l prints the “Sorry” line, or if the openssl expiry date is within 30 days. systemd then sends an alert via your integrated incident platform (e.g., a webhook to PagerDuty or an email to the compliance channel). A Grafana dashboard can plot the daily pass/fail trend; a rising failure count signals systemic drift before the next audit window.
systemd timers only execute on hosts where the unit file is installed and the timer is enabled. In auto‑scaling node pools, new instances may launch without the watcher unit, or a recent OS image rebuild may wipe custom systemd units. You only discover the gap when a nightly run passes on two nodes but the third reports no timer activity, and the audit log shows “watcher timer not present” for the new node. Automate the unit deployment as part of your host‑baseline configuration management so it arrives on every node as part of the standard provisioning flow, not as a post‑install step.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy