Stage 7 · Master
Policy as Code & Security Guardrails
Policy as Code Fundamentals
Why policy as code: versionable, testable, auditable. OPA vs Kyverno. Decision points: scaffold, CI, admission, runtime.
Why Policy as Code?
Policy as Code treats organizational standards (security, cost, reliability, compliance) as version-controlled, testable, auditable code — not wiki pages or manual reviews. It shifts governance left: developers get instant feedback, not post-deployment surprises.
| Traditional Governance | Policy as Code |
|---|---|
| PDF checklists, manual reviews | Code in Git, automated enforcement |
| Gate at deployment (slow) | Guardrails at scaffold/CI/admission (fast) |
| Tribal knowledge | Explicit, reviewable, testable rules |
| Audit: screenshots, tickets | Audit: Git history, policy eval logs |
| Exceptions: email approval | Exceptions: code with audit trail |
| Drift undetected | Continuous reconciliation |
Decision Points in the Pipeline
- Scaffold Time: Template validation — reject non-compliant golden paths before repo created
- CI Pipeline: Unit tests for policies, scan generated manifests, container image scanning
- Admission Control: Validating webhooks (block), Mutating webhooks (add defaults/labels)
- Runtime: Continuous reconciliation (Kyverno/ArgoCD), drift detection, auto-remediation
- Audit: Periodic policy evaluation across all clusters, compliance reports
OPA vs Kyverno
| Aspect | OPA/Gatekeeper | Kyverno |
|---|---|---|
| Language | Rego (domain-specific) | YAML (Kubernetes-native) |
| Learning Curve | Steeper (new language) | Lower (K8s manifests) |
| Expressiveness | Very high (arbitrary logic) | High (K8s-focused) |
| Mutation | Limited (via mutating webhook) | Native (patches, generate) |
| Ecosystem | Large (CNCF, many integrations) | Growing (CNCF incubating) |
| Performance | Fast (WASM, partial eval) | Fast (native Go, caching) |
| Debugging | Rego playground, traces | Kubectl plugin, dry-run |
| Best For | Complex cross-cutting policies, non-K8s | K8s-native policies, mutation, generation |
OPA for complex policies (cost optimization, custom business logic, multi-cloud). Kyverno for K8s-native policies (labels, resources, security, mutation, generation). Many orgs use both.
Policy Lifecycle
- Author: Write policy in Rego/YAML with tests
- Review: PR review + policy test results + impact analysis (dry-run on existing resources)
- Stage: Deploy to staging cluster in
auditmode (log violations, don't block) - Monitor: Track violation rates, false positives, developer feedback for 2 weeks
- Enforce: Switch to
enforcemode, add to scaffold/CI/admission - Exception: Formal exception process with expiration, audit trail
- Deprecate: Remove policy, clean up exceptions, communicate
Testing Policies
# policies/security/no-privileged.rego
package platform.security
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf("Container %s must not run privileged", [container.name])
}
# policies/security/no-privileged_test.rego
package platform.security
test_deny_privileged {
deny with input as {"request": {"kind": {"kind": "Pod"}, "object": {"spec": {"containers": [{"name": "app", "securityContext": {"privileged": true}}]}}}}
}
test_allow_non_privileged {
not deny with input as {"request": {"kind": {"kind": "Pod"}, "object": {"spec": {"containers": [{"name": "app", "securityContext": {"privileged": false}}]}}}}
}
OPA tests: opa test policies/ runs all *_test.rego files. Fast, deterministic, CI-friendly.
# policies/security/no-privileged.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: no-privileged-containers
spec:
validationFailureAction: Audit
rules:
- name: no-privileged
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Privileged containers are not allowed"
pattern:
spec:
containers:
- =(securityContext):
=(privileged): "false"
# policies/security/no-privileged_test.yaml
apiVersion: kyverno.io/v1
kind: PolicyTest
metadata:
name: no-privileged-test
spec:
policies:
- no-privileged-containers
resources:
- pod-privileged.yaml
- pod-non-privileged.yaml
results:
- policy: no-privileged-containers
rule: no-privileged
resource: pod-privileged.yaml
result: fail
- policy: no-privileged-containers
rule: no-privileged
resource: pod-non-privileged.yaml
result: pass
Kyverno tests: kyverno test policies/ runs PolicyTest resources. Native YAML, no separate test language.
Governance & Exceptions
Exceptions are inevitable. Handle them as code: exception resource with reason, owner, expiration, approvers. Audit trail in Git.
apiVersion: platform.example.com/v1
kind: PolicyException
metadata:
name: payment-service-privileged-init
namespace: payments
spec:
policy: no-privileged-containers
target:
kind: Deployment
name: payment-service
namespace: payments
reason: "Legacy init container requires CAP_SYS_ADMIN for kernel module loading"
owner: team:payments
approvers:
- platform-lead@example.com
- security-lead@example.com
expiresAt: "2024-06-30T23:59:59Z"
status: Approved
createdAt: "2024-01-15T10:00:00Z"
Exception resource with policy, target, reason, owner, approvers, expiration. Stored in Git, auditable.
No expiration = permanent exception. No approver = no accountability. No target = blanket exception. Exception without migration plan = technical debt. All exceptions must have: expiration date, migration plan, quarterly review.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.