Policy-as-Code Is the New Shift-Left: Security Rules Versioned, Tested, and Deployed Like Application Logic — But Who Reviews the Policies?

I’ve been a security engineer long enough to remember when security policies lived in Word documents on SharePoint, reviewed annually by a committee that hadn’t written code in a decade. Policy-as-Code is the antidote to that world, and after two years of implementing it, I’m convinced it’s the future of security enforcement. But it comes with a set of problems that the tooling vendors don’t talk about.

What Policy-as-Code Actually Means

Policy-as-Code means defining security policies — network rules, access controls, compliance checks, deployment requirements — as actual code that’s versioned in Git, tested in CI, and deployed automatically. Instead of a PDF that says “all containers must run as non-root,” you write a Rego policy (Open Policy Agent), a Kyverno rule (Kubernetes), a Sentinel policy (Terraform), or a Cedar policy (AWS) that enforces that requirement automatically.

The tooling ecosystem has matured significantly:

  • Open Policy Agent (OPA) with Rego for general-purpose policy evaluation — Kubernetes admission control, API authorization, data filtering
  • Kyverno for Kubernetes-native policy enforcement — simpler syntax than Rego, native resource mutation
  • Sentinel for HashiCorp ecosystem — Terraform plan validation, Vault access policies
  • Cedar (AWS) for fine-grained authorization — IAM-level policies with formal verification

The Promise Delivered

At my company, we implemented OPA for Kubernetes admission control, and the results have been transformative. Before OPA, security violations were caught in post-deployment audits — meaning non-compliant workloads ran in production for days or weeks before someone noticed. Now, 95% of security violations are caught before deployment, at the admission control layer.

No containers running as root. No pods without resource limits. No services exposed without TLS. No images from untrusted registries. All enforced automatically, consistently, 24/7. No exceptions, no drift, no “I’ll fix it next sprint.”

The developer experience improved too. Instead of getting a security review rejection 3 days after submitting a PR, developers get immediate feedback: “Your deployment was rejected because policy X requires Y.” They fix it, redeploy, and move on. Security reviews that used to take days now take seconds.

The New Problem: Who Writes the Policies?

Here’s where it gets complicated. The security team writes the policies, but we don’t understand all the application requirements. When a policy blocks a legitimate deployment, developers come to us and say “fix your policy.” We say “change your architecture.” Neither side understands the other’s constraints, and the result is friction.

Example: we wrote a network policy that restricted cross-namespace communication in Kubernetes. Perfectly reasonable from a security standpoint — blast radius containment. But a new feature required a service in the orders namespace to communicate with a service in the inventory namespace. Our policy blocked it. The development team didn’t know the policy existed until their deployment failed in staging. They escalated. We debated. It took two days to resolve — a policy exception plus an architecture review.

This happens weekly.

The Policy Testing Challenge

Policies need tests, just like application code. But testing a policy means anticipating every possible input. You’re not testing a function that takes integers — you’re testing a rule that evaluates against the entire space of possible Kubernetes manifests, Terraform plans, or API requests.

We test our policies against a library of known-good and known-bad configurations. But the edge cases are infinite. We missed an edge case in a network policy that blocked cross-namespace communication needed for a new observability pipeline, causing a 4-hour production incident. The policy was correct according to its specification — but the specification didn’t account for observability infrastructure that legitimately needed cross-namespace access.

Testing policies against static fixtures isn’t enough. You need to test against production-like configurations, which means maintaining a realistic test environment that evolves with your infrastructure. It’s a significant investment.

The Governance Gap

Application code has clear ownership: the feature team. Infrastructure code has clear ownership: the platform team. But policy code sits in a no-man’s-land between security, platform, and application teams. Nobody owns it definitively.

When a policy needs to change:

  • Security team says “we set the security intent”
  • Platform team says “we implement the enforcement mechanism”
  • Application team says “we need exceptions for our use case”

Who approves the change? Who reviews the PR? Who’s on-call when a policy causes an incident? In practice, all three teams need to be involved, which means policy changes move slowly and require coordination across multiple teams.

My Proposal: Co-Owned Policy Repos

I’m pushing for a model where:

  1. Security team defines policy intent and sets minimum security baselines
  2. Platform team implements enforcement (OPA/Kyverno configuration, admission controller setup)
  3. Application teams can submit exception requests as PRs, reviewed by both security and platform
  4. All policies have owners, expiration dates, and mandatory review cycles
  5. Policy changes require approval from at least one security engineer and one platform engineer

It’s not perfect — it’s slow and coordination-heavy. But it ensures that policies reflect real-world requirements, not just theoretical security postures.

