CKS — OPA Gatekeeper Lab

9 minute read

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:

  1. Is the Constraint targeting the right kind and namespace?
    kubectl describe <ConstraintKind> <constraint-name> | grep -A 10 match
    
  2. Is the enforcement action set to deny?
    kubectl get constraint <name> -o jsonpath='{.spec.enforcementAction}'
    

    If it’s dryrun or warn, the request is allowed.

  3. Is the ConstraintTemplate’s Rego correct?
    kubectl describe constrainttemplate <name>
    

    Check for errors in the status.

  4. Is the Gatekeeper webhook active?
    kubectl get validatingwebhookconfiguration | grep gatekeeper
    
  5. Are the Gatekeeper pods running?
    kubectl get pods -n gatekeeper-system
    
  6. 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 →

Updated: