The Only Compliance That Scales Is the Kind Your CI/CD Can Prove

Turn policy PDFs into guardrails, automated checks, and audit-ready evidence—without turning your release train into a museum exhibit.

Compliance that lives outside the pipeline is just future downtime with better formatting.
Back to all posts

The audit always shows up during your worst week

I’ve watched this movie more times than I’d like: an incident is still warm, leadership wants answers, and then someone forwards an email that starts with “As part of our upcoming SOC 2 / PCI / HIPAA audit…” Suddenly your team is digging through Slack threads, Jira tickets, and half-remembered kubectl commands trying to prove you meant to be compliant.

The painful truth: manual compliance doesn’t fail during normal weeks. It fails when the system is stressed—high change volume, partial outages, staff turnover, or a dependency compromise. That’s why “we’ll document it later” turns into “we can’t ship today.”

What actually works is boring and mechanical: build compliance checking into the deployment pipeline so every change emits evidence automatically.

Translate policy PDFs into executable guardrails

Most policies are written like:

  • “Production data must be encrypted at rest.”
  • “Secrets must not be stored in source control.”
  • “Only approved regions may process regulated data.”

That’s fine for auditors. It’s useless for a pipeline until you turn it into specific, machine-testable statements.

A good translation pattern:

  1. Policy (human): “No public buckets.”
  2. Guardrail (design): “S3 buckets must block public ACLs and public policies.”
  3. Check (automation): Terraform plan must fail if aws_s3_bucket_public_access_block is missing.
  4. Proof (evidence): Store the policy evaluation output alongside the plan and the commit SHA.

Here’s what that looks like with OPA/Conftest against Terraform plan JSON.

terraform plan -out tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json -p policy/terraform

Example OPA policy (minimal, but effective):

package terraform.s3

deny[msg] {
  some r
  r := input.resource_changes[_]
  r.type == "aws_s3_bucket"
  not has_public_access_block[r.name]
  msg := sprintf("S3 bucket %s missing public access block", [r.name])
}

has_public_access_block[name] {
  pab := input.resource_changes[_]
  pab.type == "aws_s3_bucket_public_access_block"
  pab.change.after.bucket == name
  pab.change.after.block_public_acls == true
  pab.change.after.block_public_policy == true
}

This is the “unlock” moment for a lot of teams: compliance becomes test output, not interpretation.

Put checks where they hurt the least (and catch the most)

If you only enforce compliance at the end of the pipeline, you get the worst of both worlds: late failures and angry engineers. The trick is layering:

  • Pre-commit / pre-push: cheap checks that prevent embarrassing leaks
    • gitleaks for secrets
    • pre-commit hooks for obvious footguns
  • Pull request: policy and security checks with fast feedback
    • semgrep for insecure patterns
    • checkov/tfsec for IaC misconfigurations
  • Build: generate security artifacts
    • syft for SBOM
    • trivy or grype for image scanning
  • Deploy / admission: enforce runtime constraints
    • Kyverno or OPA Gatekeeper policies
    • signature verification (cosign)

A concrete Kubernetes admission example using Kyverno to block privileged containers (this is one of those controls that’s almost always “no exceptions” in regulated environments):

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged
spec:
  validationFailureAction: Enforce
  rules:
    - name: privileged-containers
      match:
        resources:
          kinds:
            - Pod
      validate:
        message: "Privileged containers are not allowed"
        pattern:
          spec:
            containers:
              - securityContext:
                  =(privileged): "false"

This prevents “works on my laptop” YAML from ever becoming a production problem.

Automated proofs: make auditors bored on purpose

The best compliance outcome isn’t passing the audit. It’s reducing the number of times the audit interrupts engineering.

You do that by generating and retaining proofs per change:

  • SBOM (CycloneDX or SPDX) tied to the build
  • Vulnerability scan report with thresholds and rationale
  • Provenance / build attestations (SLSA-style)
  • Signature on the artifact (cosign)
  • Change record: who approved, what ran, what passed, what deployed

In practice, we see teams store these artifacts in:

  • S3 with Object Lock (WORM retention)
  • GCS with Bucket Lock
  • An internal “evidence” repo (works, but immutable storage is better)

Here’s a pragmatic cosign flow that signs an image and attaches an SBOM attestation:

# Generate SBOM
syft packages ghcr.io/acme/payments:1.2.3 -o cyclonedx-json > sbom.json

# Sign the image (keyless via OIDC)
cosign sign ghcr.io/acme/payments:1.2.3

# Attach SBOM as an attestation
cosign attest --predicate sbom.json --type cyclonedx \
  ghcr.io/acme/payments:1.2.3

Now your “proof” is cryptographically bound to the artifact. When someone asks “what was in production on Jan 3rd?”, you’re not reconstructing history—you’re querying it.

A CI/CD example that enforces policies and emits evidence

Below is a trimmed (but real-world) GitHub Actions workflow that:

  • blocks secrets
  • runs IaC policy checks
  • builds an image
  • produces SBOM + vuln scan
  • signs and attests
  • uploads evidence artifacts
name: build-and-comply
on:
  pull_request:
  push:
    branches: ["main"]

permissions:
  id-token: write
  contents: read
  packages: write

