Safeguard
DevSecOps

Policy as Code for Security: A Practical Guide

When your security rules live in a wiki, they are advice. When they live in version-controlled code the pipeline enforces, they are controls. Here is how to move security policy into code that actually runs.

Priya Mehta
DevSecOps Lead
6 min read

Every security team has a policy document. It says things like "no critical vulnerabilities in production," "no hardcoded secrets," and "all S3 buckets must be private." It lives in a wiki, it is reviewed annually, and it is violated constantly — not out of malice, but because a document cannot enforce anything. A rule that depends on a human remembering it and choosing to follow it is a suggestion. Policy as code turns those suggestions into controls by expressing them as version-controlled, testable code that the pipeline evaluates automatically on every change.

What policy as code means

Policy as code is the practice of defining your governance rules — security, compliance, operational — in a machine-readable language, storing them in version control, and having automated systems evaluate them. Instead of "engineers should pin their GitHub Actions," you write a policy that fails the build when an action is referenced by mutable tag instead of a commit SHA. The rule is now unambiguous, universally applied, and auditable: you can see exactly which version of the policy was in force on any given date, who changed it, and why.

The benefits follow directly from treating policy like any other code:

  • Consistency — the same rule applies to every repo and every developer, with no interpretation drift.
  • Auditability — Git history is your compliance evidence. "Prove this control was enforced in Q2" becomes a git log.
  • Testability — you can unit-test a policy against known-good and known-bad inputs before it ever gates a real pipeline.
  • Reviewability — a policy change goes through pull request review, so tightening or loosening a control is a visible, approved decision.

The main tools in 2026

Several engines dominate policy-as-code work, and they specialize:

ToolLanguageBest for
Open Policy Agent (OPA)RegoGeneral-purpose: APIs, Kubernetes, CI decisions
GatekeeperRego (OPA)Kubernetes admission control
KyvernoYAMLKubernetes-native policy without a new language
ConftestRegoTesting structured config (Terraform, Dockerfiles) in CI
Cloud CustodianYAMLCloud resource governance

Open Policy Agent (OPA), a CNCF-graduated project, is the general-purpose workhorse: it takes structured input (JSON), evaluates it against rules written in the Rego language, and returns a decision. If you learn one engine, learn OPA — its reach extends from Kubernetes admission to CI gate decisions to application authorization.

A worked example: gating on scan results

Say your policy is "a merge is blocked if it introduces a critical vulnerability that is reachable, or any hardcoded secret." Express your scanner's output as JSON, then write the rule in Rego:

# policy/security.rego
package ci.security

# deny if a new critical finding is reachable
deny[msg] {
    f := input.findings[_]
    f.status == "new"
    f.severity == "critical"
    f.reachable == true
    msg := sprintf("blocked: reachable critical %s in %s", [f.id, f.location])
}

# deny on any newly introduced secret
deny[msg] {
    f := input.findings[_]
    f.type == "secret"
    f.status == "new"
    msg := sprintf("blocked: hardcoded secret at %s", [f.location])
}

Wire the evaluation into the pipeline. The build fails if deny returns any messages:

# ci: evaluate scan output against policy
policy-gate:
  stage: security
  script:
    - safeguard scan --all-engines --diff origin/main --json > findings.json
    - opa eval --data policy/security.rego \
        --input findings.json \
        --format pretty 'data.ci.security.deny' \
        --fail-defined

--fail-defined makes opa eval exit non-zero the moment the deny set is non-empty, turning the policy into a real gate. The rule is now code: reviewed, versioned, and identically applied everywhere.

Test your policies before they gate anything

The failure mode of policy as code is a bad policy confidently enforced on everyone at once. Because policies are code, unit-test them. OPA supports test files natively:

# policy/security_test.rego
package ci.security

test_blocks_reachable_critical {
    deny[_] with input as {"findings": [
        {"id": "CVE-2026-1", "severity": "critical",
         "reachable": true, "status": "new", "location": "app.py"}
    ]}
}

test_allows_unreachable_critical {
    count(deny) == 0 with input as {"findings": [
        {"id": "CVE-2026-2", "severity": "critical",
         "reachable": false, "status": "new", "location": "vendor/x"}
    ]}
}

Run opa test policy/ in CI on any change to the policies themselves. Now a change to a control is validated against known cases before it can break a single developer's build.

Rollout patterns that avoid a revolt

  • Ship in "dry-run" first. Evaluate and log decisions without enforcing, so you can see what would have blocked before it does.
  • Separate policy repos from the rules that consume them where it helps. A central policy repo that services reference keeps controls consistent and lets security own the rules without owning every pipeline.
  • Version and pin. Pipelines should reference a specific policy version so a mid-sprint policy tightening does not surprise a team at merge time — roll the version forward deliberately.
  • Make exceptions first-class. Encode risk-acceptance and time-boxed exemptions in the policy (with an expiry and an owner) rather than forcing people to disable the gate. An exception the policy knows about is auditable; a disabled gate is a hole.

Beyond CI: the same idea everywhere

Policy as code is not only a merge gate. The same pattern secures Kubernetes (Gatekeeper/Kyverno rejecting a pod that runs as root or pulls an unsigned image), Terraform (Conftest failing a plan that opens a security group to the world), and cloud accounts (continuous checks against a posture baseline). The mental model is identical: structured input, versioned rules, automated decision. Learn it once in CI and it transfers directly to infrastructure.

How Safeguard helps

Safeguard ships policy as code as a first-class feature, so you get the enforcement without hand-writing and maintaining Rego for every control. Define which findings block a merge — by severity, reachability, type, or compliance framework — in version-controlled policy that gates across SCA, SAST, DAST, and secrets in one evaluation. Griffin AI supplies the reachability signal your policies key on, so "block reachable criticals" is a rule you can actually write and trust. Time-boxed exceptions are built in and auditable, giving you the compliance evidence a SOC 2 auditor asks for. And the Safeguard CLI runs the policy evaluation as a single step in any pipeline.

See how policy-native gating compares to configuring checks by hand in our Snyk comparison, then get started free or read the documentation.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.