tricloudify
← All writeups
DEVSECOPS

One capability, one tool, one gate: building an evidence trail into CI

How a single enforcement pattern across SAST, SCA, and policy-as-code turns a compliance checklist into something an auditor can actually verify.

OPA/Rego · SOC 2 readiness6 min read

The problem with checklist DevSecOps

Most SOC 2 and ISO 27001 preparation we see follows the same anti-pattern: a spreadsheet of controls, a screenshot for each one taken the week before the audit, and a security tool that runs somewhere in CI but whose output nobody can trace back to a specific merge decision. It passes the audit. It doesn't actually stop anything.

The pattern we build instead is simple to state and slightly annoying to implement: one capability, one tool, one enforcement gate, one evidence trail. If a control exists, there is exactly one place in the pipeline that enforces it, and that enforcement produces a machine-readable record tied to the commit it blocked or allowed.

What the gate actually looks like

For a container image capability — "only signed, scanned images reach production" — the gate is a single OPA/Rego policy evaluated at deploy time, not a collection of separate checks scattered across three pipeline stages:

policy/image_gate.rego
package deploy.gate

default allow = false

allow {
  input.image.signed == true
  input.image.scan.critical_vulns == 0
  input.image.scan.high_vulns <= input.policy.max_high_vulns
}

deny[msg] {
  input.image.signed == false
  msg := sprintf("blocked: %s is not signed", [input.image.digest])
}

deny[msg] {
  input.image.scan.critical_vulns > 0
  msg := sprintf(
    "blocked: %s has %d critical vulnerabilities",
    [input.image.digest, input.image.scan.critical_vulns]
  )
}

That policy is the single source of truth for the control. It doesn't matter whether the signature check happened in a separate Cosign step or the vulnerability count came from Snyk or Trivy — those tools feed structured input into one gate, and the gate's decision is what gets logged. No pipeline stage downstream re-implements the check with slightly different logic, which is how you end up with two "security gates" that disagree with each other during an incident.

Making the decision auditable

The gate's output — allow or deny, plus the specific `deny` messages — gets written to an append-only evidence store alongside the commit SHA, the input that was evaluated, and a timestamp. That's the entire audit trail for that control. When SOC 2 asks "show me that unsigned images can't reach production," the answer isn't a screenshot of a Slack message from six months ago — it's a query against the evidence store returning every gate decision for the last audit period, including the ones that correctly blocked a deploy.

Mapping controls to gates, not the other way around

The order matters. Teams that start from the compliance framework end up building a tool per control-list-item, which produces the scattered-checks problem above. We start from the actual capability ("unsigned or vulnerable images can't reach prod"), build one gate for it, and then map that gate to whichever framework clause needs it — SOC 2 CC7.1, ISO 27001 A.8.29, whatever's relevant. The same gate can satisfy multiple framework clauses simultaneously because it's enforcing a real control, not chasing a checklist.

Why this matters

A compliance mapping that points to a real, enforced gate survives an auditor asking follow-up questions. A compliance mapping that points to a screenshot doesn't — and more importantly, neither does your production environment when someone pushes an unsigned image under deadline pressure.