Policy as Code: Automating Security Rules
Policy as Code (PaC) is the practice of defining security and compliance rules as versionable, testable, and automatically enforceable code. Instead of documenting policies in PDFs that nobody reads, PaC transforms them into executable code that is automatically applied in pipelines and infrastructure.
With PaC, rules like "every container must run as non-root" or "S3 buckets must not be public" become automated checks that block non-compliant deploys. This eliminates human error and ensures continuous compliance.
What You'll Learn
- Policy as Code principles and advantages over traditional policies
- OPA (Open Policy Agent) and the Rego language
- Kyverno for Kubernetes policy enforcement
- HashiCorp's Sentinel for Terraform
- Conftest for configuration validation
- Testing and deploying policies
OPA: Open Policy Agent
OPA (Open Policy Agent) is the most widely used policy engine in the cloud-native ecosystem. Developed by the CNCF, OPA uses the declarative language Rego to define policies that can be applied to any type of structured data: API requests, Kubernetes configurations, Terraform configurations, CI/CD pipelines.
Writing Policies in Rego
# policy/kubernetes/pod-security.rego
package kubernetes.admission
# Deny containers running as root
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf(
"Container '%s' must have runAsNonRoot: true",
[container.name]
)
}
# Deny images with :latest tag
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf(
"Container '%s' uses :latest. Specify an explicit tag.",
[container.name]
)
}
# Require resource limits
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.resources.limits
msg := sprintf(
"Container '%s' must define resource limits (CPU and memory)",
[container.name]
)
}
Testing OPA Policies
# policy/kubernetes/pod-security_test.rego
package kubernetes.admission
test_deny_root_container {
count(deny) > 0 with input as {
"request": {
"kind": {"kind": "Pod"},
"object": {
"spec": {
"containers": [{
"name": "app",
"image": "myapp:v1",
"securityContext": {}
}]
}
}
}
}
}
test_allow_nonroot_container {
count(deny) == 0 with input as {
"request": {
"kind": {"kind": "Pod"},
"object": {
"spec": {
"containers": [{
"name": "app",
"image": "myapp:v1",
"securityContext": {"runAsNonRoot": true},
"resources": {"limits": {"cpu": "500m", "memory": "256Mi"}}
}]
}
}
}
}
}
Kyverno: Kubernetes-Native Policy
Kyverno is a policy engine designed specifically for Kubernetes. Unlike OPA which uses Rego, Kyverno defines policies as native Kubernetes resources in YAML, making adoption simpler for teams already familiar with Kubernetes.
# kyverno-policies/require-nonroot.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-run-as-nonroot
annotations:
policies.kyverno.io/title: Require runAsNonRoot
policies.kyverno.io/category: Pod Security
policies.kyverno.io/severity: high
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-containers
match:
any:
- resources:
kinds:
- Pod
validate:
message: >-
Container must run as non-root.
Set securityContext.runAsNonRoot to true.
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: true
---
# Require approved registries
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
spec:
validationFailureAction: Enforce
rules:
- name: validate-registries
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images must come from approved registries."
pattern:
spec:
containers:
- image: "ghcr.io/myorg/* | registry.internal.company.com/*"
Conftest: Validating Configurations
Conftest uses OPA/Rego to validate any type of configuration file: Dockerfile, Kubernetes manifests, Terraform files, CI/CD pipelines. It's particularly useful as a pipeline step to validate configurations before deployment.
# Install Conftest
brew install conftest
# Validate a Dockerfile
conftest test Dockerfile --policy policy/docker/
# Validate Kubernetes manifests
conftest test k8s/ --policy policy/kubernetes/
# Validate with structured output
conftest test --output json terraform/ --policy policy/terraform/
# policy/docker/dockerfile.rego
package docker
# Deny Dockerfile without USER
deny[msg] {
input[i].Cmd == "from"
not any_user_after(i)
msg := "Dockerfile must include the USER instruction to avoid running as root"
}
any_user_after(from_idx) {
input[j].Cmd == "user"
j > from_idx
}
# Deny base images with :latest
deny[msg] {
input[i].Cmd == "from"
val := input[i].Value[0]
endswith(val, ":latest")
msg := sprintf("Avoid :latest in FROM instruction. Use a specific tag: %s", [val])
}
Sentinel: Terraform Policy
Sentinel is HashiCorp's policy framework, natively integrated into Terraform Cloud and Enterprise. It allows defining policies that validate Terraform plans before application, preventing the creation of non-compliant resources.
Policy Engines Comparison
| Feature | OPA/Rego | Kyverno | Sentinel |
|---|---|---|---|
| Scope | Universal | Kubernetes | HashiCorp ecosystem |
| Language | Rego (declarative) | Native K8s YAML | Sentinel (imperative) |
| Learning curve | Medium-high | Low | Medium |
| License | Open source (Apache 2.0) | Open source (Apache 2.0) | Commercial (Terraform Cloud) |
Policy in the CI/CD Pipeline
# .github/workflows/policy-check.yml
name: Policy Validation
on:
pull_request:
jobs:
validate-policies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Conftest
run: |
wget -q https://github.com/open-policy-agent/conftest/releases/download/v0.46.0/conftest_0.46.0_Linux_x86_64.tar.gz
tar xzf conftest_0.46.0_Linux_x86_64.tar.gz
sudo mv conftest /usr/local/bin/
- name: Test OPA policies
run: opa test policy/ -v
- name: Validate Dockerfiles
run: conftest test Dockerfile --policy policy/docker/
- name: Validate K8s manifests
run: conftest test k8s/ --policy policy/kubernetes/
- name: Validate Terraform
run: conftest test terraform/ --policy policy/terraform/
Policy as Code Best Practices
- Version policies: treat policies as code, with reviews, tests, and releases
- Test policies: write unit tests for every policy to avoid regressions
- Gradual rollout: start in audit mode (warning) before switching to enforce (blocking)
- Document: every policy must have a clear description of why it exists
- Quick feedback: integrate validation in PRs for immediate feedback
- Managed exceptions: provide a process for temporary exceptions with expiration
Conclusions
Policy as Code transforms security rules from static documents into executable, automated code. With OPA, Kyverno, and Conftest, organizations can ensure continuous compliance without slowing down the development process.
In the next article, we'll explore CI/CD Security Pipeline, analyzing how to protect the build and deploy process itself: pipeline hardening, OIDC, least privilege, and signed commits.