jobs:
  comply:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Secret scan
        uses: gitleaks/gitleaks-action@v2

      - name: Terraform plan + policy checks
        run: |
          terraform -version
          terraform init
          terraform plan -out tfplan
          terraform show -json tfplan > tfplan.json
          conftest test tfplan.json -p policy/terraform

      - name: Build image
        run: |
          docker build -t ghcr.io/acme/payments:${{ github.sha }} .
          docker push ghcr.io/acme/payments:${{ github.sha }}

      - name: Generate SBOM
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
          syft packages ghcr.io/acme/payments:${{ github.sha }} -o cyclonedx-json > sbom.json

      - name: Vulnerability scan (fail on Critical)
        run: |
          curl -sSfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
          trivy image --severity CRITICAL --exit-code 1 ghcr.io/acme/payments:${{ github.sha }}

      - name: Sign + attest (keyless)
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          curl -sSfL https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 -o /usr/local/bin/cosign
          chmod +x /usr/local/bin/cosign
          cosign sign ghcr.io/acme/payments:${{ github.sha }}
          cosign attest --predicate sbom.json --type cyclonedx ghcr.io/acme/payments:${{ github.sha }}

      - name: Upload evidence
        uses: actions/upload-artifact@v4
        with:
          name: compliance-evidence-${{ github.sha }}
          path: |
            tfplan.json
            sbom.json

Is this “fully compliant” for every framework? No. But it’s the skeleton that lets you add controls without slowing delivery to a crawl. You can extend it with:

  • SAST (semgrep ci) on PRs
  • DAST in staging
  • policy checks for Kubernetes manifests
  • evidence upload to an immutable bucket

Speed vs regulated data: stop pretending it’s one lever

The teams that struggle here treat compliance like a binary: either you’re fast or you’re safe. In regulated systems (PCI, HIPAA, GDPR), you need a more grown-up model:

  • Non-negotiable gates (hard fail)
    • leaked secrets
    • public data exposure
    • privileged containers
    • missing encryption controls
  • Risk-based gates (context-aware)
    • CVEs: block on exploitable criticals, not “any CVE”
    • dependency licensing: allowlist + review queue
  • Exceptions with an expiry date
    • waiver requires: owner, ticket link, justification, compensating control, expiration

This is where I’ve seen “compliance automation” fail: teams wire up trivy and block on all severities, and suddenly nobody can deploy because the base image has a medium CVE in libssl that isn’t reachable. Engineers learn to bypass the system, and your control becomes theater.

What actually works:

  1. Define severity thresholds aligned to business impact (tie to your SLOs and threat model).
  2. Separate build-time issues from runtime-exploitable issues.
  3. Measure pipeline friction like you measure latency:
    • PR cycle time
    • deployment frequency
    • change failure rate
    • audit prep hours

If you can’t quantify it, you can’t tune it.

What GitPlumbers does when your pipeline is already on fire

At GitPlumbers, we usually get called after a “quick compliance project” turned into a six-month freeze, or after AI-assisted changes (a.k.a. vibe coding in production) sprayed noncompliant config across repos.

Our approach is practical:

  • Start with the top 10 controls that actually reduce risk in your environment
  • Implement policy-as-code with OPA/Conftest and/or Kyverno
  • Add artifact proof generation (SBOM, signatures, attestations)
  • Build an exception workflow that doesn’t require a week of meetings
  • Leave you with a pipeline your team can own (not a consultant pet project)

If you’re trying to ship fast and pass audits without heroics, that’s the work.

Related Resources

Key takeaways

  • Treat compliance requirements as executable rules (`policy-as-code`), not tribal knowledge.
  • Put guardrails at multiple layers: pre-commit, CI, image build, and cluster admission.
  • Generate automated proofs (SBOMs, signatures, attestations, approvals) as pipeline artifacts and store them immutably.
  • Fail fast on high-signal controls (secrets, public S3 buckets, privileged containers), and route gray areas to exception workflows.
  • Use risk-based gating: block releases on exploitability/criticality, not on “any CVE exists.”
  • Auditors love boring: a consistent evidence trail beats heroic spreadsheet reconstructions every time.

Implementation checklist

  • Inventory your top 10 non-negotiable controls (secrets, encryption, network exposure, IAM least privilege, logging).
  • Translate each control into a machine-checkable rule (OPA/Conftest, Kyverno, Semgrep, Checkov).
  • Decide where each check runs: `pre-commit`, PR, build, deploy, or admission.
  • Add artifact generation: SBOM (`syft`), vuln report (`trivy`/`grype`), provenance/attestations (`cosign`).
  • Store evidence in immutable storage (S3 Object Lock / GCS Bucket Lock) with retention policies.
  • Implement an exception process (time-boxed waivers, ticket link, owner, expiry).
  • Measure impact: PR cycle time, deployment frequency, change failure rate, audit prep hours.

Questions we hear from teams

Do we need OPA for everything?
No. Use the simplest tool that produces repeatable results. `OPA`/`Conftest` shines for IaC and structured inputs (Terraform plan JSON, Kubernetes manifests). For Kubernetes-native admission controls, `Kyverno` is often faster to roll out. For code patterns, `semgrep` is usually the right hammer.
How do we avoid turning the pipeline into a 45-minute compliance gate?
Layer checks and fail early. Run quick high-signal checks (secrets, obvious misconfigs) on PRs, run heavier scans in parallel, and only block on clearly defined thresholds (e.g., exploitable criticals). Treat performance as a first-class requirement: measure job timings and set budgets.
What counts as “automated proof” for auditors?
Artifacts tied to a specific commit/build that show controls ran and passed: scan outputs, policy evaluation results, SBOMs, signatures/attestations, and an immutable record of approvals and deployments. The key is traceability: commit SHA → pipeline run → artifact digest → deployment.
We use ArgoCD/GitOps—where do these checks belong?
Still do most checks in CI on PRs, but add an admission layer in the cluster: enforce signature verification and baseline runtime policies. GitOps reduces drift; admission control prevents bad manifests (or unsigned images) from ever becoming reality.

Ready to modernize your codebase?

Let GitPlumbers help you transform AI-generated chaos into clean, scalable applications.

Get a pipeline compliance teardown See how GitPlumbers fixes risky CI/CD and AI-assisted code

Related resources