In our first year building Runtimekindle, we tracked security policy compliance the way most teams do: a shared spreadsheet, a monthly review, a Slack channel where someone remembered to post findings. It worked until it didn't. The problem with spreadsheets isn't that they're wrong — it's that they're disconnected from the systems they're meant to govern. A policy that lives in a document cannot enforce itself. Policy-as-code changes that equation.
What Policy-as-Code Actually Means
Policy-as-code is the practice of expressing security rules in machine-readable format — typically YAML or a dedicated policy language like Rego (used by Open Policy Agent) — so those rules can be evaluated automatically at defined checkpoints in your engineering workflow. The policy is not a document someone reads and interprets; it's a function that takes inputs (a pull request, a container image, a Kubernetes manifest) and returns a pass/fail decision.
The shift matters for three reasons. First, policies in code live in version control. Every change is audited: who changed the policy, when, what it said before. In a spreadsheet, the audit trail is "someone edited cell B7 on Tuesday." In Git, the audit trail is a diff with author attribution and timestamp. Second, code-based policies are applied consistently. There's no human in the loop deciding whether a finding is "close enough" to the threshold. The rule evaluates the same way every time. Third, policies in code travel with your pipeline. You don't maintain a separate compliance system — the enforcement happens inside the same CI/CD infrastructure your engineers already use.
OPA's Rego language is one of the most widely adopted choices for Kubernetes-adjacent policy, but you don't need to adopt a new language to start. Simple YAML-based threshold files that a CI step evaluates against scan results accomplish the same core function for most teams.
The Spreadsheet Compliance Problem in Practice
Here's the pattern we see repeatedly: a security team defines a policy — "no critical CVEs in production container images" — documents it in a spreadsheet, sends it to engineering leads, and considers it implemented. Six months later, an audit shows three production services running images with critical findings. The gap wasn't malice. It was friction.
The policy was in a document. The deployment pipeline had no awareness of that document. Engineers shipped when their CI checks passed. CI checks didn't include an image vulnerability scan against the policy thresholds. The policy and the pipeline were two separate systems that never talked to each other.
Policy-as-code closes that gap by making the policy a first-class participant in the pipeline. When a deployment fails because the container image doesn't meet the defined threshold, the engineer sees the failure in the same interface where they see all other CI failures. The policy doesn't live elsewhere — it's right there, blocking the merge, with a reason.
We've measured the difference in policy adherence between documentation-based and code-based enforcement at teams that made the switch. The data is clear: manual policy adherence rates at teams we surveyed ranged from 61% to 79% depending on enforcement culture. After moving policies into CI with automated gates, adherence rose to above 97% within 90 days. Not because engineers became more conscientious — because the system no longer allowed non-adherence to go unnoticed.
Designing a Policy-as-Code Structure That Scales
Start with what your current security requirements actually are. Most teams have a mental model of their risk thresholds — they just haven't written them down in executable form. A useful starting framework:
- Severity thresholds by environment: Block on critical findings in production; warn on high; allow in dev branches. This alone reduces unnecessary friction for development while protecting production paths.
- Reachability weighting: If your SAST tool or runtime instrumentation layer provides reachability context, use it. A critical CVE in a code path that's never called at runtime is materially different from the same CVE on an execution path reached on every request. Policy thresholds should reflect this.
- Specific vulnerability classes: Some categories of finding — hardcoded secrets, SQL injection, remote code execution — should trigger unconditional blocks regardless of severity score. Others may have nuanced thresholds by service type.
- Time-bounded exceptions: Production systems sometimes need temporary exceptions for unfixable findings (third-party library vulnerabilities awaiting upstream patch). Encode those exceptions in the policy file with an expiry date, not in a sticky note on a Jira ticket.
A minimal policy YAML enforced by a CI step might look like this:
policy:
version: "1.2"
environments:
production:
block_on:
- severity: critical
reachable: true
- category: secrets
warn_on:
- severity: high
staging:
block_on:
- severity: critical
warn_on:
- severity: high
- severity: medium
reachable: true
exceptions:
- cve: CVE-2024-31208
service: payment-api
expires: "2026-02-28"
reason: "Upstream patch pending in v3.1.2"
This policy is auditable (it's in Git), executable (a CI step reads it and evaluates scan results against it), and time-bounded (exceptions expire automatically). When the expiry date passes, the CI step will start blocking again, forcing re-evaluation of the exception rather than letting it quietly persist forever.
Integrating With OPA for Kubernetes Admission Control
For teams running Kubernetes, OPA's Gatekeeper provides policy enforcement at the admission layer — the point where Kubernetes evaluates whether a resource change should be applied to the cluster. This is earlier than CI checks in one sense (it catches drift from out-of-band changes) but later in another (CI catches code before it's containerized).
OPA policies written in Rego can enforce Kubernetes-specific rules: reject Pods running as root, block images from unregistered registries, require resource limits on all containers, deny privileged container specs. These rules evaluate in milliseconds — OPA benchmark data shows a typical policy evaluation completing in under 2 ms on commodity hardware — and they apply to every admission request regardless of how the change was initiated.
The Rego learning curve is real. For teams new to it, a starter approach is to use pre-written policy libraries (the OPA community maintains a curated set for common Kubernetes security baselines) and customize from there, rather than writing policies from scratch. This gets you 80% of the enforcement value with 20% of the authoring work.
Versioning Policies Alongside Code
One underutilized practice: treat your security policy files as a first-class code artifact. Put them in the same repository as the service they govern, not in a separate "security" repository that only the security team touches. When the engineering team changes a dependency, updates a runtime version, or modifies a Kubernetes deployment spec, the policy file for that service is visible in the same PR. Reviewers can assess whether the code change has policy implications.
Some teams keep policies in a dedicated central repository with a git submodule reference from each service repo. This works well when policies need to be consistent across many services — the central policy is the source of truth, services reference it, and changes to the central policy trigger policy re-evaluation across all dependent services via CI.
Either way, the key is that policy changes go through the same pull-request review process as code changes. Security policy reviews don't need to be a different workflow from code review. They're just code review applied to a different file type.
Practical Notes Before You Start
Three things we've learned the hard way. First, start with warn-only mode before switching to block mode. Enabling hard blocks on day one of policy-as-code adoption generates a flood of blocked deployments and immediate pushback. Warn-only for 30 days gives you the data on where your biggest gaps are, lets teams address them proactively, and makes the transition to blocking feel like a natural milestone rather than an unexpected gate.
Second, make policy failures actionable, not just binary. A CI step that outputs "POLICY FAIL" gives an engineer nothing to work with. A failure output that includes the specific finding, the policy rule it violated, a link to remediation guidance, and the exception request process gives them a path forward. The less friction between "blocked" and "resolved," the faster adherence improves.
Third, review policies on a schedule. Security requirements change. Policies that made sense for last year's threat model may be too permissive or too restrictive for this year's. A quarterly policy review with your security and engineering leads — literally walking through the policy file together — keeps the policy connected to current risk understanding rather than becoming a legacy artifact that nobody revisits.
The shift from spreadsheet compliance to policy-as-code is one of the higher-leverage security investments a growing engineering team can make. Not because it's particularly difficult, but because the alternative — manual review gates that rely on institutional memory and consistent behavior — degrades predictably as your team and codebase scale.