Manual policy enforcement doesn’t scale. Security reviews become bottlenecks. Compliance audits are painful. Policy as code solves this—define policies once, enforce them everywhere, automatically.
Open Policy Agent Basics#
OPA uses Rego, a declarative language for expressing policies.
Simple Policy#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # policy/authz.rego
package authz
default allow = false
# Allow if user is admin
allow {
input.user.role == "admin"
}
# Allow if user owns the resource
allow {
input.user.id == input.resource.owner_id
}
# Allow read access to public resources
allow {
input.action == "read"
input.resource.public == true
}
|
Test the Policy#
1
2
3
4
5
6
7
8
9
10
| # input.json
{
"user": {"id": "user-123", "role": "member"},
"resource": {"owner_id": "user-123", "public": false},
"action": "read"
}
# Run OPA
opa eval -i input.json -d policy/ "data.authz.allow"
# Result: true (user owns the resource)
|
Policy Testing#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # policy/authz_test.rego
package authz
test_admin_allowed {
allow with input as {
"user": {"role": "admin"},
"action": "delete",
"resource": {"owner_id": "other"}
}
}
test_owner_allowed {
allow with input as {
"user": {"id": "user-1", "role": "member"},
"action": "update",
"resource": {"owner_id": "user-1"}
}
}
test_non_owner_denied {
not allow with input as {
"user": {"id": "user-1", "role": "member"},
"action": "update",
"resource": {"owner_id": "user-2", "public": false}
}
}
|
1
2
| # Run tests
opa test policy/ -v
|
Kubernetes Gatekeeper#
Enforce policies on Kubernetes resources at admission time.
Installation#
1
| kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.14.0/deploy/gatekeeper.yaml
|
Constraint Template#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # templates/k8srequiredlabels.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
|
Apply Constraint#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # constraints/require-labels.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-owner-label
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
- apiGroups: ["apps"]
kinds: ["Deployment"]
parameters:
labels:
- "owner"
- "environment"
|
Common Policies#
No Privileged Containers#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8spsprivilegedcontainer
spec:
crd:
spec:
names:
kind: K8sPSPPrivilegedContainer
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8spsprivilegedcontainer
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf("Privileged container not allowed: %v", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
container.securityContext.privileged == true
msg := sprintf("Privileged init container not allowed: %v", [container.name])
}
|
Require Resource Limits#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequireresources
spec:
crd:
spec:
names:
kind: K8sRequireResources
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequireresources
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("Container %v must have CPU limits", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container %v must have memory limits", [container.name])
}
|
Allowed Registries#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedregistries
spec:
crd:
spec:
names:
kind: K8sAllowedRegistries
validation:
openAPIV3Schema:
type: object
properties:
registries:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedregistries
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not registry_allowed(container.image)
msg := sprintf("Image %v is from unauthorized registry", [container.image])
}
registry_allowed(image) {
registry := input.parameters.registries[_]
startswith(image, registry)
}
|
Conftest for CI/CD#
Validate configurations before they’re applied.
Installation#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # policy/terraform.rego
package terraform
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
resource.change.after.versioning[0].enabled != true
msg := sprintf("S3 bucket %v must have versioning enabled", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_security_group_rule"
resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
resource.change.after.from_port <= 22
resource.change.after.to_port >= 22
msg := "SSH (port 22) must not be open to the world"
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_db_instance"
resource.change.after.publicly_accessible == true
msg := sprintf("RDS instance %v must not be publicly accessible", [resource.address])
}
|
1
2
3
4
| # Validate Terraform plan
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
conftest test plan.json -p policy/
|
Kubernetes Manifest Policies#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # policy/kubernetes.rego
package kubernetes
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.resources.limits
msg := sprintf("Container %v must have resource limits", [container.name])
}
deny[msg] {
input.kind == "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot
msg := "Deployment must run as non-root"
}
deny[msg] {
input.kind == "Service"
input.spec.type == "LoadBalancer"
not input.metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-internal"]
msg := "LoadBalancer services must be internal"
}
|
1
2
| # Validate manifests
conftest test deployment.yaml -p policy/
|
Dockerfile Policies#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # policy/dockerfile.rego
package dockerfile
deny[msg] {
input[i].Cmd == "from"
val := input[i].Value[0]
contains(val, ":latest")
msg := "Do not use 'latest' tag for base images"
}
deny[msg] {
input[i].Cmd == "user"
val := input[i].Value[0]
val == "root"
msg := "Do not run as root user"
}
deny[msg] {
input[i].Cmd == "run"
val := concat(" ", input[i].Value)
contains(val, "curl")
contains(val, "|")
contains(val, "sh")
msg := "Avoid piping curl to shell"
}
|
CI Integration#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| # .github/workflows/policy.yml
name: Policy Checks
on:
pull_request:
paths:
- 'terraform/**'
- 'kubernetes/**'
- 'Dockerfile'
jobs:
terraform-policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Plan
working-directory: terraform
run: |
terraform init
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
- name: Policy Check
uses: open-policy-agent/conftest-action@v3
with:
files: terraform/plan.json
policy: policy/terraform
kubernetes-policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Policy Check
uses: open-policy-agent/conftest-action@v3
with:
files: kubernetes/*.yaml
policy: policy/kubernetes
dockerfile-policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Parse Dockerfile
run: |
docker run --rm -i hadolint/hadolint hadolint -f json - < Dockerfile > dockerfile.json
- name: Policy Check
uses: open-policy-agent/conftest-action@v3
with:
files: dockerfile.json
policy: policy/dockerfile
|
Monitoring Policy Violations#
1
2
3
4
5
6
7
8
9
10
11
12
| # Gatekeeper metrics
apiVersion: v1
kind: ServiceMonitor
metadata:
name: gatekeeper
spec:
selector:
matchLabels:
gatekeeper.sh/system: "yes"
endpoints:
- port: metrics
interval: 30s
|
1
2
3
4
5
6
7
8
| # Alert on violations
- alert: PolicyViolationDetected
expr: gatekeeper_violations > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Policy violations detected in cluster"
|
Best Practices#
- Start permissive, tighten gradually — Warn before deny
- Test policies thoroughly — Unit tests for every policy
- Version your policies — Same rigor as application code
- Provide helpful messages — Tell users how to fix violations
- Exclude system namespaces — Don’t break kube-system
- Monitor enforcement — Track violations and exceptions
- Document exceptions — When you bypass, explain why
Policy as code shifts security left. Instead of catching issues in production, catch them at PR time. Instead of audit panic, have continuous compliance. Define once, enforce everywhere.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.