CKS — Rego Policies for CKS
Rego Policies for CKS
CKS Exam Domain: Minimize Microservice Vulnerabilities (20%)
Rego is the policy language used by OPA and Gatekeeper. For the CKS exam, you don’t need to be a Rego expert — you need to read, understand, and write simple Rego rules that enforce common Kubernetes security policies.
Rego Language Fundamentals
What Is Rego?
Rego (pronounced “ray-go”) is a declarative query language designed for policy evaluation. It operates on structured data (JSON/YAML converted to JSON).
Key characteristics:
- Declarative — you describe what violations look like, not step-by-step logic.
- Set-based — rules evaluate to sets of results.
- Unification — variables are bound by matching, not by assignment.
The Gatekeeper Input Object
When Gatekeeper receives an admission request, it provides the full context via input:
{
"input": {
"review": {
"kind": {
"kind": "Pod",
"group": "",
"version": "v1"
},
"object": {
"metadata": {
"name": "my-pod",
"namespace": "default",
"labels": {
"app": "web"
}
},
"spec": {
"containers": [
{
"name": "nginx",
"image": "nginx:latest",
"securityContext": {
"privileged": false,
"runAsUser": 1000
},
"resources": {
"limits": {
"cpu": "500m",
"memory": "128Mi"
}
}
}
]
}
},
"oldObject": null,
"operation": "CREATE",
"userInfo": {
"username": "developer@example.com",
"groups": ["system:authenticated"]
}
},
"parameters": {
"labels": ["app", "team"]
}
}
}
Key Paths
| Path | Description |
|---|---|
input.review.object |
The incoming Kubernetes resource |
input.review.object.metadata |
Name, namespace, labels, annotations |
input.review.object.spec |
Resource spec (containers, volumes, etc.) |
input.review.oldObject |
Previous version (for UPDATE operations) |
input.review.operation |
CREATE, UPDATE, DELETE |
input.review.userInfo |
Who submitted the request |
input.parameters |
Values from the Constraint’s spec.parameters |
The Violation Rule
In Gatekeeper, the Rego rule must be named violation. Every truthy evaluation of a violation rule produces a policy violation.
Basic Pattern
package <template-name>
violation[{"msg": msg}] {
# condition 1
# condition 2
# ...all conditions must be true for violation to fire
msg := "Human-readable violation message"
}
- All statements in a rule body are AND-ed together.
- Multiple
violationrules with the same head are OR-ed (any one can fire). - The rule produces a violation when all conditions in the body are satisfied.
Multiple Rules = OR
# Violation if container is privileged
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf("Container '%v' is privileged", [container.name])
}
# OR violation if init container is privileged
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
container.securityContext.privileged == true
msg := sprintf("Init container '%v' is privileged", [container.name])
}
Common CKS Rego Patterns
Pattern 1: Require Labels
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])
}
Line-by-line:
provided— builds a set of all label keys on the object.required— builds a set of required label keys from parameters.missing— set difference: required labels not found on the object.- If any are missing, produce a violation.
Pattern 2: Block Privileged Containers
package k8sblockprivileged
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])
}
Key detail: containers[_] iterates over all containers. The rule fires for each container that matches.
Pattern 3: Enforce Allowed Registries (String Prefix Check)
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])
}
registry_allowed(image) {
registry := input.parameters.registries[_]
startswith(image, registry)
}
Key Rego concepts:
not— negation: the image is NOT from an allowed registry.- Helper function
registry_allowed— returns true if any registry prefix matches. startswith(image, registry)— built-in string function.
Pattern 4: Require Resource Limits
package k8srequirelimits
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("Container '%v' must set CPU limits", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container '%v' must set memory limits", [container.name])
}
Two separate rules — one for CPU, one for memory. Both fire independently.
Pattern 5: Block hostPath Volumes
package k8sblockhostpath
violation[{"msg": msg}] {
volume := input.review.object.spec.volumes[_]
volume.hostPath
msg := sprintf("hostPath volume '%v' is not allowed", [volume.name])
}
Security impact: hostPath volumes expose the host filesystem to the container. An attacker can read /etc/shadow, write to /var/run/docker.sock, or access any host file.
Pattern 6: Block Latest Tag
package k8sblocklatest
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container '%v' uses :latest tag — use a specific version", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not contains(container.image, ":")
msg := sprintf("Container '%v' has no tag — use a specific version", [container.name])
}
Images without a tag default to :latest, so we check for both cases.
Pattern 7: Require runAsNonRoot
package k8srunasnonroot
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.securityContext.runAsNonRoot == true
not input.review.object.spec.securityContext.runAsNonRoot == true
msg := sprintf("Container '%v' must set runAsNonRoot: true", [container.name])
}
This checks both the container-level and Pod-level securityContext.
Rego Syntax Reference for CKS
Iteration
# Iterate over all containers
container := input.review.object.spec.containers[_]
# Iterate over all volumes
volume := input.review.object.spec.volumes[_]
# Iterate with index
container := input.review.object.spec.containers[i]
Comparison
# Equality
container.securityContext.privileged == true
# Inequality (negation of equality)
not container.securityContext.runAsNonRoot == true
# Numeric comparison
input.review.object.spec.replicas > 5
String Functions
# Prefix check
startswith(image, "gcr.io/")
# Suffix check
endswith(image, ":latest")
# Contains
contains(image, "malicious")
# Format string
msg := sprintf("Error: %v in %v", [detail, context])
Set Operations
# Create a set from an array
labels := {l | l := input.parameters.labels[_]}
# Set difference
missing := required - provided
# Count elements
count(missing) > 0
Negation
# "not" negates a condition or helper function
not container.resources.limits.cpu
not registry_allowed(container.image)
Helper Functions
# Define a helper (returns true if any condition matches)
registry_allowed(image) {
registry := input.parameters.registries[_]
startswith(image, registry)
}
# Use in a rule
violation[{"msg": msg}] {
not registry_allowed(container.image)
...
}
Debugging Rego
Using opa eval Locally
Install OPA CLI:
# macOS
brew install opa
# Linux
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64
chmod +x opa && sudo mv opa /usr/local/bin/
Create a test input file:
// input.json
{
"review": {
"object": {
"metadata": {
"name": "test-pod",
"labels": {}
},
"spec": {
"containers": [
{
"name": "nginx",
"image": "nginx:latest",
"securityContext": {
"privileged": true
}
}
]
}
}
},
"parameters": {
"labels": ["app", "team"]
}
}
Create the policy file:
# policy.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])
}
Evaluate:
opa eval --input input.json --data policy.rego "data.k8srequiredlabels.violation"
Expected output:
{
"result": [
{
"expressions": [
{
"value": [
{"msg": "Missing required labels: {\"app\", \"team\"}"}
]
}
]
}
]
}
Using opa test
Write test cases:
# policy_test.rego
package k8srequiredlabels
test_missing_labels {
results := violation with input as {
"review": {"object": {"metadata": {"labels": {}}}},
"parameters": {"labels": ["app"]}
}
count(results) > 0
}
test_labels_present {
results := violation with input as {
"review": {"object": {"metadata": {"labels": {"app": "web"}}}},
"parameters": {"labels": ["app"]}
}
count(results) == 0
}
Run tests:
opa test policy.rego policy_test.rego -v
data.k8srequiredlabels.test_missing_labels: PASS (1.234ms)
data.k8srequiredlabels.test_labels_present: PASS (0.567ms)
--------------------------------------------------------------------------------
PASS: 2/2
Testing Rego Policies Before Deploying
Workflow
- Write the Rego policy locally.
- Test with
opa evalandopa test. - Embed in a ConstraintTemplate YAML.
- Deploy the ConstraintTemplate with
kubectl apply. - Create a Constraint in
dryrunmode. - Verify violations are reported correctly.
- Switch to
denyenforcement.
Using Gatekeeper’s gator CLI
gator is Gatekeeper’s testing tool — it evaluates ConstraintTemplates and Constraints locally without a cluster:
# Install gator
go install github.com/open-policy-agent/gatekeeper/v3/cmd/gator@latest
# Test a template + constraint + test resource
gator test --filename template.yaml --filename constraint.yaml --filename test-pod.yaml
CKS Exam Rego Tips
-
Don’t memorise complex Rego — understand the
violationpattern andinput.review.objectpath. -
Pattern-match from Kubernetes documentation — the exam provides access to Kubernetes docs. The Gatekeeper library examples are well-documented.
-
Start with a working template and modify — it’s faster to adapt an existing ConstraintTemplate than to write one from scratch.
-
Key paths to remember:
input.review.object.metadata.name input.review.object.metadata.namespace input.review.object.metadata.labels input.review.object.spec.containers[_] input.review.object.spec.initContainers[_] input.review.object.spec.volumes[_] input.review.object.spec.containers[_].securityContext input.review.object.spec.containers[_].image input.review.object.spec.containers[_].resources.limits input.parameters.<param-name> -
Keep rules simple — one violation per check. Don’t combine multiple checks in one rule.
-
Always check both containers and initContainers — common exam trap.
-
Use
sprintffor clear messages — it helps with debugging and the exam grader can see your intent.
Complete ConstraintTemplate with Rego — End-to-End
Here is a complete, exam-ready example that blocks containers with dangerous capabilities:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sblockdangerouscaps
spec:
crd:
spec:
names:
kind: K8sBlockDangerousCaps
validation:
openAPIV3Schema:
type: object
properties:
blockedCapabilities:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sblockdangerouscaps
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
cap := container.securityContext.capabilities.add[_]
blocked := input.parameters.blockedCapabilities[_]
upper(cap) == upper(blocked)
msg := sprintf("Container '%v' adds blocked capability: %v", [container.name, cap])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
cap := container.securityContext.capabilities.add[_]
blocked := input.parameters.blockedCapabilities[_]
upper(cap) == upper(blocked)
msg := sprintf("Init container '%v' adds blocked capability: %v", [container.name, cap])
}
Constraint:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockDangerousCaps
metadata:
name: block-dangerous-caps
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system"]
parameters:
blockedCapabilities:
- "SYS_ADMIN"
- "NET_RAW"
- "SYS_PTRACE"
Practice Questions
Question 1
Write a Rego violation rule that blocks Pods with hostNetwork: true.
Show Answer
package k8sblockhostnetwork
violation[{"msg": msg}] {
input.review.object.spec.hostNetwork == true
msg := sprintf("Pod '%v' uses hostNetwork which is not allowed",
[input.review.object.metadata.name])
}
hostNetwork: true gives the Pod access to the node’s network namespace, including all listening services and network interfaces. This is a significant security risk.
Question 2
What does containers[_] mean in Rego?
Show Answer
containers[_] is Rego’s iteration syntax. The _ is a wildcard variable that iterates over all elements of the containers array.
container := input.review.object.spec.containers[_]
This binds container to each element of the containers array in turn. If the Pod has 3 containers, the rule evaluates 3 times — once for each container.
It’s equivalent to a for each loop in imperative languages.
Question 3
Fix this Rego policy — it’s supposed to require memory limits but doesn’t work:
package k8srequirelimits
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.resources.limits.memory == ""
msg := sprintf("Container '%v' must set memory limits", [container.name])
}
Show Answer
The issue is that when memory is not set, the field doesn’t exist — it’s not an empty string. Comparing to "" won’t catch missing fields.
Fixed version:
package k8srequirelimits
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container '%v' must set memory limits", [container.name])
}
Use not container.resources.limits.memory to check for the absence of the field. This also handles cases where resources or limits are entirely missing.
Question 4
Write a complete ConstraintTemplate that blocks Pods mounting /var/run/docker.sock as a hostPath volume.
Show Answer
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sblockdockersock
spec:
crd:
spec:
names:
kind: K8sBlockDockerSock
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sblockdockersock
violation[{"msg": msg}] {
volume := input.review.object.spec.volumes[_]
volume.hostPath.path == "/var/run/docker.sock"
msg := sprintf("Volume '%v' mounts Docker socket — this is not allowed", [volume.name])
}
Mounting the Docker socket gives the container full control over the Docker daemon, enabling container escape and host compromise.
Question 5
How do you pass parameters from a Constraint to the Rego policy, and how are they accessed?
Show Answer
In the ConstraintTemplate — define the parameter schema:
spec:
crd:
spec:
validation:
openAPIV3Schema:
type: object
properties:
maxReplicas:
type: integer
In the Constraint — provide parameter values:
spec:
parameters:
maxReplicas: 3
In the Rego policy — access via input.parameters:
violation[{"msg": msg}] {
input.review.object.spec.replicas > input.parameters.maxReplicas
msg := sprintf("Replicas %v exceeds max allowed %v",
[input.review.object.spec.replicas, input.parameters.maxReplicas])
}
The flow: Constraint spec.parameters → Gatekeeper → Rego input.parameters.
| Previous | Next |
|---|---|
| ← Constraints | Kyverno Alternative → |