Is anyone doing policy-as-code well? How do you handle the ownership question? And how do you test policies against the combinatorial explosion of possible inputs? I’d love to hear what’s working and what isn’t.

Sam, I’m the platform engineer on the other side of this — I’m the one implementing OPA and Kyverno policies that your security team defines. And I want to validate something you said: the enforcement is easy; the authoring is hard.

The Authoring Problem

Setting up OPA as a Kubernetes admission controller? Straightforward. Configuring Kyverno with webhook policies? Well-documented. The infrastructure side of policy-as-code is honestly a solved problem at this point.

But writing policies that are simultaneously secure, practical, and don’t break legitimate workloads? That’s where everything falls apart.

I’ve lived through both failure modes:

  • Policies written only by security engineers are too restrictive. They reflect theoretical best practices without accounting for real-world infrastructure requirements. I’ve seen security-authored policies that blocked every single deployment in our staging environment because they required capabilities that our base container images didn’t support.

  • Policies written only by platform engineers (like me) are too permissive. We understand the infrastructure constraints but tend to write policies that accommodate current workloads rather than enforcing security improvements. We default to warn mode instead of deny because we don’t want to be the ones blocking deployments.

Paired Authoring

The best policies come from paired authoring: security defines the intent (“no privileged containers”), platform translates that to Rego or Kyverno syntax, and we test together against real workloads. It’s slow — a single policy can take a full day of paired work — but the output is dramatically better than either team working alone.

The security engineer catches the cases where my implementation is too loose (“your policy allows NET_ADMIN capability, which effectively gives root-equivalent network access”). I catch the cases where the security intent doesn’t account for infrastructure reality (“your policy blocks the CNI plugin daemonset, which needs host networking to function”).

Policy CI Pipeline

To address your testing challenge, I’ve been building what I call a “policy CI” pipeline. Here’s the approach:

  1. Snapshot production configs: Every night, we export all current Kubernetes manifests, Helm values, and Terraform plans from our staging and production environments
  2. Replay against new policies: When someone submits a policy change PR, CI runs the new policy against the entire snapshot of production configs
  3. Impact report: The pipeline generates a report showing exactly which existing workloads would be affected by the policy change — how many would be blocked, which namespaces, which teams
  4. Graduated rollout: New policies deploy first in audit mode (log violations but don’t block), then warn mode (warn but allow), then deny mode (hard block) — with a minimum 2-week observation period at each stage

This doesn’t solve the “infinite edge cases” problem entirely, but it dramatically reduces the risk of a new policy causing production incidents. We haven’t had a policy-induced incident since implementing the graduated rollout, whereas before we averaged one every 6 weeks.

The Ownership Model That Works For Us

On the ownership question: we use a CODEOWNERS model in our policy repo. Security team owns the policies/security/ directory, platform team owns policies/infrastructure/, and both teams must approve changes in policies/shared/. Exception requests go into policies/exceptions/ and require security approval plus an expiration date — no permanent exceptions.

It’s not fast, Sam, but it’s predictable. And in policy management, predictability matters more than speed.

Sam, as an OPA power user who’s been writing Rego policies for about three years now, I want to highlight a problem that doesn’t get enough attention: policy evolution and observability.

The Policy Decay Problem

Policies aren’t static. The threat landscape changes, your architecture evolves, new services introduce new patterns, and regulatory requirements shift. A policy that was perfectly calibrated 6 months ago may be too restrictive (blocking legitimate new patterns) or too permissive (not covering new attack vectors) today.

But nobody reviews old policies unless they cause an incident. Sound familiar? It’s the same problem we had with firewall rules in the pre-cloud era — rules accumulate, nobody knows which ones are still relevant, and removing any rule feels risky because you don’t know what might break.

Policy Expiration

I implemented a system I call “policy expiration” to address this:

  • Every policy in our OPA bundle includes a metadata field: review_by with a date
  • A CI job scans all policies weekly and flags any where the review date has passed
  • Expired policies generate a Jira ticket assigned to the policy owner, requiring re-evaluation
  • If a policy isn’t reviewed within 30 days of expiration, it automatically transitions from deny to warn mode — it still logs violations but stops blocking deployments
  • If it’s not reviewed within 60 days, it transitions to audit mode (log only)

This sounds aggressive, but it forces policy hygiene. In the first quarter after implementing expiration, we reviewed 47 policies and discovered that 12 of them (about 25%) were either redundant, outdated, or could be significantly simplified. Three policies were blocking patterns that had become standard practice months earlier.

Policy Observability

