Examples in this article are illustrative, not customer case studies. Product availability and coverage are described on the linked capability pages.
The payments API release failed at the pipeline gate, and the only explanation was a non-zero exit code from a shell script nobody on the team had written. This post shows how to express deployment gates as policy as code with Open Policy Agent (OPA), so every gate gives the same answer at each stage, explains its decision in plain language and leaves a record you can review later.
Policy as code means writing deployment rules in a policy language, keeping them in version control and evaluating them with an engine such as OPA. Gates written this way can be reviewed, tested and rolled back like any other code, apply consistent rules across stages while allowing outcomes to change with the inputs, and explain every decision.
Informal gates fail because nobody can read them
Most teams already have gates of some kind: a pipeline script that searches for known-bad patterns, a manual sign-off, a reviewer who scans the diff before approving. The trouble is that the logic lives in someone’s head or in a script untouched since its author moved on. Ask what the payments API’s gate actually checks and the honest answer is “read the script”. When rules are invisible, developers cannot predict them, security cannot evidence them, and nobody notices when they stop matching intent.
Policy as code makes the gate a reviewable artefact
Each gate answers one question (may this change merge, may this build ship, may this workload run), and policy as code moves that answer out of pipeline scripts into a file anyone can read, diff and review.
OPA is a widely adopted engine for this pattern. It is not tied to one stage: the same engine and language can evaluate a Kubernetes admission request, a pipeline gate or an authorisation decision inside an application. That generality is what suits it to deployment gating, where one set of rules must hold across several points in the delivery lifecycle.
Every Rego policy is input, rules and a decision
OPA’s language is Rego. You do not need fluency to understand a gate, because every policy has the same three parts: input, the object under evaluation, such as a pod spec, a build record or a pull request; rules, the conditions that inspect it; and a decision, usually an allow plus a set of reasons.
Here is a minimal gate for the payments API’s container:
package deploy.gates
default allow := false
allow if count(deny) == 0
deny contains msg if {
not input.image.digest
msg := "image must be pinned by digest"
}
deny contains msg if {
not input.container.runAsNonRoot
msg := "container must set runAsNonRoot"
}
Read it as: allow stays false unless no deny rule fires, and each deny rule contributes a human-readable message. That message is what turns a blocked release from a support ticket into a fix the developer can make straight away.
A second rule checks provenance against an approved list of registries:
package deploy.gates
approved_registries := {"registry.internal.example", "mirror.internal.example"}
deny contains msg if {
registry := split(input.image.ref, "/")[0]
not approved_registries[registry]
msg := sprintf("registry %v is not approved", [registry])
}
Neither example is production policy. Real gates handle exceptions, environment-specific rules and the staged rollout of new checks, but the shape holds: input in, rules evaluated, a decision and its reasons out.
Run one policy at pull request, pipeline and admission
Where a gate runs changes what it can see, so the same policy should run at every point that matters.
At the pull request. This is the cheapest place to catch a problem, because nothing has been built. A gate here sees the diff and the manifests, so an unpinned image reference in the payments API’s deployment file is caught before review finishes. It cannot yet see a built artefact.
In the pipeline. Once a build exists, a gate can evaluate the artefact itself: whether the image was scanned, what the scan found and whether the bill of materials matches expectations. SCA versus SBOM explains what each of those inputs can tell a gate.
At admission. The last checkpoint evaluates the object as it is about to be created. Only here does the gate see the target cluster, the namespace and the workloads already running around it, so checks about combinations of state belong here. Kubernetes admission gates and cluster drift covers this stage in depth.
Running one policy at all three points, rather than three implementations of similar checks, reduces policy drift. Different inputs, policy versions or deployment state can legitimately change the outcome; record those differences.
A deployment gate must explain and record every decision
A gate that answers “denied” and nothing else teaches people to route around it. Every decision should carry a reason a developer can act on without escalating, which is why the deny messages above matter as much as the conditions.
Every decision should also be recorded, allows as well as denials. If only denials are kept, nobody can later prove that the payments API release passed the gate, only that no refusal was found, which is a much weaker claim in an incident review or an audit. A signed record of each decision, with the policy version and input that produced it, turns the gate into evidence. The companion post on fail-closed deployment gates covers what a gate should do when it cannot decide at all.
Review and test policy as code like production code
Because policy lives in version control, changes deserve the same review as any consequential code: a merge request against the policy repository, a diff showing exactly what changed, and a rollback path if a new rule proves too strict or too loose under real traffic. Treating policy as configuration rather than code is how gates drift away from what the security team believes they enforce.
Rego has a built-in test framework, so each rule can ship with a test proving it fires:
package deploy.gates_test
import data.deploy.gates
test_unpinned_image_is_denied if {
"image must be pinned by digest" in gates.deny with input as {"image": {}, "container": {"runAsNonRoot": true}}
}
Running those tests helps catch regressions covered by the test cases. Add missing-input, malformed-input and override cases before enforcement.
Key takeaways
- Move gate logic out of pipeline scripts into a policy repository with review, tests and rollback.
- Write every deny rule with a message a developer can act on without asking for help.
- Evaluate the same policy at pull request, pipeline and admission and explain any changed outcome using the input and policy versions.
- Record allow and deny decisions together with the policy version and input that produced them.
How SecurityVault approaches this
SecurityVault uses OPA for policy evaluation and records signed decisions with their supporting context. Policy-engine errors result in denial. The same policy intent can be applied across checkpoints, but each stage has different evidence. Test missing fields and stale or incomplete scans explicitly, and record policy versions and authorised overrides.
Start with a narrowly scoped rule that both security and delivery owners can explain. Build fixtures for allowed, denied, missing and malformed input, then run the policy in an evaluation pipeline before enforcement. Compare the decision record with the input and policy version used. Practise an exception and verify that it remains distinguishable from an ordinary pass. This gives the team a repeatable review process for policy changes and a concrete way to investigate a surprising outcome without guessing which stage supplied different evidence.
Explore Policy authoring or talk to our team about an evaluation with your engineering, security and compliance stakeholders.