Stage 7 · Master
Policy as Code & Security Guardrails
OPA/Rego for Kubernetes Admission
Write Rego policies for resource limits, labels, image registries, privilege escalation. Test with OPA test framework.
Rego Basics
Rego is a declarative query language for policy. It evaluates to a set of documents (allow/deny/violation). Key concepts: rules, sets, comprehensions, functions, imports.
package platform.admission
# Import external data
import data.kubernetes.namespaces
import data.platform.teams
# Default deny
default allow = false
# Allow if all conditions met
allow {
# Input validation
valid_kind
valid_labels
valid_resources
valid_images
valid_security
valid_team
}
# Rule: valid_kind succeeds if kind is in allowed list
valid_kind {
allowed_kinds := {"Deployment", "StatefulSet", "DaemonSet", "Service", "ConfigMap", "Secret"}
input.request.kind.kind in allowed_kinds
}
# Rule: valid_labels - all required labels present
valid_labels {
required := {"team", "service", "environment", "managed-by"}
labels := input.request.object.metadata.labels
all_required_present(required, labels)
}
# Helper function
all_required_present(required, labels) {
labels := labels | default({})
count({r | r := required[_]; labels[r]}) == count(required)
}
# Rule: valid_resources - requests <= limits
valid_resources {
containers := input.request.object.spec.template.spec.containers
all_containers_have_limits(containers)
}
all_containers_have_limits(containers) {
not any_missing_limits(containers)
}
any_missing_limits(containers) {
container := containers[_]
not container.resources.limits.cpu
not container.resources.limits.memory
}
# Rule: valid_images - only approved registries
valid_images {
allowed_registries := {"ghcr.io", "registry.platform.example.com", "docker.io/library"}
containers := input.request.object.spec.template.spec.containers
all_images_allowed(containers, allowed_registries)
}
all_images_allowed(containers, allowed) {
not any_disallowed_image(containers, allowed)
}
any_disallowed_image(containers, allowed) {
container := containers[_]
image := container.image
not startswith(image, allowed[_])
}
# Rule: valid_security - no privileged, runAsNonRoot, readOnlyRootFS
valid_security {
containers := input.request.object.spec.template.spec.containers
all_containers_secure(containers)
}
all_containers_secure(containers) {
not any_insecure_container(containers)
}
any_insecure_container(containers) {
container := containers[_]
insecure(container)
}
insecure(container) {
container.securityContext.privileged == true
}
insecure(container) {
container.securityContext.runAsNonRoot != true
}
insecure(container) {
container.securityContext.readOnlyRootFilesystem != true
}
insecure(container) {
container.securityContext.capabilities.add[_] == "ALL"
}
# Rule: valid_team - team exists and owns namespace
valid_team {
team := input.request.object.metadata.labels.team
teams[team]
namespace := input.request.namespace
namespaces[namespace].owner == team
}
Rego policy with modular rules. Each rule is a boolean condition. Helper functions for reusability. Default deny pattern.
Kubernetes Admission Review
OPA receives AdmissionReview request, evaluates policy, returns AdmissionReview response. Gatekeeper manages the webhook and OPA integration.
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
"uid": "abc-123",
"kind": {"group": "apps", "version": "v1", "kind": "Deployment"},
"resource": {"group": "apps", "version": "v1", "resource": "deployments"},
"requestKind": {"group": "apps", "version": "v1", "kind": "Deployment"},
"requestResource": {"group": "apps", "version": "v1", "resource": "deployments"},
"name": "payment-api",
"namespace": "payments",
"operation": "CREATE",
"userInfo": {
"username": "alice@example.com",
"groups": ["system:authenticated", "team:payments"]
},
"object": {
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "payment-api",
"namespace": "payments",
"labels": {
"team": "payments",
"service": "payment-api",
"environment": "production",
"managed-by": "platform-controller"
}
},
"spec": {
"replicas": 3,
"template": {
"spec": {
"containers": [{
"name": "payment-api",
"image": "ghcr.io/myorg/payment-api:v1.2.3",
"resources": {
"requests": {"cpu": "100m", "memory": "128Mi"},
"limits": {"cpu": "500m", "memory": "512Mi"}
},
"securityContext": {
"privileged": false,
"runAsNonRoot": true,
"readOnlyRootFilesystem": true,
"capabilities": {"drop": ["ALL"]}
}
}]
}
}
}
}
}
}
AdmissionReview request contains the object being created/updated, user info, operation type. Policy evaluates this.
# Allow
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "abc-123",
"allowed": true
}
}
# Deny with message
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "abc-123",
"allowed": false,
"status": {
"code": 403,
"message": "Validation failed: missing required label 'team', container 'payment-api' missing CPU limit"
}
}
}
Response must include uid, allowed boolean, and optional status with message for denial.
Common Policy Patterns
| Pattern | Rego Snippet | Use Case |
|---|---|---|
| Required Labels | required := {"team", "env"} all_present(required, input.labels) | Ownership, cost allocation, compliance |
| Allowed Registries | allowed := {"ghcr.io", "registry.corp"} all_images_allowed(input.containers, allowed) | Supply chain security |
| Resource Limits | container.resources.limits.cpu container.resources.limits.memory | Cost control, QoS guarantees |
| No Privileged | not container.securityContext.privileged | Security baseline |
| Run As Non-Root | container.securityContext.runAsNonRoot == true | Security baseline |
| Read-Only Root FS | container.securityContext.readOnlyRootFilesystem == true | Security baseline |
| Drop ALL Capabilities | "ALL" in container.securityContext.capabilities.drop | Least privilege |
| Seccomp Profile | container.securityContext.seccompProfile.type == "RuntimeDefault" | Syscall filtering |
| Network Policy | exists_network_policy(input.namespace) | Network segmentation |
| Pod Disruption Budget | exists_pdb(input.name, input.namespace) | Availability |
| Image Pull Secrets | input.spec.template.spec.imagePullSecrets[_].name == "registry-creds" | Private registry access |
| Annotation Validation | input.metadata.annotations["platform.example.com/owner"] | Metadata standards |
External Data Sources
Policies often need external data: team registry, approved images, vulnerability DB, cost data. OPA supports external data via bundles, HTTP, or Kubernetes ConfigMaps.
# Load team data from Kubernetes ConfigMap (via Gatekeeper external data)
import data.kubernetes.configmaps
teams := configmaps["platform-system"]["teams"]["teams.json"]
# Or load from HTTP endpoint (OPA HTTP sender)
teams := http.send({
"method": "GET",
"url": "https://api.platform.example.com/teams",
"headers": {"Authorization": "Bearer " + input.request.userInfo.token}
}).body
# Use in policy
valid_team {
team := input.request.object.metadata.labels.team
teams[team].active == true
}
# Approved images from external registry scan
approved_images := http.send({
"method": "GET",
"url": "https://registry.platform.example.com/v2/_catalog",
"headers": {"Authorization": "Bearer " + registry_token}
}).body.repositories
image_approved {
image := input.request.object.spec.template.spec.containers[_].image
approved_images[_] == image
}
External data loaded via ConfigMap, HTTP, or bundles. Used in policy rules for dynamic decisions.
Performance Optimization
- Use
count()instead of comprehensions for existence checks - Index external data via
opa.eval()cache - Avoid recursion; use comprehensions with
any/all - Compile policies to WASM for faster evaluation:
opa build -t wasm - Use partial evaluation:
opa eval --partialfor static parts - Limit input size: Gatekeeper
--max-request-size - Monitor:
opa_metrics_rego_query_duration_secondshistogram
Testing & Debugging
# Run all tests
opa test policies/
# Run with verbose output
opa test -v policies/
# Run specific test file
opa test policies/security/
# Benchmark
opa bench -b ./policies -i input.json 'data.platform.admission.allow'
# Debug with trace
opa eval -i input.json -d policies/ 'data.platform.admission.allow' --trace
# Explain (why allowed/denied)
opa eval -i input.json -d policies/ 'data.platform.admission.allow' --explain full
# Format
opa fmt policies/
# Check syntax
opa parse policies/
OPA CLI for testing, benchmarking, tracing, explaining decisions. Integrate opa test in CI.
# Debug: print all violations with context
violation[msg] {
not allow
msg := sprintf("Violation: %v", [deny_reasons])
}
deny_reasons := [reason |
reason := "missing_labels"
not valid_labels
]
deny_reasons := [reason |
reason := "invalid_resources"
not valid_resources
]
deny_reasons := [reason |
reason := "disallowed_image"
not valid_images
]
deny_reasons := [reason |
reason := "insecure_container"
not valid_security
]
deny_reasons := [reason |
reason := "invalid_team"
not valid_team
]
# Debug: trace evaluation
trace(msg) {
true
msg := sprintf("Evaluating: kind=%s, namespace=%s, name=%s", [input.request.kind.kind, input.request.namespace, input.request.object.metadata.name])
}
Debug rules collect all violation reasons. Trace rule logs evaluation context.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.