Make Compliance Boring: 97% Automated Without Drama
Let’s turn audits into push-button checkouts with guardrails and receipts.
Compliance Is a Product Feature, Not a Penalty
Let’s say the quiet part out loud: customers buy trust. Compliance is one of the few ways we can show trust in a form people can verify. Done well, it stops being a set of hoops and turns into a product feature that reduces deal friction, accelerates onboarding, and keeps the lights on during growth spurts. The trap is treating compliance as a parallel universe of spreadsheets and ritual. Instead, we fold controls into our architecture, deployments, and day-to-day operations. Think of it like performance or reliability—nonfunctional, sure, but deeply tied to user happiness and revenue. When we make compliance invisible to engineers and obvious to auditors, we win twice.
Before we automate anything, we anchor on a baseline catalog that isn’t hand-wavy. That means mapping our risk profile and data flows to a real set of controls. If we’re handling PII, credentials, or money, we go beyond “passwords should be secure” and pick a recognized framework with clear control statements. For a thorough baseline, we like NIST SP 800-53; it’s wordy, but it’s specific and widely understood. We translate each control into a “What we do” and “How we prove it” statement. If our story for a control is basically “we try our best,” we don’t have a control—we have a wish. Finally, we line up stakeholders: compliance leads, security engineers, SREs, and product owners who own the shape of features. Compliance isn’t a weekend clean-up; it’s a thing we build in, the same way we build logging and tests.
Start With a Control Catalog You Actually Understand
Let’s avoid framework bingo. Pick one or two primary control sets that match our market and risk, then map everything else to them. If we’re building SaaS and our customers ask for SOC 2, we use SOC trust criteria as our north star but still lean on a practical, technical catalog for implementation. A great pragmatic choice is CIS Controls v8, which aligns to real operational tasks we can automate. We also define our system boundary: what’s in scope (prod clusters, CI runners, artifact repos) and what’s not (that forgotten demo VM—we either retire it or pull it into scope). We inventory assets, classify data, and pin down where sensitive information actually goes. No control catalog survives contact with unknown assets.
For each control, we write two lines: “Mechanism” and “Evidence.” Mechanism is the thing we do (e.g., enforce MFA on cloud accounts, deny privileged pods). Evidence is the artifact we can surface on demand (e.g., an exported policy list and last-30-days event logs). If a control doesn’t map to a measurable signal, we rewrite it until it does. Then we decide what’s human work and what’s machine work. Human work includes risk acceptance, change approvals for material changes, and initial design reviews. Machine work includes drift detection, policy enforcement, attestation and provenance, and immutable logs. Last, we set our ambition: the percentage of controls we’ll fully automate this quarter. “97% automated” isn’t a meme; it’s a forcing function.
Turn Policy Into Pipelines: Example CI Job
Compliance that doesn’t run in CI/CD is just a poster. We push enforcement left by making our pipeline the referee. Build steps become control points, and artifacts become receipts. We bake in static analysis, dependency hygiene, Infrastructure-as-Code linting, and signature verification. We don’t block on low-risk issues; we block on the things auditors and attackers both care about. We also generate machine-readable evidence in the same run, so we never sprint to build ad hoc screenshots the night before an audit. Bonus points for build provenance aligned to the SLSA framework.
Here’s a compact GitHub Actions job that enforces a few controls and captures artifacts:
name: compliance-gate
on:
push:
branches: [ main ]
pull_request:
jobs:
verify-and-scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Setup Tools
run: |
curl -sSL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
curl -sSL https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash
pip install semgrep
- name: IaC Scan (tfsec)
run: ./tfsec --format sarif --out tfsec.sarif .
- name: Dependency Scan (Trivy)
run: ./trivy fs --security-checks vuln,config --format sarif --output trivy.sarif .
- name: Semgrep App Rules
run: semgrep ci --sarif --output semgrep.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy.sarif
- name: Archive Evidence
uses: actions/upload-artifact@v4
with:
name: compliance-evidence
path: |
trivy.sarif
tfsec.sarif
semgrep.sarif
We tune thresholds per repo. Fail the build on criticals, warn on mediums, and always produce evidence. If teams get blocked, they can request a timeboxed exception with a ticket ID and expiration that the pipeline reads before it proceeds. No sticky notes, no endless waivers.
Evidence On Autopilot: Attest, Tag, and Retain
Auditors love receipts. Attackers hate them. We generate both provenance and attestations in the build so we can prove where code came from, which policies ran, and who approved. We avoid homebrew signatures and lean on modern tooling. For containers, provenance and signing via in-toto and cosign are fast wins, and short-lived OIDC tokens beat long-lived secrets every day. The point is to produce tamper-evident breadcrumbs without asking engineers to remember extra steps. Evidence must ride the happy path.
Operationally, we tag every artifact with commit SHA, build system, policy bundle version, and ticket link. We store build logs and SARIF in a write-once bucket with lifecycle retention. Access is via least privilege service accounts. Here’s a tiny taste with cosign:
# Build
docker build -t ghcr.io/acme/api:${GITHUB_SHA} .
# Sign with OIDC identity
cosign sign ghcr.io/acme/api:${GITHUB_SHA} --identity-token $ACTIONS_ID_TOKEN_REQUEST_TOKEN
# Generate and attach SLSA-style attestation
cosign attest --predicate provenance.json --type slsaprovenance ghcr.io/acme/api:${GITHUB_SHA}
# Verify (deployer step)
cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com ghcr.io/acme/api:${GITHUB_SHA}
We also snapshot policy states (which rules were enabled, their versions) and include them in the evidence archive. That way, when someone asks, “Were critical scans enabled for release 2025.10?” we answer with a file, not a shrug. For a pragmatic overview of signing and attestations, the Sigstore cosign docs are a great reference. Evidence gets boring when it’s automatic, and boring is exactly what we want.
Guardrails in the Cluster: Admission, Not Permission
The pipeline is our first guardrail; the cluster is our last. We don’t rely on “please don’t do that” Slack messages when Kubernetes can deny risky workloads automatically. Policy engines like Kyverno and OPA Gatekeeper let us codify rules: no :latest tags, read-only root filesystems, required labels for data classification, and no hostPath mounts. We keep policies small, testable, and versioned like code. Each policy corresponds to a control. The audit trail is the admission log plus a policy report, which is auditor-friendly and engineer-readable.
Here’s a Kyverno example that knocks out three classics:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: baseline-pod-security
spec:
validationFailureAction: enforce
rules:
- name: no-latest-tag
match:
any:
- resources:
kinds: [Pod, Deployment]
validate:
message: "Images must not use :latest"
pattern:
spec:
containers:
- image: "!*:latest"
- name: read-only-rootfs
match:
any:
- resources:
kinds: [Pod, Deployment]
validate:
message: "Containers must use readOnlyRootFilesystem"
pattern:
spec:
containers:
- securityContext:
readOnlyRootFilesystem: true
- name: required-labels
match:
any:
- resources:
kinds: [Pod, Deployment]
validate:
message: "Missing required labels"
pattern:
metadata:
labels:
data.sensitivity: "?*"
owner.team: "?*"
We start in audit mode to measure impact, then ratchet to enforce once teams are green. Reporters aggregate violations by namespace and team. If you want a library of battle-tested rules, the Kyverno policies catalog is gold. Guardrails that say “no” in the right places let teams move faster everywhere else.
People Still Matter: Lightweight Rituals That Stick
If we automate 97% of compliance, people own the remaining 3%—and it’s the important 3%. We need rituals that take minutes, not hours. Start with design reviews: a one-page RFC template that calls out data classification, inbound/outbound dependencies, and auth. The reviewer list should include a security or platform engineer, but the goal is minimal friction. For changes that touch regulated data or critical paths, we attach risk notes and define rollback. Don’t drag every feature into a change advisory board; reserve human approvals for material changes, and let pipelines handle the rest.
Next, make exceptions real. If someone needs a temporary waiver—say, allowing a privileged container for a vendor agent—they submit a ticket with risk, compensating controls, and an expiry date. Our admission controller reads the waiver list and only allows matching resources until expiry. No permanent hall passes. On the training side, ditch the annual three-hour clickfest. Do micro-lessons: five-minute videos or a short doc tied to incidents we actually had. We also bake compliance prompts into PR templates: “Does this touch PII?” “Have you updated the data flow diagram?” These nudges are more effective than an annual compliance scavenger hunt.
We report behavior, not blame. Dashboards show exception counts, average time-to-fix for policy violations, and percentage of controls automated per team. Friendly competition beats compliance theater. When folks see that their pipeline is quietly burning down risk, they’re happy to keep feeding it.
Measure What Matters: From Signals to Slice-and-Export
We measure compliance like we measure reliability: with signals that predict bad days. Our top metrics are simple. Control coverage: percentage of mapped controls with automated checks and evidence. Policy pass rate: proportion of builds and cluster admissions that meet our policy without exceptions. Mean time to remediate: how long it takes to fix a failed control in CI or a denied deployment. Exception rate: number of active waivers and how many are overdue. We also watch provenance coverage: percentage of deploys with verifiable signatures and attestations. If a metric doesn’t influence a decision, we don’t track it.
Dashboards live close to the work. CI reports roll up into the same place we track DORA metrics. Cluster policy reports publish to a shared namespace and a log sink for historical analysis. Every metric has a “show me” button that links to raw evidence: SARIF, admission logs, attestation bundles. We keep the export path boring: scheduled jobs dump CSV/JSON snapshots into a dedicated bucket by week and control family. Auditors don’t need our Grafana; they need consistent, timestamped, immutable records. We also score controls by risk weight so we can say, “We’re 97% automated overall, and 100% for high-risk controls.”
Finally, we practice. Once a quarter we run a mock audit: pick five controls at random, time how long it takes to retrieve evidence, and fix the slowest step. Fifteen minutes per control is fine; five minutes is better. If we can’t pull receipts quickly, we don’t really have them.
Make Audits Uneventful: A 90-Day Game Plan
Let’s turn this into a concrete, quarter-long plan that leaves us sipping coffee while the audit runs. Weeks 1–2: define the boundary, pick the control catalog, write “Mechanism” and “Evidence” lines for the top 25 controls. Tag systems and data flows, decommission the strays. Weeks 3–4: implement pipeline checks for code, deps, and IaC; produce SARIF and archive artifacts. Wire up provenance and signing for at least one critical service. Weeks 5–6: deploy admission controls in audit mode, tune noisy rules, and onboard two teams. Weeks 7–8: flip enforcement for low-risk, high-confidence policies; publish exception workflow with expiry; add evidence snapshots to a dedicated bucket. Weeks 9–10: build the dashboard, wire “show me” links, and generate weekly CSV exports. Weeks 11–12: do a mock audit on five controls, pull evidence end-to-end, and fix time-sinks.
We keep stakeholders in the loop with a single page that lists the current automation percentage, exception counts, and any open risks. No slide decks; just links to living systems. We map a handful of our controls to external frameworks we care about—NIST, SOC trust criteria, or PCI. If auditors need to crosswalk, we show our mapping. For a sense of rigor and architecture alignment without bloat, we also sanity-check choices against the CIS Controls v8 we started from and the higher-level intent of NIST SP 800-53 and the build integrity guidance from SLSA. After one quarter, we’ll have a pipeline that says “no” in the right places, a cluster that backs it up, and a tidy folder full of receipts. That’s how we make compliance boring—and keep it that way.




