Steven's Knowledge

Policy as Code

OPA, Kyverno, Cedar, Sentinel - expressing security, compliance, and operational rules as version-controlled code

Policy as Code

Policy as Code (PaC) is the practice of expressing rules — security, compliance, operational guardrails — as machine-evaluated code rather than wiki pages and tribal knowledge. The policy decides "is this allowed?" and the answer is enforced at the point of action: pod admission, Terraform plan, API request, S3 bucket creation.

Without policy as code, rules live as PDFs ("S3 buckets must not be public") and depend on every engineer reading them. With policy as code, the rule is a function: an attempt to create a public S3 bucket fails at the gate.

Why Policy as Code

Without PaCWith PaC
Rules are documents people forgetRules are code that runs
Enforcement is "the security team noticed"Enforcement is automatic at admission time
Drift between policy and realityReality matches policy by construction
Compliance audit is a paper exerciseCompliance is continuous, evidenced by logs
New rules require manual rolloutA PR ships a new rule everywhere
Hard to testPolicies have unit tests
Policy logic is opaqueAnyone can read the rule's source

The Players

General-purpose policy engines

ToolLanguageBest for
OPA (Open Policy Agent)RegoCross-cutting; CNCF graduated; Kubernetes, Terraform, microservices
Cedar (AWS)CedarAWS Verified Permissions; cleaner syntax than Rego; growing
Sentinel (HashiCorp)SentinelTerraform Cloud/Enterprise specifically
KyvernoYAML (k8s-native)Kubernetes-only; no new language to learn

OPA is the dominant general-purpose choice — runs anywhere, integrates with anything. Kyverno wins for K8s-only teams who don't want Rego.

Specific integrations

WhereTool
Kubernetes admissionOPA Gatekeeper, Kyverno, Polaris
Terraform plansOPA + conftest, Sentinel, Checkov
CI/CD pipelinesconftest, Open Policy Agent, custom hooks
Microservice authorizationOPA sidecar, Cedar, OpenFGA, Zanzibar-style (Authzed)
AWS / Azure / GCP runtimeCloud Custodian, AWS Config, Azure Policy, GCP Policy Controller
Container imagesCosign + Rekor + policy; OPA + image scanners
API gatewaysOPA at the gateway; Cedar at AWS

The point of OPA is the same policy engine evaluates all of these. One language, one mental model, one set of tests.

Authorization-specific

ToolModel
OPARego rules; general-purpose
OpenFGA / Authzed (SpiceDB)Zanzibar (Google's auth) — relationship-based
Cedar (AWS, open source)Policies + entity store; expressive ABAC + RBAC
CasbinRBAC/ABAC; pluggable; in-process libraries

For "can this user do this action on this resource?" at app-level — these are stronger than OPA, which is more about "is this configuration allowed?"

What You Can Enforce

A non-exhaustive list:

CategoryExamples
Kubernetes admissionNo privileged pods; required labels; resource limits; allowed image registries
TerraformNo public S3 buckets; mandatory tags; instance type allowlist; estimated cost limits
Container imagesSigned by trusted authority; vulnerability score below threshold; no :latest tag
Cloud config (continuous)Encrypted volumes; no public IAM roles; CloudTrail enabled in all regions
CI/CDRequired approvals for prod; deploy windows; merge only from main
NetworkDefault-deny; allowlist of egress destinations; no cross-namespace traffic
DataPII fields encrypted; access logged; retention enforced
Auth (runtime)RBAC + attribute checks at every API call

Admission vs. Audit vs. Mutation

In Kubernetes:

WebhookWhat
ValidatingAllow or deny the request (admission)
MutatingModify the resource (add a sidecar, default a value, enforce a label)
Audit / Background scanDetect violations on already-running resources

A mature policy stack does all three: block bad new things, fix things you can (add missing labels), detect drift in existing things.

Rego in 60 Seconds

Rego is OPA's language. Looks unusual the first time:

package kubernetes.admission

# Deny if a Pod runs as root
deny[msg] {
  input.request.kind.kind == "Pod"
  container := input.request.object.spec.containers[_]
  container.securityContext.runAsUser == 0
  msg := sprintf("Container '%v' runs as root", [container.name])
}

Rules collect into sets. If the deny set is non-empty, the request is denied. Each rule expresses a single condition. The language is declarative — order doesn't matter, multiple rules can independently produce denies.

Conventions:

  • input is the JSON request being evaluated
  • data is the policy bundle's static data (lookup tables, allow-lists)
  • A rule's body is implicit AND; multiple rules with the same head are implicit OR
  • _ is "any element of"

Test (policy_test.rego):

test_root_user_denied {
  result := deny with input as {
    "request": {
      "kind": {"kind": "Pod"},
      "object": {"spec": {"containers": [{"name": "x", "securityContext": {"runAsUser": 0}}]}}
    }
  }
  count(result) == 1
}
opa test policy.rego policy_test.rego

Kyverno: K8s-Native Alternative

Kyverno expresses policy as YAML — no new language:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-root-user
spec:
  validationFailureAction: enforce
  rules:
    - name: validate-runAsNonRoot
      match:
        any:
          - resources: { kinds: [Pod] }
      validate:
        message: "Containers must not run as root"
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true

Pro: zero learning curve; Kubernetes-native. Con: K8s-only (so the cross-cutting benefit of OPA is lost).

For K8s-only environments, Kyverno is often easier. For teams who want one policy language across K8s + Terraform + microservices, OPA wins.

Learning Path

When PaC Doesn't Fit

Honest cases:

  • You're a single small team. Documents + careful code review work; PaC is overhead until you have multiple teams or compliance pressure.
  • No-to-low policy logic. "We have one rule: no public buckets." A Terraform module that just doesn't expose that option is simpler than a policy engine.
  • Policy hostile to your culture. PaC works when teams accept guardrails. If every policy is contested at runtime, you have a culture problem, not a policy gap.

The policy paradox: the best policies are the ones nobody notices because they fire so rarely. The worst are the ones engineers spend their day arguing with. The skill is calibration — block what's clearly wrong, warn on what's borderline, document the rest. A policy that fires on 30% of PRs isn't policy; it's friction.

On this page