The other critical piece is policy observability — understanding which policies are actually firing, how often, and what they’re blocking. Without this, you’re flying blind.

We built a policy observability stack:

  • Decision logging: Every OPA decision (allow/deny) is logged with the input context, the policy that fired, and the result
  • Metrics dashboard: Real-time visibility into policy evaluation rates, denial rates, and latency per policy
  • Anomaly detection: Alerts when a policy’s denial rate spikes (potential misconfiguration or new deployment pattern) or drops to zero (policy may be irrelevant)
  • Impact analysis: Before deploying a policy change, we can replay the last 30 days of decision logs against the new policy to predict the impact

The anomaly detection has been particularly valuable. We caught a case where a network policy’s denial rate dropped to zero after an infrastructure migration — the policy was referencing old namespace names that no longer existed, so it was effectively dead code. Without observability, that policy would have sat in our bundle indefinitely, giving us a false sense of security.

On the Ownership Question

Sam, your co-owned model is pragmatic. I’d add one thing: every policy should have a documented “why”. Not just what the policy does, but why it exists, what threat or compliance requirement it addresses, and what the expected impact of removing it would be. When someone asks “can we remove this policy?” the documented rationale lets you make an informed decision instead of keeping it out of fear.

Policy-as-code is a massive improvement over policy-as-PDF. But we need to treat policies with the same lifecycle management rigor we apply to application code — versioning, testing, monitoring, deprecation, and eventual retirement.

Sam, I love policy-as-code in principle, but I want to push on something: the organizational model matters more than the tooling. I’ve seen teams adopt OPA, Kyverno, and Sentinel with excellent technical implementation and still fail because nobody could answer the question “who owns this policy?”

The Three-Layer Ownership Model

At my company, we solved the ownership question by splitting policies into three explicit layers, each with clear ownership:

Layer 1: Global Security Policies (owned by Security Team)

These are non-negotiable, organization-wide security baselines:

  • No containers running as root
  • All data encrypted at rest and in transit
  • Minimum TLS 1.2 (we’re migrating to TLS 1.3 minimum)
  • No images from untrusted registries
  • Mandatory vulnerability scanning before deployment
  • No hardcoded secrets in configurations

The security team has sole authority over these policies. Application teams cannot request exceptions — these are organizational invariants. Changes to global policies go through a formal security review board. The bar for changing them is intentionally high.

Layer 2: Infrastructure Policies (owned by Platform Team)

These govern infrastructure resource usage and operational standards:

  • CPU and memory resource limits required for all pods
  • Horizontal pod autoscaler configuration requirements
  • Namespace resource quotas
  • Ingress and networking standards
  • Logging and monitoring requirements
  • Scaling boundaries and circuit breaker configurations

The platform team owns these policies and iterates on them based on operational experience. Application teams can propose changes through RFCs, and the platform team evaluates them against operational data.

Layer 3: Application Policies (owned by Application Teams)

These are team-specific policies that govern application behavior:

  • Rate limiting configurations
  • Feature flag rollout policies
  • Circuit breaker thresholds
  • Team-specific deployment windows
  • Application-level access controls

Application teams have full authority over these policies. They version, test, and deploy them independently. The platform provides the policy enforcement infrastructure; the application team provides the policy content.

Separate Repos, Separate Pipelines

Each layer has its own Git repository, its own CODEOWNERS, its own CI pipeline, and its own deployment schedule:

  • security-policies repo: security team approvers, monthly deployment cycle, formal change management
  • infra-policies repo: platform team approvers, bi-weekly deployment cycle, RFC-based changes
  • app-policies/ repos (one per team): team approvers, continuous deployment, self-service

This separation prevents the “nobody owns it” problem you described. When a policy causes an incident, there’s no ambiguity about who’s responsible: check which layer the policy belongs to, and that’s the team that owns the response.

The Integration Challenge

The tricky part is when policies across layers interact. A security policy might require encrypted connections, an infrastructure policy might set network boundaries, and an application policy might define service communication patterns. These need to be evaluated together, and conflicts need clear resolution rules.

Our approach: security policies always win. If an application policy conflicts with a security policy, the security policy takes precedence. If an infrastructure policy conflicts with a security policy, same result. This hierarchy is documented, understood, and non-negotiable.

For conflicts between infrastructure and application policies, the platform team mediates. In practice, these conflicts are rare because the layers govern different concerns.

Sam, your co-ownership model is a good starting point, but I’d encourage you to formalize the layers. When everyone co-owns everything, nobody truly owns anything. Explicit layer ownership with clear hierarchy gives teams the autonomy to move fast within their layer while maintaining organizational consistency across layers.