Ship Policy, Not PDFs: Secure Coding Standards That Compile in CI

If your secure coding standard lives in Confluence, it’s not a control—it’s a suggestion. Turn intent into guardrails, checks, and automated proofs that ship with every PR.

Policies that don’t compile into CI checks are just suggestions—and suggestions don’t survive Friday deploys.
Back to all posts

The day the policy PDF met main

I walked into a SOC 2 readiness review where the team had a 32-page “Secure Coding Standard” in Confluence and a production outage caused by a hardcoded token merged the night before. Classic. The PDF said “no secrets in code”; the repo said AWS_SECRET_ACCESS_KEY=... in a test helper. No pre-commit hook, no PR check, no gate. Auditors were unimpressed. Developers were annoyed.

I’ve seen this movie at fintechs, healthtechs, and old-school enterprises: policy as prose, not as code. What works is simple and boring: translate intent into guardrails that run where developers live, prove it automatically, and keep lead time intact.

Translate intent into guardrails developers can’t ignore

Policies are just hypotheses until they’re tests. Start with a handful that matter and map them to controls:

  • No plaintext secrets in code (SOC 2 CC6.6, HIPAA 164.312(a))
    • Pre-commit: detect-secrets or gitleaks
    • CI: repo-wide scan on every PR; allow baselines with TTL and ticket
  • Regulated data never on public endpoints (GDPR, HIPAA)
    • IaC policy-as-code: OPA/Rego via conftest or Gatekeeper
    • Disallow public S3 buckets or public LoadBalancers for data-classification: regulated
  • Dependencies patched within 14 days for critical vulns (PCI DSS 6.3.3)
    • SBOM + vulnerability scan (syft + grype); CI fails if any critical older than 14 days
    • Renovate for fast, low-friction updates
  • Network egress controls for services touching PII
    • Kubernetes policies (Gatekeeper/ Kyverno) and egress proxies; fail manifests that bypass

Write policies in the tools devs already use. Keep the rule set small, crisp, and noisy-once.

Wire it into the developer flow (not a security sidecar)

If it isn’t in the PR, it doesn’t exist. Minimal friction path that works:

  1. Pre-commit for cheap, noisy findings (secrets, obvious code smells)
  2. PR checks for SAST, IaC, and dependency risk with SARIF results no one has to “go find”
  3. Branch protection requires green checks and review from CODEOWNERS of policy/
  4. CD gates only on high-signal proofs (signed artifact + attestation)
  5. Exceptions in-repo with TTL, reviewers, and auto-expiry

Example .pre-commit-config.yaml:

repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ["--baseline", ".secrets.baseline"]
  - repo: https://github.com/returntocorp/semgrep
    rev: v1.78.0
    hooks:
      - id: semgrep
        args: ["--config", ".semgrep.yml", "--error"]

Protect the critical bits with CODEOWNERS:

