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 PaC | With PaC |
|---|---|
| Rules are documents people forget | Rules are code that runs |
| Enforcement is "the security team noticed" | Enforcement is automatic at admission time |
| Drift between policy and reality | Reality matches policy by construction |
| Compliance audit is a paper exercise | Compliance is continuous, evidenced by logs |
| New rules require manual rollout | A PR ships a new rule everywhere |
| Hard to test | Policies have unit tests |
| Policy logic is opaque | Anyone can read the rule's source |
The Players
General-purpose policy engines
| Tool | Language | Best for |
|---|---|---|
| OPA (Open Policy Agent) | Rego | Cross-cutting; CNCF graduated; Kubernetes, Terraform, microservices |
| Cedar (AWS) | Cedar | AWS Verified Permissions; cleaner syntax than Rego; growing |
| Sentinel (HashiCorp) | Sentinel | Terraform Cloud/Enterprise specifically |
| Kyverno | YAML (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
| Where | Tool |
|---|---|
| Kubernetes admission | OPA Gatekeeper, Kyverno, Polaris |
| Terraform plans | OPA + conftest, Sentinel, Checkov |
| CI/CD pipelines | conftest, Open Policy Agent, custom hooks |
| Microservice authorization | OPA sidecar, Cedar, OpenFGA, Zanzibar-style (Authzed) |
| AWS / Azure / GCP runtime | Cloud Custodian, AWS Config, Azure Policy, GCP Policy Controller |
| Container images | Cosign + Rekor + policy; OPA + image scanners |
| API gateways | OPA 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
| Tool | Model |
|---|---|
| OPA | Rego rules; general-purpose |
| OpenFGA / Authzed (SpiceDB) | Zanzibar (Google's auth) — relationship-based |
| Cedar (AWS, open source) | Policies + entity store; expressive ABAC + RBAC |
| Casbin | RBAC/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:
| Category | Examples |
|---|---|
| Kubernetes admission | No privileged pods; required labels; resource limits; allowed image registries |
| Terraform | No public S3 buckets; mandatory tags; instance type allowlist; estimated cost limits |
| Container images | Signed 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/CD | Required approvals for prod; deploy windows; merge only from main |
| Network | Default-deny; allowlist of egress destinations; no cross-namespace traffic |
| Data | PII fields encrypted; access logged; retention enforced |
| Auth (runtime) | RBAC + attribute checks at every API call |
Admission vs. Audit vs. Mutation
In Kubernetes:
| Webhook | What |
|---|---|
| Validating | Allow or deny the request (admission) |
| Mutating | Modify the resource (add a sidecar, default a value, enforce a label) |
| Audit / Background scan | Detect 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:
inputis the JSON request being evaluateddatais the policy bundle's static data (lookup tables, allow-lists)- A rule's body is implicit
AND; multiple rules with the same head are implicitOR _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.regoKyverno: 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: truePro: 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
1. Getting Started
Install OPA Gatekeeper and Kyverno on kind; write a first policy; block a bad pod; run conftest on Terraform
2. Patterns
Policy structure, bundles, testing, mutation, exceptions, gradual rollout, library structure, cross-cutting policies
3. Best Practices
Policy lifecycle, performance, debugging, working with compliance, common pitfalls, scaling across teams
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.