Security Compliance Engineering: ISO 27001 and SOC 2

What a security compliance program actually demands of an engineering team: control design versus operating effectiveness, evidence as a system, scoping decisions, and the failure modes that turn an audit into archaeology.

This page is for engineers who need to operationalize ISO 27001 and SOC 2 requirements through code, pipelines, and audit artifacts. It matters when you're preparing for an external audit, onboarding a new compliance framework for a SaaS product, or maintaining an ISMS that must survive continuous delivery. The following sections walk through concrete, repeatable engineering patterns: translating control objectives into verifiable checks, gathering and versioning evidence without manual spreadsheets, and gating pipeline changes against compliance state. Every example uses real, shipping tools or standard Unix/container interfaces so you can copy‑paste and adapt immediately.

Mapping Controls to Technical Checks

ISO 27001 Annex A and SOC 2’s Security criterion both require that technical configurations enforce stated objectives. The engineering pattern is always the same: define the control’s intent, encode that intent as a checkable state, and run a command that exits non‑zero when the state diverges.

Verify TLS hardening against a live endpoint

ISO 27001 A.13 and SOC 2 Security both expect that TLS implementations reject weak ciphers and use strong key sizes. A live‑endpoint test needs no agents on the host:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -text 2>/dev/null | head -20

Typical output includes notBefore and notAfter dates, the public‑key algorithm, and key‑size fields. If the key size is below 2048 bits or the signature algorithm is SHA‑1, the control is failing.

To check that only approved ciphers are offered:

openssl s_client -connect example.com:443 -servername example.com -cipher "AES256-GCM-SHA384" </dev/null 2>/dev/null | grep -i "Cipher is"

A blank return means the cipher is not in the server’s offered list—desired. If DES‑CBC3‑SHA appears in an unfiltered s_client session, that is a concrete finding an auditor would record.

Check HTTP security headers

SOC 2’s “Security” criterion frequently references headers that harden browsers against common attacks. Verify with a single curl call:

curl -sI https://example.com | grep -E "Strict-Transport-Security|X-Content-Type-Options|X-Frame-Options"

Expected output lines such as Strict-Transport-Security: max-age=31536000; includeSubDomains map directly to a control objective. Missing lines indicate a configuration gap that creates an immediate audit ticket.

Map an access‑control objective to a policy JSON check

Many frameworks expect that “only authorized processes may read secrets.” A typical IAM‑style policy document has effect, action, and resource fields. The following jq filter isolates overly permissive entries:

jq '.Statement[] | select(.Effect == "Allow" and .Action == "s3:*")' iam-policy.json

If the command returns any objects, the policy grants unrestricted S3 access—a clear control violation. Teams typically add a justification comment or tighten the Resource ARN, then re‑run the filter to confirm remediation.

Automating Evidence Collection

Auditors require a reproducible snapshot of the control state at a point in time. The most reliable engineering pattern is to treat each evidence artifact as a Git‑tracked file generated by a scheduled CI job.

Generate and version a compliance report

#!/usr/bin/env bash
set -euo pipefail


trivy image --format json --output compliance-report.json .

# 2. Commit the artifact with an immutable timestamp tag
git add compliance-report.json
git commit -m "chore: compliance evidence $(date -u +%Y-%m-%dT%H:%M:%SZ)"
git tag evidence-$(date -u +%Y%m%d%H%M%S) -m "auto‑generated evidence"
git push --follow-tags origin main

When an auditor asks for the state of controls on 2024‑08‑31, run:

git show evidence-20240831T120000Z:compliance-report.json

The exact JSON report that was in force at that moment is reproduced—no spreadsheet merges, no “it worked on my machine” ambiguity.

Emit a non‑zero exit code when findings exceed threshold

trivy image --severity HIGH,CRITICAL --exit-code 1 . || exit 1

If the image contains any HIGH or CRITICAL vulnerability, trivy prints:

Fatal: image contains 3 HIGH severity vulnerabilities

