Make Compliance Boring: 97% Automated Without Drama

compliance

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.

Share