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:
- Policy (human): “No public buckets.”
- Guardrail (design): “S3 buckets must block public ACLs and public policies.”
- Check (automation): Terraform plan must fail if
aws_s3_bucket_public_access_blockis missing. - 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/terraformExample 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
gitleaksfor secretspre-commithooks for obvious footguns
- Pull request: policy and security checks with fast feedback
semgrepfor insecure patternscheckov/tfsecfor IaC misconfigurations
- Build: generate security artifacts
syftfor SBOMtrivyorgrypefor image scanning
- Deploy / admission: enforce runtime constraints
KyvernoorOPA Gatekeeperpolicies- 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 (
CycloneDXorSPDX) 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:
S3with Object Lock (WORM retention)GCSwith 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.3Now 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.jsonIs 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 PRsDASTin stagingpolicychecks 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:
- Define severity thresholds aligned to business impact (tie to your SLOs and threat model).
- Separate build-time issues from runtime-exploitable issues.
- 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/Conftestand/orKyverno - 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.
