Module 4.2: OPA & Gatekeeper
Toolkit Track | Complexity:
[COMPLEX]| Time: 45-50 minutes
Overview
Section titled “Overview”“Trust but verify” doesn’t work in Kubernetes—you need “deny by default, allow explicitly.” This module covers Open Policy Agent (OPA) and Gatekeeper for policy-as-code admission control, ensuring every resource that enters your cluster meets your security and compliance requirements.
What You’ll Learn:
- OPA and Rego policy language basics
- Gatekeeper architecture and constraints
- Writing effective admission policies
- Policy testing and CI/CD integration
Prerequisites:
- Security Principles Foundations
- Kubernetes admission controllers concept
- Basic programming logic
What You’ll Be Able to Do
Section titled “What You’ll Be Able to Do”After completing this module, you will be able to:
- Deploy OPA Gatekeeper and configure ConstraintTemplates for Kubernetes admission policy enforcement
- Implement Rego policies for pod security, resource limits, label requirements, and image restrictions
- Configure Gatekeeper audit mode for policy violation reporting without blocking existing workloads
- Evaluate OPA Gatekeeper against Kyverno for policy-as-code enforcement complexity and flexibility trade-offs
Why This Module Matters
Section titled “Why This Module Matters”Without admission control, anyone with kubectl create permissions can deploy anything—privileged containers, images from untrusted registries, pods without resource limits. Gatekeeper acts as your cluster’s bouncer, checking every resource against your policies before allowing it in.
💡 Did You Know? Open Policy Agent was created by Styra and donated to CNCF in 2018. It’s now used beyond Kubernetes—in Terraform, Envoy, Kafka, and even CI/CD pipelines. Learning OPA/Rego is an investment that pays off across the entire cloud-native stack.
The Admission Control Problem
Section titled “The Admission Control Problem”WITHOUT POLICY ENFORCEMENT════════════════════════════════════════════════════════════════════
Developer kubectl apply ──▶ API Server ──▶ etcd ──▶ 😱 Running │ │ No checks! │ - Privileged container? ✓ │ - No resource limits? ✓ │ - Image from docker.io? ✓ │ - Root filesystem writable? ✓
═══════════════════════════════════════════════════════════════════
WITH GATEKEEPER════════════════════════════════════════════════════════════════════
Developer kubectl apply ──▶ API Server ──▶ Gatekeeper ──▶ etcd │ │ │ │ Check policies: │ │ - No privileged? ✓ │ │ - Has limits? ✓ │ │ - Allowed registry? ✓ │ │ - Read-only root? ✓ │ │ │ ▼ │ DENY or ALLOW │ │ If denied: └──▶ "Error: container must not be privileged"Open Policy Agent (OPA)
Section titled “Open Policy Agent (OPA)”What is OPA?
Section titled “What is OPA?”OPA is a general-purpose policy engine. You give it:
- Data - JSON representing current state
- Query - What you want to know
- Policy - Rules written in Rego
OPA DECISION FLOW════════════════════════════════════════════════════════════════════
┌───────────────┐│ Policy │ # Written in Rego│ (rules.rego) │└───────┬───────┘ │ ▼┌───────────────┐ ┌───────────────┐│ OPA │◀────│ Input │ # JSON data to evaluate│ Engine │ │ (request) │└───────┬───────┘ └───────────────┘ │ ▼┌───────────────┐│ Decision │ # allow: true/false│ (output) │ # violations: [...]└───────────────┘Rego Language Basics
Section titled “Rego Language Basics”# Rego 101 - The Policy Language
# Package declaration (namespace for rules)package kubernetes.admission
# Import statementsimport future.keywords.inimport future.keywords.containsimport future.keywords.if
# Constantsallowed_registries := ["gcr.io", "registry.example.com"]
# Rules - evaluate to true/false or a set of values
# Simple boolean ruleis_privileged if { input.request.object.spec.containers[_].securityContext.privileged == true}
# Rule with iteration (for each container)violation contains msg if { container := input.request.object.spec.containers[_] not container.resources.limits.cpu msg := sprintf("Container '%v' has no CPU limit", [container.name])}
# Rule with comprehensionall_container_images := [image | container := input.request.object.spec.containers[_] image := container.image]
# Helper functionstarts_with_allowed_registry(image) if { some registry in allowed_registries startswith(image, registry)}Key Rego Concepts
Section titled “Key Rego Concepts”| Concept | Example | Description |
|---|---|---|
| Unification | x := input.name | Assignment with pattern matching |
| Iteration | containers[_] | Iterate over array elements |
| Comprehension | [x | x := arr[_]] | Build arrays/sets from iteration |
| some | some i; arr[i] | Explicit iteration variable |
| contains | set contains msg if {...} | Add to set when condition true |
| default | default allow := false | Default value if rule undefined |
💡 Did You Know? Rego is named after the Lego brick factory in Billund, Denmark. The creators wanted policies to “snap together” like Lego bricks. The language was designed specifically for policy—it’s declarative, making it easier to audit than imperative code.
Gatekeeper
Section titled “Gatekeeper”Architecture
Section titled “Architecture”GATEKEEPER ARCHITECTURE════════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐│ Kubernetes Cluster ││ ││ kubectl apply ──▶ API Server ││ │ ││ │ ValidatingAdmissionWebhook ││ ▼ ││ ┌────────────────────────────────────────────────────────────┐ ││ │ GATEKEEPER │ ││ │ │ ││ │ ┌─────────────────┐ ┌─────────────────┐ │ ││ │ │ Constraint │ │ OPA Engine │ │ ││ │ │ Templates │────▶│ │ │ ││ │ │ (Rego policies) │ │ Evaluate │ │ ││ │ └─────────────────┘ │ Request │ │ ││ │ └────────┬────────┘ │ ││ │ ┌─────────────────┐ │ │ ││ │ │ Constraints │──────────────┘ │ ││ │ │ (policy params) │ │ ││ │ └─────────────────┘ │ ││ │ │ ││ └────────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ Allow or Deny + Message ││ │└─────────────────────────────────────────────────────────────────┘Key Components
Section titled “Key Components”| Component | Purpose | Example |
|---|---|---|
| ConstraintTemplate | Defines reusable policy logic (Rego) | “Container must have resource limits” |
| Constraint | Instance of template with parameters | ”Apply to namespace ‘prod’, require CPU/memory” |
| Config | Gatekeeper settings | Exempt namespaces, audit interval |
Installation
Section titled “Installation”# Install Gatekeeperkubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.14.0/deploy/gatekeeper.yaml
# Verify installationkubectl get pods -n gatekeeper-system
# Check webhook is registeredkubectl get validatingwebhookconfigurationsWriting Policies
Section titled “Writing Policies”ConstraintTemplate Structure
Section titled “ConstraintTemplate Structure”apiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8srequiredlabels # lowercase, no spacesspec: crd: spec: names: kind: K8sRequiredLabels # CamelCase validation: openAPIV3Schema: type: object properties: labels: type: array items: type: string targets: - target: admission.k8s.gatekeeper.sh rego: | package k8srequiredlabels
violation[{"msg": msg}] { # Get provided labels provided := {label | input.review.object.metadata.labels[label]}
# Get required labels required := {label | label := input.parameters.labels[_]}
# Find missing missing := required - provided count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing]) }Constraint Usage
Section titled “Constraint Usage”apiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sRequiredLabelsmetadata: name: require-team-labelspec: match: kinds: - apiGroups: [""] kinds: ["Pod"] - apiGroups: ["apps"] kinds: ["Deployment", "StatefulSet"] namespaces: - "production" excludedNamespaces: - "kube-system" parameters: labels: - "team" - "app"Common Policy Patterns
Section titled “Common Policy Patterns”# 1. Require Container Resource LimitsapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8scontainerlimitsspec: crd: spec: names: kind: K8sContainerLimits targets: - target: admission.k8s.gatekeeper.sh rego: | package k8scontainerlimits
violation[{"msg": msg}] { container := input.review.object.spec.containers[_] not container.resources.limits.cpu msg := sprintf("Container '%v' has no CPU limit", [container.name]) }
violation[{"msg": msg}] { container := input.review.object.spec.containers[_] not container.resources.limits.memory msg := sprintf("Container '%v' has no memory limit", [container.name]) }---# 2. Allowed Container RegistriesapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8sallowedreposspec: crd: spec: names: kind: K8sAllowedRepos validation: openAPIV3Schema: type: object properties: repos: type: array items: type: string targets: - target: admission.k8s.gatekeeper.sh rego: | package k8sallowedrepos
violation[{"msg": msg}] { container := input.review.object.spec.containers[_] not image_allowed(container.image) msg := sprintf("Container '%v' uses image '%v' from disallowed registry", [container.name, container.image]) }
image_allowed(image) { repo := input.parameters.repos[_] startswith(image, repo) }---# 3. Block Privileged ContainersapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8sblockprivilegedspec: crd: spec: names: kind: K8sBlockPrivileged targets: - target: admission.k8s.gatekeeper.sh rego: | package k8sblockprivileged
violation[{"msg": msg}] { container := input.review.object.spec.containers[_] container.securityContext.privileged == true msg := sprintf("Container '%v' must not run as privileged", [container.name]) }
violation[{"msg": msg}] { container := input.review.object.spec.initContainers[_] container.securityContext.privileged == true msg := sprintf("Init container '%v' must not run as privileged", [container.name]) }💡 Did You Know? Gatekeeper includes a library of pre-built policies called the Gatekeeper Library. There are 50+ ready-to-use ConstraintTemplates covering Pod Security Standards, general security, and Kubernetes best practices.
Policy Testing
Section titled “Policy Testing”Testing Rego Locally
Section titled “Testing Rego Locally”# Install OPA CLIbrew install opa # or download from https://www.openpolicyagent.org/
# Create test filecat > policy_test.rego << 'EOF'package k8sallowedrepos
test_allowed_registry { image_allowed("gcr.io/my-project/app:v1") with input.parameters.repos as ["gcr.io/"]}
test_disallowed_registry { not image_allowed("docker.io/nginx:latest") with input.parameters.repos as ["gcr.io/"]}EOF
# Run testsopa test . -vGatekeeper Dry-Run Mode
Section titled “Gatekeeper Dry-Run Mode”# Test constraint without enforcingapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sBlockPrivilegedmetadata: name: block-privileged-dry-runspec: enforcementAction: dryrun # warn or deny for enforcement match: kinds: - apiGroups: [""] kinds: ["Pod"]# Check violations in auditkubectl get k8sblockprivileged block-privileged-dry-run -o yaml
# Look at status.violationsCI/CD Integration
Section titled “CI/CD Integration”name: Test OPA Policieson: [push, pull_request]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Setup OPA uses: open-policy-agent/setup-opa@v2 with: version: latest
- name: Run Rego tests run: opa test policies/ -v
- name: Validate ConstraintTemplates run: | for file in policies/*.yaml; do opa fmt --check "$file" || exit 1 doneAdvanced Patterns
Section titled “Advanced Patterns”External Data
Section titled “External Data”# Sync data from cluster into OPAapiVersion: config.gatekeeper.sh/v1alpha1kind: Configmetadata: name: config namespace: gatekeeper-systemspec: sync: syncOnly: - group: "" version: "v1" kind: "Namespace" - group: "networking.k8s.io" version: "v1" kind: "Ingress"# Use synced data in policypackage k8suniqueingress
violation[{"msg": msg}] { input.review.kind.kind == "Ingress" host := input.review.object.spec.rules[_].host
# Check against all existing ingresses other := data.inventory.namespace[ns][otherapiversion]["Ingress"][name] other.spec.rules[_].host == host
# Not the same ingress other.metadata.name != input.review.object.metadata.name
msg := sprintf("Ingress host '%v' already in use by '%v/%v'", [host, ns, name])}Mutation Policies
Section titled “Mutation Policies”# Gatekeeper also supports mutationapiVersion: mutations.gatekeeper.sh/v1kind: Assignmetadata: name: add-default-securitycontextspec: applyTo: - groups: [""] kinds: ["Pod"] versions: ["v1"] match: scope: Namespaced kinds: - apiGroups: [""] kinds: ["Pod"] location: "spec.securityContext.runAsNonRoot" parameters: assign: value: true💡 Did You Know? Gatekeeper’s mutation feature was one of its most requested additions. Before mutation, teams had to use separate tools like Kyverno or custom webhooks to add default values. Now you can both validate AND mutate with a single tool—enforce that pods run as non-root, and automatically add the setting if developers forget.
Debugging Policies
Section titled “Debugging Policies”# Check constraint statuskubectl describe k8srequiredlabels require-team-label
# View audit resultskubectl get constraint -o json | jq '.items[].status'
# Check Gatekeeper logskubectl logs -n gatekeeper-system -l control-plane=controller-manager
# Test policy with specific inputopa eval --data policy.rego --input input.json "data.k8srequiredlabels.violation"Common Mistakes
Section titled “Common Mistakes”| Mistake | Problem | Solution |
|---|---|---|
| Policy blocks kube-system | Core components can’t deploy | Exclude kube-system in constraints |
| No dry-run testing | Breaking changes hit production | Use enforcementAction: dryrun first |
| Overly strict policies | Developers can’t work | Start permissive, tighten gradually |
| Complex Rego with no tests | Policy bugs in production | Write unit tests for every policy |
| Forgetting init containers | Security holes in init phase | Always check both containers and initContainers |
| Blocking all during rollout | Existing pods fail validation | Gatekeeper only checks new/updated resources |
War Story: The Policy That Cried Wolf
Section titled “War Story: The Policy That Cried Wolf”A platform team deployed strict container registry policies on Monday. By Wednesday, they’d received 500+ Slack messages asking for exceptions.
What went wrong: They blocked everything except gcr.io/company-project/, but:
- Helm charts pulled from
ghcr.io - Logging agents used
docker.io/fluent/ - Monitoring needed
quay.io/prometheus/
The fix:
- Audit existing images BEFORE deploying policies
- Start with
dryrunto identify violations - Build allowlist from actual usage, not assumptions
- Communicate changes with 2-week notice
# Audit existing imageskubectl get pods -A -o jsonpath='{.items[*].spec.containers[*].image}' | tr ' ' '\n' | sort -uQuestion 1
Section titled “Question 1”What’s the difference between a ConstraintTemplate and a Constraint?
Show Answer
ConstraintTemplate: Defines the policy LOGIC in Rego. It’s like a class definition or function.
Constraint: Instance of a template with specific PARAMETERS. It’s like calling a function with arguments.
Example:
- ConstraintTemplate: “Check if labels exist”
- Constraint: “Require labels
teamandenvon Pods in namespaceproduction”
One ConstraintTemplate can have many Constraints with different parameters.
Question 2
Section titled “Question 2”How do you test a policy without blocking deployments?
Show Answer
Use enforcementAction: dryrun in the Constraint:
spec: enforcementAction: dryrun # Options: deny, dryrun, warndeny: Block violations (default)dryrun: Record violations but don’t blockwarn: Show warning but allow
Check violations via: kubectl get constraint <name> -o yaml
Question 3
Section titled “Question 3”Why might a policy work for Pods but miss deployments?
Show Answer
Gatekeeper evaluates the exact resource submitted. When you kubectl apply a Deployment, Gatekeeper sees the Deployment—not the Pods it will create.
Solutions:
- Match on
Pod- catches pods when created by controllers - Match on
DeploymentAND check.spec.template.spec.containers - Use the pod-specific path in Rego:
# For Deploymentscontainers := input.review.object.spec.template.spec.containers# For Podscontainers := input.review.object.spec.containers
Hands-On Exercise
Section titled “Hands-On Exercise”Objective
Section titled “Objective”Create and deploy policies to enforce container security standards.
Environment Setup
Section titled “Environment Setup”# Install Gatekeeperkubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.14.0/deploy/gatekeeper.yaml
# Wait for itkubectl wait --for=condition=ready pod -l control-plane=controller-manager -n gatekeeper-system --timeout=90s-
Create ConstraintTemplate for allowed registries (provided above)
-
Deploy Constraint allowing only
gcr.io/andghcr.io/:apiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sAllowedReposmetadata:name: allowed-reposspec:match:kinds:- apiGroups: [""]kinds: ["Pod"]parameters:repos:- "gcr.io/"- "ghcr.io/" -
Test policy by deploying a violating pod:
Terminal window kubectl run nginx --image=nginx:latest# Should be denied -
Test compliance with allowed registry:
Terminal window kubectl run allowed --image=gcr.io/google-containers/pause:3.2# Should succeed -
Check audit results:
Terminal window kubectl get k8sallowedrepos allowed-repos -o yaml
Success Criteria
Section titled “Success Criteria”- Gatekeeper controller running
- ConstraintTemplate created successfully
- Constraint shows
0totalViolations initially -
nginx:latestdeployment blocked with clear error message -
gcr.io/image allowed - Audit shows violation details for blocked attempt
Bonus Challenge
Section titled “Bonus Challenge”Add a second constraint that requires all pods to have a team label.
Further Reading
Section titled “Further Reading”- OPA Documentation
- Gatekeeper Documentation
- Gatekeeper Policy Library
- Rego Playground - Test policies online
Next Module
Section titled “Next Module”Continue to Module 4.3: Falco to learn runtime security monitoring for detecting threats in running containers.
“Security is not a feature you add—it’s a constraint you enforce. Gatekeeper makes that constraint automatic.”