…and every new scanner in the default job adds minutes before anyone has looked at the UI. At my previous employer, we put three security tools behind every pull request and people started batching changes just to avoid the wait. In my current job, a dependency feed outage once turned a routine CSS fix into a two-hour release delay. The SARIF receipts are useful, but downloading installers with curl during the build is going to be its own reliability problem. Cache the tools, pin them, and keep the blocking checks narrow… otherwise the compliance gate becomes the thing everyone routes around. The exception path also needs to be faster than a feature team giving up and opening a ticket after lunch.
follow-up on expiring exceptions at 3am… who owns the faild deploy?
if an exception expires at 3am, the deploy should fail before it gets anywhere near the front end. we had 14 of these in one quarter, and nobody enjoyed being the accidental owner because a yaml date rolled over. give the service team a warning in daylight hours, then make the release owner explicit. otherwise its just a tiny compliance gremlin waiting to break the css fix.
The CI gate is only as boring as its failure messages, which is where these plans usually acquire teeth. I disagree with making compliance invisible to engineers, because a blocked build with no clear owner or remediation path is just invisible bureaucracy wearing a YAML hat. On my last team, we cut security-review back-and-forth by 38% after putting the exception expiry and the exact failing rule directly in the pull request check. Small correction: the sample only uploads trivy.sarif, so tfsec.sarif and semgrep.sarif are archived but not visible in GitHub’s code scanning UI. We also learned to test policy changes against a corpus of known-good repositories before rolling them out, after three noisy rule updates in two weeks. It is the same reason I test a new sourdough recipe on one loaf before inflicting it on the neighbours
I haven’t tried this model yet, but we plan to test evidence generation in the next roadmap cycle. I disagree that compliance is a product feature, because customers mostly notice it only when procurement is blocked