# Require security to review policy and infra changes
policy/**        @security-team
infrastructure/** @platform-team @security-team

Policy-as-code you can paste today

Semgrep rule to catch Python HTTP calls without timeouts (a real outage vector):

# .semgrep.yml
rules:
  - id: py-requests-no-timeout
    patterns:
      - pattern: requests.$METHOD($ARGS)
      - pattern-not: requests.$METHOD(..., timeout=$TIMEOUT, ...)
    message: "requests.* call without timeout"
    severity: WARNING
    languages: [python]
    metadata:
      cwe: CWE-400

Rego policy to stop public S3 buckets when tagged regulated=true:

# policy/s3_public.rego
package s3

deny[msg] {
  input.resource_type == "aws_s3_bucket"
  input.tags.regulated == "true"
  public := input.acl == "public-read" or input.acl == "public-read-write"
  public
  msg := sprintf("regulated bucket %s cannot be public (acl=%s)", [input.name, input.acl])
}

Run with Conftest in CI over Terraform plans:

terraform init && terraform plan -out=tf.plan
terraform show -json tf.plan | conftest test --policy policy/ -

GitHub Actions: SAST, IaC, SARIF upload, and gate on failures:

name: security
on: [pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.11' }
      - name: Semgrep SAST
        uses: returntocorp/semgrep-action@v1
        with:
          config: .semgrep.yml
          generateSarif: true
          publishSarif: true
      - name: Terraform plan
        run: |
          terraform -chdir=infrastructure init
          terraform -chdir=infrastructure plan -out=tf.plan
          terraform -chdir=infrastructure show -json tf.plan > plan.json
      - name: Conftest OPA
        uses: instrumenta/conftest-action@v1
        with:
          files: infrastructure/plan.json
          policy: policy/

Automated proofs: attestations, SBOM, and “show me” for auditors

Auditors don’t want your feelings; they want evidence tied to a commit. Produce machine-verifiable proofs:

  • SBOM per image with syft (CycloneDX or SPDX), signed and attached to the artifact
  • Vulnerability scan with grype, fail on policy (e.g., critical older than 14 days)
  • Build provenance (SLSA-style) and policy attestation with Sigstore cosign
  • SARIF results uploaded to code scanning for traceability

Action snippet to build, SBOM, scan, and attest:

jobs:
  build_and_prove:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # for keyless signing
      packages: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: |
          docker build -t ghcr.io/acme/payments:${{ github.sha }} .
          echo "IMAGE=ghcr.io/acme/payments:${{ github.sha }}" >> $GITHUB_ENV
      - name: Generate SBOM (CycloneDX)
        uses: anchore/sbom-action@v0
        with:
          image: ${{ env.IMAGE }}
          format: cyclonedx-json
          artifact-name: sbom.cdx.json
      - name: Scan vulnerabilities
        uses: anchore/scan-action@v3
        with:
          image: ${{ env.IMAGE }}
          fail-build: true
          severity-cutoff: critical
      - name: Sign image and attest policy
        uses: sigstore/cosign-installer@v3
      - name: Cosign sign
        run: cosign sign --yes $IMAGE
      - name: Cosign attest (policy)
        run: |
          echo '{"semgrep": "pass", "opa": "pass", "sbom": "attached"}' > policy.json
          cosign attest --yes --predicate policy.json --type slsaprovenance $IMAGE

Now your artifact carries cryptographic proofs that controls ran and passed. ArgoCD or your deployment system can verify signatures at pull-time. If you want belt-and-suspenders, store attestations in an evidence bucket keyed by commit SHA and release tag.

Keep delivery fast: tiered risk and progressive enforcement

This is where most teams faceplant. They ship 100 rules overnight and wonder why devs revolt. Here’s what works:

  • Tier services by data sensitivity: regulated (PII/PHI/PCI), internal, public. Only hard-fail regulated initially.
  • Progressive rollout: two weeks audit-only (comment), two weeks soft-fail (non-blocking), then hard-fail for regulated tier.
  • Policy canaries: enable new rules on 10% of repos first, measure false positives, then ramp.
  • Exception workflow: YAML in-repo with reason, ticket, and expires that CI enforces.

Example exception file and check:

# .policy-exceptions.yaml
exceptions:
  - id: py-requests-no-timeout
    repo: payments-api
    reason: 3rd-party SDK lacks timeout param
    ticket: SEC-1842
    expires: 2025-03-31

Then in CI, a tiny script filters Semgrep findings if an unexpired exception exists. No emails. No spreadsheets. Everything is code-reviewed and time-boxed.

Track the right metrics:

  • PR cycle time (P50/P90) before/after rollout
  • Failed check rate per repo (should drop after week 2)
  • Time-to-patch critical vulns (target < 14 days)
  • Mean age of exceptions (target < 14 days)

What good looks like in 90 days

I’ve run this playbook at a HIPAA SaaS and a PCI fintech. The curve is consistent:

  • Days 0–30: 5 rules in audit mode (secrets, SAST hotspots, IaC egress, SBOM+scan, license allowlist). Baseline created, noisy repos identified. PR cycle time unchanged.
  • Days 31–60: Soft-fail then hard-fail for regulated tier. Secrets in PRs drop to near-zero. Critical vuln backlog drops 60% as Renovate merges safely.
  • Days 61–90: Attestations verified at deploy; auditor evidence is a URL and a SHA. One-click export of SBOM, SARIF, and policy results. Exceptions < 10, mean age under two weeks. No measurable impact to lead time for non-regulated tier.

If a control can’t be proven automatically, it doesn’t exist during an audit and it probably won’t survive a Friday deploy.

If you want a partner that’s done this under SOC 2, HIPAA, PCI, and GDPR pressure, GitPlumbers builds and hands off the whole system: policies, CI, attestations, and dashboards. We’ll even clean up the AI-generated “vibe code” your team merged at 2 a.m. to make the checks pass for the right reasons, not with duct tape.

Related Resources

Key takeaways

  • Policies don’t work until they compile into checks that run in the developer path: pre-commit, PR, CI, and CD.
  • Start with 5-7 high-signal rules (secrets, dependency risk, data egress) and enforce progressively: warn → soft fail → hard fail.
  • Produce automated proofs per artifact: signed SBOM, vulnerability scan, and policy attestation tied to a release SHA.
  • Balance speed with risk: tier services by data sensitivity and only hard-fail the regulated path at first.
  • Make exceptions first-class: track them in code, time-box them, and auto-expire in CI.
  • Measure what matters: PR cycle time, failed check rate, mean exception age, and time-to-patch on critical vulns.

Implementation checklist

  • Codify 5-7 top policies in Semgrep, OPA/Rego, and dependency scanners.
  • Install `pre-commit` with secrets and lightweight code rules.
  • Require PR checks: Semgrep (SAST), SBOM + vuln scan, Conftest/OPA for IaC, license compliance.
  • Generate and sign SBOM with `syft` and attach to artifacts; scan with `grype`.
  • Attest policy compliance with `cosign attest` and store in your registry.
  • Gate merges with branch protection and CODEOWNERS for `policy/` and `infrastructure/`.
  • Roll out in audit mode for two weeks, then soft-fail, then hard-fail by service tier.
  • Create an exception workflow in-repo with TTL and ticket links.

Questions we hear from teams

How do we avoid tanking developer velocity when we turn on enforcement?
Use tiered risk and progressive enforcement. Start in audit mode for two weeks, then soft-fail, then hard-fail only for regulated services. Track PR cycle time and failed check rates; tune rules before expanding scope.
We’re on GitLab/Jenkins, not GitHub Actions. Does this still work?
Yes. Semgrep, Conftest, Syft/Grype, and Cosign are CI-agnostic. Replace Actions with GitLab CI YAML or Jenkinsfiles; the pattern—checks on PRs, signed SBOMs, and attestations—stays the same.
What about Kubernetes runtime policies?
Enforce at deploy with OPA Gatekeeper or Kyverno for cluster policies (egress, allowed annotations) and verify in CI that manifests will pass. Defense-in-depth: static fail early, dynamic enforce always.
How do we prove compliance to auditors without manual screenshots?
Publish SBOMs, vulnerability scan results, and policy attestations per release SHA. Store them in your registry and an evidence bucket. Provide a report mapping control IDs (SOC 2, HIPAA, PCI) to these artifacts. Auditors love immutable, signed evidence.
We have a lot of AI-generated code creeping in. Any special handling?
Add Semgrep rules and pre-commit patterns tuned for ‘vibe code’ issues: insecure defaults, missing timeouts, eval/exec usage, and unsafe deserialization. Gate on these in PRs. GitPlumbers can run an AI code rescue to clean up and align with your standards.
What’s the first rule you’d ship if you could only pick one?
Secrets prevention with pre-commit + PR blocking. It’s high-signal, universally applicable, and pays off immediately in risk reduction and audit hygiene.

Ready to modernize your codebase?

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

Get guardrails that ship with your code See how we codify SOC 2 controls

Related resources