and exits with code 1, causing the surrounding script or pipeline to fail. If the image is clean, the command exits 0 and execution continues. This pattern is the basis for hard gating in CI pipelines.

CI/CD Gating and Policy Enforcement

Compliance checks belong in the same automation that runs unit tests and linting. The gate can be hard (pipeline blocks) or soft (results posted, merge allowed).

Hard gate in a pipeline step

A minimal hard gate ensures no non‑compliant container image reaches production:

- name: Run compliance scan
  run: |
    trivy image --severity HIGH,CRITICAL --exit-code 0 || exit 1

The || exit 1 forces the workflow step to fail if the scan reports findings above the configured severity. A failed step blocks the job, and the audit trail records the exact trivy version, severity filter, and output snapshot.

Soft gate with SARIF output

When the organization prefers to surface findings without blocking merges, the scanner can emit SARIF format:

trivy image --format sarif --output trivy-report.sarif .

The CI system can then post the SARIF results as a pull‑request comment. The merge proceeds, but the findings remain visible to reviewers and auditors. This pattern satisfies SOC 2’s “Security” requirement for continuous monitoring while maintaining delivery velocity.

Local Makefile target for quick verification

.PHONY: compliance
compliance:
	@trivy image --severity HIGH,CRITICAL --exit-code 1 . || (echo "Compliance check failed"; exit 1)

Run make compliance locally or in any CI runner that has make and trivy on PATH. The target exits non‑zero if the image contains HIGH or CRITICAL vulnerabilities, producing the exact error text Fatal: image contains … that an auditor would expect to see in a CI log.

Sharp Edges: What Silently Fails

Stale evidence tags

If the evidence‑generation job that creates evidence-*.json tags fails silently—e.g., git commit aborts because of a missing GPG key—the tag never appears. An auditor requesting evidence-20240801 receives a “tag not found” error. The fix is to ensure the job runs with GIT_STRATEGY=none or proper credentials, and to verify the tag list after each run with git tag --list 'evidence-*'.

Scanner false positives and negatives

trivy may report a vulnerability that is actually a language runtime version baked into the base image, not a runtime dependency. The engineering team learns to tag such findings as ignored with a justification comment in the scanner configuration, otherwise the pipeline stays broken on every build. Conversely, a missing vulnerability in a distro‑provided image can go undetected if the scanner’s vulnerability feed is not refreshed; scheduling the scanner image pull with --pull always mitigates this.

Configuration drift between environments

A TLS check that passes against a staging endpoint because of a different certificate‑authority bundle may fail in production. The pattern is to pin the exact CA bundle version in the check script and compare outputs side‑by‑side:

diff <(openssl s_client -connect staging.example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer) \
     <(openssl s_client -connect prod.example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer)

A diff output signals a drift that must be reconciled before the next audit cycle.

Log retention gaps

SOC 2 requires logs to be retained for a minimum period. If systemctl journal rotates before the retention window, journalctl --list-boots will show older boots purged. The engineering remedy is to forward structured logs to an immutable store—a write‑once object bucket or a dedicated log aggregation service—rather than relying on the host’s journald, which is subject to space‑based rotation.

Pipeline dependency freezes

A compliance scanner that pins a specific trivy version (e.g., trivy:0.45.0) may break when a new OS package update pulls in trivy:0.46.0 from the distro repo. Pin the binary version explicitly in the Docker image or CI runner image, and verify the sha256 checksum on first pull:

docker pull trivy:0.45.0@sha256:3a7f...

This ensures that every pipeline instance uses the exact scanner binary the team validated against the compliance framework.

Wrap‑up

ISO 27001 and SOC 2 are not checklist exercises; they are engineering constraints that must survive the same continuous‑delivery pressures as any feature. The patterns above—verifying controls with real commands, versioning evidence in Git, and gating pipelines on scanner exit codes—provide a concrete, repeatable foundation. When a control slips, the failure mode is usually one of the sharp edges described: a missing tag, a stale scanner feed, or a drift between environments. Detecting and fixing those patterns before an auditor asks is the difference between a smooth review and a findings report.

This page was rewritten on 16 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.