Practice — Security & Policy Enforcement
This page holds the five practice tasks (S1–S5) for the Security & Policy Enforcement domain, which is worth 15% of the CNPE — the smallest slice by weight, but the one where the verification steps are cleanest and the points are most reliably bankable. Drill them the way the exam runs: cold, from an empty terminal with nothing left over from the last attempt, time-boxed to 5–7 minutes each, with only the official project docs open — and open the worked solution only after you have genuinely attempted the task, because reading a solution feels like learning and teaches you almost nothing. These five are lifted from the full practice bank, where you will also find the other four domains, the scoring rubric and the 120-minute mock exam plan.
These tasks are about building fences. Some fences decide who is allowed to talk to whom (network policy). Some decide who is allowed to press which button (RBAC). And some are a bouncer on the door who simply refuses to let bad things in at all (admission policy). The last one is about checking the delivery van: what’s actually inside the box, is anything in it broken, and did it really come from our own factory?
Expect the real exam to hand you at least a NetworkPolicy, some RBAC, and one admission policy in either Kyverno or Gatekeeper — you do not get to choose which engine. Background reading for this domain: security & policy, secrets management, governance & compliance, and networking for the CNI side of NetworkPolicy. When a task stalls, the command reference has the imperative shortcuts and the troubleshooting playbook has the “why is this being denied” decision trees.
Network isolation and least-privilege access
☺ Like you’re 10: First fence: who can talk to whom. Second fence: who is allowed to press which button.
These two tasks are the classic pairing, and they share a habit worth internalising early — in both cases the grader checks a negative. Locking something down is easy; proving that the thing you wanted to keep working still works, while the thing you wanted blocked is genuinely blocked, is the actual skill.
S1 · Default-deny the namespace, then open exactly one path
A pen test showed that any compromised pod in payments can reach the database, the metrics stack and the internet. The CNI supports NetworkPolicy. You are asked to close it down without breaking the one call that legitimately needs to work.
Your task:
- Apply a default-deny policy for both ingress and egress in
payments. - Allow the
apipods to receive traffic on 8080 only from pods labelledapp=checkoutin thecheckoutnamespace. - Allow DNS egress to kube-dns, and prove an unauthorised connection now fails.
Done when: a curl from an unlabelled pod to api:8080 times out, the same call from a checkout pod returns 200, and DNS resolution still works inside the namespace.
Show the worked solution
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {} # empty selector = every pod in the namespace
policyTypes: [Ingress, Egress] # with no rules below = deny everything
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-checkout-to-api
namespace: payments
spec:
podSelector:
matchLabels: { app: api }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: checkout }
podSelector:
matchLabels: { app: checkout }
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: payments
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }kubectl -n payments run probe --rm -it --image=curlimages/curl --restart=Never \ -- curl -sS -m 5 http://api:8080/healthz # times out - denied kubectl -n checkout run probe --rm -it --image=curlimages/curl --restart=Never \ --labels=app=checkout -- curl -sS -m 5 http://api.payments:8080/healthz # 200
Why: NetworkPolicies are additive allow-lists — there is no deny rule, so “default deny” is expressed as a policy that selects every pod and lists no rules, and every later policy adds a hole. Two traps cost real points: forgetting DNS egress, which breaks every service name lookup in the namespace and looks like a total outage; and the difference between namespaceSelector and podSelector written as one list item (AND — pods matching that label in that namespace) versus two (OR — anything in that namespace, plus those pods anywhere).
S2 · Build least-privilege RBAC and prove the denial
A deploy bot currently runs with cluster-admin because “it needed to work by Friday.” It only ever reads ConfigMaps and restarts Deployments in one namespace. Audit wants that fixed this sprint.
Your task:
- Create a ServiceAccount
deploy-botinpayments. - Create a Role granting only get/list/watch on ConfigMaps and get/list/patch on Deployments, and bind it.
- Prove the bot can patch a Deployment but cannot read Secrets or touch another namespace.
Done when: kubectl auth can-i patch deployments -n payments run as the bot (--as= plus system:serviceaccount:payments:deploy-bot) prints yes, and the same check for get secrets — and for any verb in namespace checkout — prints no.
Show the worked solution
apiVersion: v1
kind: ServiceAccount
metadata:
name: deploy-bot
namespace: payments
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deploy-bot
namespace: payments
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "patch"]
- apiGroups: ["apps"]
resources: ["deployments/scale"] # subresources are named separately
verbs: ["update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deploy-bot
namespace: payments
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: deploy-bot
subjects:
- kind: ServiceAccount
name: deploy-bot
namespace: paymentsSA=system:serviceaccount:payments:deploy-bot kubectl auth can-i patch deployments --as=$SA -n payments # yes kubectl auth can-i get secrets --as=$SA -n payments # no kubectl auth can-i list pods --as=$SA -n checkout # no kubectl auth can-i --list --as=$SA -n payments # the full picture # find who still has cluster-admin kubectl get clusterrolebindings -o json \ | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name'
Why: kubectl auth can-i --as=… is the fastest verification tool in Kubernetes and it is exactly what a grader would run — get in the habit of proving both the allow and the deny, since a Role that grants nothing also passes a naive “it’s locked down” check. Two details worth memorising: core-group resources use apiGroups: [""], and subresources like deployments/scale and pods/exec must be listed explicitly — granting deployments does not grant its scale subresource.
Admission control — policy as code
☺ Like you’re 10: A bouncer on the cluster door. First he just writes down who would have been turned away, and only later does he actually start turning them away.
Two engines, one idea. You will get whichever one the cluster in front of you has installed, so practise both — and note that both give you a measure-first mode (Audit in Kyverno, dryrun in Gatekeeper) that a well-worded task will expect you to use before you flip the switch.
S3 · Roll out a Kyverno policy from audit to enforce
Half the workloads on the cluster run as root and several deploy from :latest. You cannot simply block them — that would break running teams overnight — so you need to measure the blast radius first, then enforce.
Your task:
- Write a Kyverno
ClusterPolicywith two rules: containers must setrunAsNonRoot: true, and images must not use the:latesttag. - Deploy it in
Auditmode and count the existing violations. - Switch it to
Enforce(excludingkube-system) and prove a violating pod is now rejected.
Done when: kubectl get polr -A (policy reports) lists the failing resources in audit mode, and after switching, kubectl run bad --image=nginx:latest is rejected by the admission webhook with your message.
Show the worked solution
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: pod-baseline
spec:
validationFailureAction: Audit # flip to Enforce after measuring
background: true # also scan existing resources
rules:
- name: require-non-root
match:
any:
- resources:
kinds: [Pod]
exclude:
any:
- resources:
namespaces: [kube-system, kyverno]
validate:
message: "Containers must set securityContext.runAsNonRoot=true"
pattern:
spec:
=(securityContext):
=(runAsNonRoot): "true"
containers:
- =(securityContext):
=(runAsNonRoot): "true"
- name: disallow-latest-tag
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Images must be pinned to an explicit tag or digest, never :latest"
pattern:
spec:
containers:
- image: "!*:latest"kubectl apply -f pod-baseline.yaml
kubectl get clusterpolicy pod-baseline
kubectl get polr -A # PolicyReports: pass/fail counts per namespace
kubectl get cpol pod-baseline -o jsonpath='{.status}' | jq .
# measure, then enforce
kubectl patch cpol pod-baseline --type=merge -p '{"spec":{"validationFailureAction":"Enforce"}}'
kubectl run bad --image=nginx:latest
# error: admission webhook "validate.kyverno.svc-fail" denied the request:
# Images must be pinned to an explicit tag or digest, never :latestWhy: the audit-then-enforce ramp is the professionally correct way to introduce any policy, and it is a genuinely likely exam instruction — background: true makes Kyverno evaluate resources that already exist, so the PolicyReports tell you the blast radius before you break anyone. Kyverno’s pattern syntax uses =(field) for “if present, must match” and ! for negation. Note the field was renamed from the older validationFailureAction: audit/enforce lowercase values to capitalised Audit/Enforce, and newer versions prefer a per-rule failureAction — check the installed version’s docs.
S4 · Write a Gatekeeper ConstraintTemplate and Constraint
A different cluster in the estate standardised on OPA Gatekeeper rather than Kyverno. Governance requires every namespace to carry cost-center and owner labels so spend and incidents can be attributed.
Your task:
- Write a
ConstraintTemplatenamedk8srequiredlabelswith a Rego rule that denies resources missing required labels. - Create a
Constraintapplying it to Namespaces, requiringcost-centerandowner. - Prove a namespace without the labels is rejected, and inspect existing violations.
Done when: kubectl create ns test is denied with your message, creating it with both labels succeeds, and kubectl get k8srequiredlabels ns-must-have-labels -o jsonpath='{.status.totalViolations}' reports pre-existing offenders.
Show the worked solution
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}] {
required := input.parameters.labels[_]
not input.review.object.metadata.labels[required]
msg := sprintf("missing required label: %v", [required])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: ns-must-have-labels
spec:
enforcementAction: deny # or 'dryrun' / 'warn' while you measure
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
excludedNamespaces: [kube-system, gatekeeper-system]
parameters:
labels: ["cost-center", "owner"]kubectl apply -f template.yaml
kubectl get constrainttemplate k8srequiredlabels # wait for it to be created
kubectl apply -f constraint.yaml
kubectl create ns test
# admission webhook "validation.gatekeeper.sh" denied the request:
# [ns-must-have-labels] missing required label: cost-center
kubectl create ns test --dry-run=client -o yaml \
| kubectl label -f - --local -o yaml cost-center=platform owner=team-a \
| kubectl apply -f -
kubectl get k8srequiredlabels ns-must-have-labels -o jsonpath='{.status.totalViolations}'Why: Gatekeeper splits logic from configuration: the ConstraintTemplate holds reusable Rego and generates a new CRD, and each Constraint is an instance of that CRD parameterised for a specific case. That is why the order matters — the template must be established before the Constraint’s kind exists. enforcementAction: dryrun is Gatekeeper’s equivalent of Kyverno’s Audit mode, and the status.totalViolations field comes from the audit controller scanning existing objects on an interval.
Supply-chain security
☺ Like you’re 10: Check the delivery van. What’s in the box, is anything in it broken, and did it really come from our own factory?
One task, three separate questions — and the exam-relevant insight is that scanning, inventory and provenance are genuinely different controls that answer different questions. The last piece, the admission check, is what turns signing from paperwork into enforcement, which loops this sub-theme straight back to the Kyverno work in S3.
S5 · Add vulnerability scanning, an SBOM and signing to a pipeline
A CVE in a base image reached production last quarter and nobody could answer “which of our images contain this library?” for two days. You are adding supply-chain controls to the golden-path pipeline.
Your task:
- Add a Trivy scan step that fails the pipeline on
CRITICALorHIGHfixable vulnerabilities. - Generate an SBOM in SPDX or CycloneDX format and attach it to the image.
- Sign the image with cosign keyless signing and verify the signature.
Done when: the pipeline exits non-zero on a deliberately vulnerable base image, cosign verify succeeds against the signed image, and cosign verify-attestation --type cyclonedx returns the SBOM attestation (the older cosign download sbom was removed in cosign v2).
Show the worked solution
# 1. scan - fail only on fixable HIGH/CRITICAL so the gate stays actionable trivy image --severity HIGH,CRITICAL --ignore-unfixed \ --exit-code 1 --format table registry.internal/acme/checkout:1.0.0 # 2. SBOM trivy image --format cyclonedx --output sbom.cdx.json registry.internal/acme/checkout:1.0.0 syft registry.internal/acme/checkout:1.0.0 -o spdx-json=sbom.spdx.json # alternative # 3. sign + attach + verify (keyless: identity comes from the CI OIDC token) # cosign v2 is keyless by default - COSIGN_EXPERIMENTAL=1 was a v1 requirement cosign sign --yes registry.internal/acme/checkout:1.0.0 cosign attest --yes --predicate sbom.cdx.json --type cyclonedx \ registry.internal/acme/checkout:1.0.0 cosign verify --certificate-identity-regexp 'https://github.com/acme/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ registry.internal/acme/checkout:1.0.0 cosign verify-attestation --type cyclonedx \ --certificate-identity-regexp 'https://github.com/acme/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ registry.internal/acme/checkout:1.0.0
# and close the loop at admission - only signed images may run
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
webhookTimeoutSeconds: 30
rules:
- name: verify-signature
match:
any:
- resources: { kinds: [Pod] }
verifyImages:
- imageReferences: ["registry.internal/acme/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/acme/*"
issuer: "https://token.actions.githubusercontent.com"Why: the three pieces answer three different questions — Trivy asks “does this image contain known vulnerabilities,” the SBOM asks “what is in this image at all,” and cosign asks “did our pipeline actually build this, or did someone push it by hand.” --ignore-unfixed matters in practice: a gate that fails on unfixable CVEs gets switched off within a week, and a switched-off gate protects nothing. The final Kyverno policy is what makes signing more than paperwork — without an admission check, an unsigned image still runs happily.
Drill these five as a set
Because this domain is only 15%, you are likely to see two or three of these shapes on the day — but they are short, so the correct strategy is to bank them early and fast rather than leave them until the clock is tight. The two admission tasks in particular reward knowing which engine is installed before you start typing: kubectl api-resources | grep -E 'kyverno|gatekeeper' costs three seconds and saves you writing the wrong CRD.
Run all five back-to-back as a mini-sitting, shuffled, seven minutes each, no solutions open. Then do the only thing that improves your score: re-run the ones you fumbled 48 hours later, cold. Specifically, if S1 cost you time, the culprit is almost always DNS egress or the one-item-versus-two-item selector — write both patterns out from memory before you start. If S3 or S4 cost you time, the culprit is usually the measure-first step, so practise flipping Audit → Enforce and dryrun → deny with a single kubectl patch rather than re-editing and re-applying the file.
1. Why is there no “deny” rule in a NetworkPolicy, and how do you express default-deny? 2. What breaks first when you apply an egress default-deny and forget one thing — and what is that thing? 3. Which single command proves both the allow and the deny for a ServiceAccount’s RBAC? 4. What does Kyverno’s background: true buy you that plain admission validation does not? 5. Why must a Gatekeeper ConstraintTemplate be applied before its Constraint? 6. Trivy, an SBOM and cosign each answer a different question — what are the three questions?
Check your answers
- NetworkPolicies are additive allow-lists — every policy only ever opens holes, so there is nothing to deny with. Default-deny is a policy with an empty
podSelector: {}(selecting every pod),policyTypes: [Ingress, Egress], and no rules underneath. - DNS. Without an egress allow to
k8s-app: kube-dnsinkube-systemon UDP/TCP 53, every service-name lookup in the namespace fails and it presents as a total outage rather than a policy problem. kubectl auth can-i --as=system:serviceaccount:<ns>:<sa>— run it for the verb you expect to be allowed, forget secrets, and for another namespace;--listgives the whole picture at once.- It evaluates resources that already exist, not just new admission requests, so the PolicyReports (
kubectl get polr -A) tell you the blast radius before you switch to Enforce. - The template holds the reusable Rego and generates the CRD for the constraint kind — until it is established, the Constraint’s
kinddoes not exist on the API server. - Trivy: “does this image contain known vulnerabilities?” SBOM: “what is inside this image at all?” cosign: “did our pipeline actually build this, or did someone push it by hand?”
Back to the full practice bank for the other four domains, the self-scoring rubric and the 120-minute mock exam plan. If a task here exposed a knowledge gap rather than a speed gap, stop drilling and go read security & policy and governance & compliance properly first; if it was purely speed, the command reference and the troubleshooting playbook are the two pages that buy back the most minutes. Adjacent practice sets: architecture (which includes strict mTLS) and GitOps (where signed images meet the delivery pipeline).