CKS — OPA Gatekeeper Lab
OPA Gatekeeper — Hands-On Lab
CKS Exam Domain: Minimize Microservice Vulnerabilities (20%)
This lab walks you through installing OPA Gatekeeper, writing ConstraintTemplates and Constraints, testing enforcement, and using audit mode. Each exercise mirrors the type of tasks you may face on the CKS exam.
Prerequisites: A running Kubernetes cluster (minikube, kind, or kubeadm) with kubectl configured.
Lab 1: Install OPA Gatekeeper
Objective
Install OPA Gatekeeper and verify all components are running.
Steps
# Install Gatekeeper
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.15.0/deploy/gatekeeper.yaml
Wait for all Pods to be ready:
kubectl -n gatekeeper-system wait --for=condition=Ready pod --all --timeout=120s
Verification
# Check namespace
kubectl get ns gatekeeper-system
# Check pods
kubectl get pods -n gatekeeper-system
Expected output:
NAME READY STATUS RESTARTS AGE
gatekeeper-audit-6f8b6f6b7c-x2j4k 1/1 Running 0 60s
gatekeeper-controller-manager-d4c7b8f5d-abc12 1/1 Running 0 60s
gatekeeper-controller-manager-d4c7b8f5d-def34 1/1 Running 0 60s
gatekeeper-controller-manager-d4c7b8f5d-ghi56 1/1 Running 0 60s
# Verify CRDs
kubectl get crd | grep gatekeeper
You should see constrainttemplates.templates.gatekeeper.sh in the list.
# Verify webhook
kubectl get validatingwebhookconfiguration | grep gatekeeper
Expected:
gatekeeper-validating-webhook-configuration 2 60s
Lab 2: Create a ConstraintTemplate to Require Labels
Objective
Create a ConstraintTemplate that checks for required labels on resources.
Steps
Save this as require-labels-template.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
description: "A list of required label keys"
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:
kubectl apply -f require-labels-template.yaml
Verification
# Check the template was created
kubectl get constrainttemplates
# Verify the CRD was registered
kubectl get crd | grep k8srequiredlabels
# Check template status
kubectl describe constrainttemplate k8srequiredlabels
Expected:
NAME AGE
k8srequiredlabels 10s
The CRD k8srequiredlabels.constraints.gatekeeper.sh should now exist.
Lab 3: Create a Constraint Enforcing the Template
Objective
Create a Constraint that requires an app label on all Pods in the default namespace.
Steps
Save as require-app-label-constraint.yaml:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-label
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces: ["default"]
excludedNamespaces: ["kube-system", "gatekeeper-system"]
parameters:
labels: ["app"]
Apply:
kubectl apply -f require-app-label-constraint.yaml
Verification
# Check the constraint
kubectl get K8sRequiredLabels
# Describe for details
kubectl describe K8sRequiredLabels require-app-label
Expected output:
NAME ENFORCEMENT-ACTION TOTAL-VIOLATIONS
require-app-label deny 0
Lab 4: Test — Deploy a Pod Without Required Labels
Objective
Verify that Gatekeeper blocks Pods without the required app label.
Test 1: Pod WITHOUT the required label (should be REJECTED)
kubectl run test-no-label --image=nginx -n default
Expected output:
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request:
[require-app-label] Missing required labels: {"app"}
Test 2: Pod WITH the required label (should be ALLOWED)
kubectl run test-with-label --image=nginx -n default --labels="app=nginx"
Expected output:
pod/test-with-label created
Test 3: Pod in a different namespace (should be ALLOWED — constraint only targets default)
kubectl create namespace test-ns
kubectl run test-other-ns --image=nginx -n test-ns
Expected output:
pod/test-other-ns created
Test 4: Dry-run server-side (tests webhook without creating resource)
kubectl run test-dryrun --image=nginx -n default --dry-run=server -o yaml
Expected: Error — webhook still evaluates dry-run=server requests.
Cleanup
kubectl delete pod test-with-label -n default
kubectl delete pod test-other-ns -n test-ns
kubectl delete namespace test-ns
Lab 5: Create a ConstraintTemplate to Block Privileged Containers
Objective
Create a ConstraintTemplate and Constraint that blocks privileged containers.
Step 1: ConstraintTemplate
Save as block-privileged-template.yaml:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sblockprivilegedcontainer
spec:
crd:
spec:
names:
kind: K8sBlockPrivilegedContainer
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sblockprivilegedcontainer
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])
}
Step 2: Constraint
Save as block-privileged-constraint.yaml:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockPrivilegedContainer
metadata:
name: block-privileged
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system", "gatekeeper-system"]
Apply
kubectl apply -f block-privileged-template.yaml
kubectl apply -f block-privileged-constraint.yaml
Test — Privileged Pod (should be REJECTED)
Save as bad-privileged-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: priv-pod
namespace: default
spec:
containers:
- name: priv
image: nginx
securityContext:
privileged: true
kubectl apply -f bad-privileged-pod.yaml
Expected:
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request:
[block-privileged] Privileged container not allowed: priv
Test — Non-Privileged Pod (should be ALLOWED)
kubectl run safe-pod --image=nginx -n default --labels="app=safe"
Expected:
pod/safe-pod created
Cleanup
kubectl delete pod safe-pod -n default
Lab 6: Create a ConstraintTemplate to Enforce Allowed Image Registries
Objective
Restrict container images to approved registries only.
Step 1: ConstraintTemplate
Save as allowed-registries-template.yaml:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedregistries
spec:
crd:
spec:
names:
kind: K8sAllowedRegistries
validation:
openAPIV3Schema:
type: object
properties:
registries:
type: array
description: "List of allowed image registry prefixes"
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 not from an allowed registry. Allowed: %v",
[container.image, input.parameters.registries])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
not registry_allowed(container.image)
msg := sprintf("Init container image '%v' is not from an allowed registry. Allowed: %v",
[container.image, input.parameters.registries])
}
registry_allowed(image) {
registry := input.parameters.registries[_]
startswith(image, registry)
}
Step 2: Constraint
Save as allowed-registries-constraint.yaml:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
name: allowed-registries
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system", "gatekeeper-system"]
parameters:
registries:
- "docker.io/library/"
- "registry.k8s.io/"
- "gcr.io/company/"
Apply
kubectl apply -f allowed-registries-template.yaml
kubectl apply -f allowed-registries-constraint.yaml
Test — Disallowed Registry (should be REJECTED)
kubectl run bad-image --image=malicious-registry.io/evil:latest -n default --labels="app=bad"
Expected:
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request:
[allowed-registries] Image 'malicious-registry.io/evil:latest' is not from an allowed registry.
Allowed: ["docker.io/library/", "registry.k8s.io/", "gcr.io/company/"]
Test — Allowed Registry (should be ALLOWED)
kubectl run good-image --image=docker.io/library/nginx:1.25 -n default --labels="app=good"
Expected:
pod/good-image created
Cleanup
kubectl delete pod good-image -n default
Lab 7: Use Audit Mode to Scan Existing Violations
Objective
Use Gatekeeper’s audit mode to find existing resources that violate policies.
Step 1: Create Non-Compliant Resources BEFORE the Constraint
First, delete the existing require-app-label constraint:
kubectl delete K8sRequiredLabels require-app-label
Now create Pods without labels:
kubectl run legacy-pod-1 --image=nginx -n default
kubectl run legacy-pod-2 --image=nginx -n default
kubectl run legacy-pod-3 --image=nginx -n default
Step 2: Create Constraint in Dryrun Mode
Save as require-label-dryrun.yaml:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-label-audit
spec:
enforcementAction: dryrun
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces: ["default"]
excludedNamespaces: ["kube-system", "gatekeeper-system"]
parameters:
labels: ["app"]
kubectl apply -f require-label-dryrun.yaml
Step 3: Wait for Audit Scan
The audit controller runs every 60 seconds by default. Wait and then check:
# Wait for audit
sleep 65
# Check violations
kubectl get K8sRequiredLabels require-app-label-audit -o jsonpath='{.status.totalViolations}'
Expected: 3 (or more, depending on other Pods in default namespace without app label).
Step 4: View Violation Details
kubectl get K8sRequiredLabels require-app-label-audit -o yaml | grep -A 10 violations
Expected output:
violations:
- enforcementAction: dryrun
group: ""
kind: Pod
name: legacy-pod-1
namespace: default
version: v1
message: 'Missing required labels: {"app"}'
- enforcementAction: dryrun
group: ""
kind: Pod
name: legacy-pod-2
namespace: default
version: v1
message: 'Missing required labels: {"app"}'
- enforcementAction: dryrun
group: ""
kind: Pod
name: legacy-pod-3
namespace: default
version: v1
message: 'Missing required labels: {"app"}'
Step 5: Fix Violations and Switch to Enforce
Fix existing Pods by adding labels:
kubectl label pod legacy-pod-1 app=legacy -n default
kubectl label pod legacy-pod-2 app=legacy -n default
kubectl label pod legacy-pod-3 app=legacy -n default
Switch enforcement:
kubectl patch K8sRequiredLabels require-app-label-audit --type='json' \
-p='[{"op": "replace", "path": "/spec/enforcementAction", "value": "deny"}]'
Verify:
kubectl get K8sRequiredLabels require-app-label-audit
NAME ENFORCEMENT-ACTION TOTAL-VIOLATIONS
require-app-label-audit deny 0
Cleanup
kubectl delete pod legacy-pod-1 legacy-pod-2 legacy-pod-3 -n default
Full Lab Cleanup
Remove all lab resources:
# Delete constraints
kubectl delete K8sRequiredLabels --all
kubectl delete K8sBlockPrivilegedContainer --all
kubectl delete K8sAllowedRegistries --all
# Delete constraint templates
kubectl delete constrainttemplate k8srequiredlabels
kubectl delete constrainttemplate k8sblockprivilegedcontainer
kubectl delete constrainttemplate k8sallowedregistries
# Optionally uninstall Gatekeeper entirely
kubectl delete -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.15.0/deploy/gatekeeper.yaml
Verify cleanup:
kubectl get constrainttemplates
kubectl get constraints
kubectl get pods -n gatekeeper-system
All should return “No resources found.”
Lab Summary
| Lab | Skill Tested | CKS Relevance |
|---|---|---|
| 1. Install Gatekeeper | Deployment, verification | Setting up policy enforcement |
| 2. ConstraintTemplate | Writing Rego, defining CRDs | Core exam skill |
| 3. Constraint | Matching resources, parameters | Core exam skill |
| 4. Test enforcement | Verify deny/allow behaviour | Validating policies work |
| 5. Block privileged | Security policy enforcement | High-frequency exam topic |
| 6. Allowed registries | Supply chain security | Image trust policies |
| 7. Audit mode | Dryrun, scan existing resources | Safe policy rollout |
Practice Questions
Question 1
You installed Gatekeeper but kubectl run test --image=nginx succeeds even though you have a Constraint that should block it. What should you check?
Show Answer
Debugging checklist:
- Is the Constraint targeting the right kind and namespace?
kubectl describe <ConstraintKind> <constraint-name> | grep -A 10 match - Is the enforcement action set to
deny?kubectl get constraint <name> -o jsonpath='{.spec.enforcementAction}'If it’s
dryrunorwarn, the request is allowed. - Is the ConstraintTemplate’s Rego correct?
kubectl describe constrainttemplate <name>Check for errors in the status.
- Is the Gatekeeper webhook active?
kubectl get validatingwebhookconfiguration | grep gatekeeper - Are the Gatekeeper pods running?
kubectl get pods -n gatekeeper-system - Check controller logs:
kubectl logs -n gatekeeper-system -l control-plane=controller-manager --tail=50
Question 2
Write a one-liner to check how many total violations exist across all Constraints in the cluster.
Show Answer
kubectl get constraints -o jsonpath='{range .items[*]}{.metadata.name}: {.status.totalViolations}{"\n"}{end}'
Or for a single total:
kubectl get constraints -o json | jq '[.items[].status.totalViolations // 0] | add'
This queries all Constraint resources (regardless of type) and sums their violation counts.
Question 3
Create a ConstraintTemplate and Constraint that blocks Pods using the latest tag or no tag at all.
Show Answer
ConstraintTemplate:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sblocklatest
spec:
crd:
spec:
names:
kind: K8sBlockLatest
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sblocklatest
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container '%v' uses :latest tag", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not contains(container.image, ":")
msg := sprintf("Container '%v' has no tag specified", [container.name])
}
Constraint:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockLatest
metadata:
name: block-latest-tag
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system", "gatekeeper-system"]
Question 4
How do you switch a Constraint from dryrun to deny without deleting and recreating it?
Show Answer
Use kubectl patch:
kubectl patch <ConstraintKind> <constraint-name> --type='json' \
-p='[{"op": "replace", "path": "/spec/enforcementAction", "value": "deny"}]'
Or use kubectl edit:
kubectl edit <ConstraintKind> <constraint-name>
# Change enforcementAction from "dryrun" to "deny"
Or modify the YAML file and re-apply:
spec:
enforcementAction: deny
kubectl apply -f constraint.yaml
Question 5
In the CKS exam, you are given a cluster with Gatekeeper installed. A ConstraintTemplate exists but the Constraint is missing. Create a Constraint that uses the K8sAllowedRegistries template to allow only registry.k8s.io/ and block all other registries for Pods in the production namespace.
Show Answer
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
name: production-registry-only
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces: ["production"]
parameters:
registries:
- "registry.k8s.io/"
kubectl apply -f production-registry-constraint.yaml
# Verify
kubectl get K8sAllowedRegistries production-registry-only
# Test — should be denied
kubectl run test --image=docker.io/nginx -n production --labels="app=test"
# Test — should be allowed
kubectl run test --image=registry.k8s.io/pause:3.9 -n production --labels="app=test"
| Previous | Next |
|---|---|
| ← Kyverno Alternative | Monitoring, Logging & Runtime Security → |