07 · Compliance & Audit Considerations for Ops¶
Everything built so far — IaC, backups, patching, incident response — turns out to also be most of what auditors ask for when a system needs to meet a compliance framework (SOC 2, ISO 27001, PCI-DSS, HIPAA, or an internal security review). This module reframes those existing practices through a compliance/audit lens, and covers the parts that are genuinely new: access control, audit logging, and evidence collection.
Compliance is mostly "prove you do what you already claim to do"¶
The biggest misconception about compliance work: it's not usually about adopting brand-new practices, it's about evidencing practices that should already exist for good operational reasons. Most frameworks map cleanly onto earlier modules:
| Control area | Framework asks (paraphrased) | Where this is already covered |
|---|---|---|
| Change management | Changes are reviewed and tracked | IaC + PR review (Level 3 module 05) |
| Backup & recovery | Backups exist, are tested, are recoverable within a stated time | Level 3 module 03 |
| Access control | Access is least-privilege, reviewed, and revocable | This module, below |
| Vulnerability management | Known vulnerabilities are tracked and remediated on a schedule | Level 4 module 04 |
| Incident response | A documented process exists and incidents are logged | Level 4 module 03 |
| Monitoring | Systems are monitored, and alerts reach a responsible party | Level 3 module 09 |
| Audit logging | Who did what, when, is recorded and tamper-resistant | This module, below |
If a team already runs the earlier modules' practices well, most of a compliance audit becomes "collect the evidence that's already being generated," rather than building new capability under deadline pressure.
Least-privilege access control¶
Principle of least privilege: every identity (human or service) gets only the permissions it needs to do its job, no more, and access is reviewed and revoked when no longer needed.
# Terraform IAM policy: scoped to exactly what the deploy role needs,
# not a blanket AdministratorAccess grant
resource "aws_iam_policy" "deploy_role_policy" {
name = "deploy-role-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"ecs:UpdateService",
"ecs:DescribeServices",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
]
Resource = "arn:aws:ecs:*:123456789012:service/production/orders"
}]
})
}
# Kubernetes RBAC: an on-call engineer can read logs/describe pods,
# but cannot delete resources or read secrets, in production
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { namespace: production, name: oncall-readonly }
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "events"]
verbs: ["get", "list", "watch"]
# deliberately no "secrets" resource, no "delete"/"create"/"update" verbs
Access reviews — a recurring (usually quarterly) process where access grants are checked against "does this person/service still need this," and revoked if not:
# quarterly access review: list everyone with production database access,
# cross-reference against current team roster and role
psql -c "SELECT rolname FROM pg_roles WHERE rolcanlogin AND rolname LIKE 'prod_%';"
# → manually or via script, confirm each against HR/team data; revoke stale grants
Access that was correctly granted (an engineer needed prod DB access for a migration) becomes a compliance gap the moment it's no longer needed and nobody revokes it — this is exactly the kind of drift Level 3 module 06 covers, applied to permissions instead of infrastructure config.
Audit logging: who did what, when, immutably¶
An audit log records security-relevant actions — logins, permission changes, data access, infrastructure changes — in a way that's tamper- resistant (a compromised account, or even a malicious insider, shouldn't be able to quietly edit their own trail).
# CloudTrail (AWS): logs every API call, shipped to a separate account's
# S3 bucket with write-once semantics so the source account can't delete it
aws cloudtrail create-trail \
--name org-audit-trail \
--s3-bucket-name company-audit-logs-locked-account \
--is-multi-region-trail \
--enable-log-file-validation
-- Database audit logging: who read/modified sensitive tables, not just
-- application-level logs (which a compromised app can't be trusted to log honestly)
-- (Postgres pgaudit extension)
CREATE EXTENSION pgaudit;
ALTER SYSTEM SET pgaudit.log = 'read, write, role';
ALTER SYSTEM SET pgaudit.log_relation = on;
-- then filter to sensitive tables via pgaudit.role settings
The separate account/bucket with write-once/immutable storage detail matters: audit logs stored in the same account/system they're auditing can be altered or deleted by whoever compromises that system, defeating the purpose. Shipping them to an isolated, restricted-access destination (and enabling log file integrity validation) is what makes the trail trustworthy as evidence, not just a debugging convenience.
Evidence collection: build it into the pipeline, don't scramble at audit time¶
The single highest-leverage compliance habit: generate audit evidence as a byproduct of normal operations, continuously, rather than manually assembling it under deadline when an auditor asks.
# CI: every production deploy automatically produces an evidence artifact —
# who approved it, what changed, when, linked to the PR
- name: record deploy evidence
run: |
cat <<EOF >> deploy-evidence.log
timestamp: $(date -Iseconds)
deployed_by: ${{ github.actor }}
approved_by: $(gh pr view ${{ github.event.pull_request.number }} --json reviews -q '.reviews[0].author.login')
commit: ${{ github.sha }}
service: orders
EOF
aws s3 cp deploy-evidence.log s3://company-audit-logs/deploys/$(date +%s).log
# quarterly automated evidence pull, instead of manual screenshots the week of the audit
./scripts/collect-evidence.sh --quarter 2026-Q3
# → pulls: backup test results (Level 3 module 03), patch compliance %
# (Level 4 module 04), access review sign-offs, incident postmortems
# (Level 4 module 03), all into one dated evidence package
An organization that can run collect-evidence.sh and produce a complete,
already-true evidence package at any time is in a fundamentally different
(and far less stressful) position than one that starts building evidence
from scratch when the auditor's request lands.
Data classification and handling¶
Not all data needs the same controls — classify it, and apply controls proportional to sensitivity:
Public: marketing content — no special controls
Internal: internal docs, non-sensitive logs — access limited to employees
Confidential: customer PII, financial data — encryption at rest + in
transit, access logged (pgaudit above), least-privilege
Restricted: payment card data (PCI scope), health records (HIPAA) —
additional controls per the specific framework (e.g. PCI's
network segmentation requirements, tokenization instead of
storing raw card numbers at all where possible)
Getting this classification wrong in the "under-classify" direction (treating confidential data as merely internal) is the more common and more dangerous mistake — it means weaker controls are applied to data that needed stronger ones, discovered only during an incident or an audit finding.
How It Actually Works¶
Why CloudTrail's cross-account destination is what makes a trail resistant to a compromised admin, not merely "best practice." IAM permissions in AWS are evaluated per-account; an identity with admin rights in the account being audited can, by definition, modify or delete resources (including a same-account S3 bucket and its bucket policy) in that account. Shipping the trail to a separate AWS account whose only relationship to the source account is "receives write-once log objects" means deleting the trail requires compromising a second, more tightly restricted account with a much smaller attack surface — the security property comes specifically from the account boundary, since IAM has no concept of "this admin can do everything except touch the audit log" that would be equally trustworthy from a single account.
Why pgaudit logging at the database layer catches what
application-level logging structurally cannot. Application logs record
what the app chose to log about a request — a compromised or buggy
application can simply not log a query it doesn't want traced, or an
engineer with direct database credentials can bypass the application
entirely and query the table straight through psql. pgaudit hooks into
Postgres's own executor, logging every statement that actually reaches the
query engine regardless of which client or credential issued it — so it
captures the direct-access path the application-log approach has no
visibility into at all, which is exactly the gap auditors probing "who
read this PII table" are checking for.
Why access review has to be a recurring process, mechanically tied to
drift, not a one-time grant audit. An IAM/RBAC grant is a static
artifact that stays valid until someone explicitly revokes it — nothing in
the permission model itself expires access when the underlying need
disappears (a project ends, a role changes, an engineer moves teams).
This is structurally identical to Level 3 module 06's configuration drift:
the declared state ("this person needs prod DB access") silently diverges
from reality ("they haven't needed it in 4 months") with no automatic
signal, which is why access review has to be a scheduled, recurring check
against current need — exactly the same "run the check regularly, don't
assume the initial grant is still correct" discipline as a nightly
terraform plan.
Exercise¶
- Pick one compliance framework relevant to a system you know (or SOC 2 as a generic default) and map three of its control areas to practices from earlier modules, identifying what evidence each practice already produces (or would need to start producing).
- Write an IAM policy or RBAC role scoped to the minimum permissions a specific role (on-call engineer, CI deploy pipeline) actually needs, and list what a broader "just give them admin" grant would have exposed unnecessarily.
- Design an audit logging setup for one sensitive action in your system (e.g. reading a customer PII table, or changing a production permission) — specify what's logged, where it's stored, and why that storage location is resistant to tampering by whoever's being audited.
- Write a
collect-evidence.shoutline (pseudocode is fine) that would assemble, on demand, evidence for backup testing, patch compliance, and access reviews from the practices in earlier modules.