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
brew install conftest

Terraform 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/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

  1. Start permissive, tighten gradually — Warn before deny
  2. Test policies thoroughly — Unit tests for every policy
  3. Version your policies — Same rigor as application code
  4. Provide helpful messages — Tell users how to fix violations
  5. Exclude system namespaces — Don’t break kube-system
  6. Monitor enforcement — Track violations and exceptions
  7. 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.