Completed upto lesson 28

This commit is contained in:
Mosiur Rahman
2026-08-23 15:29:06 +06:00
parent fb86f0b60a
commit 8e43c00cd6
104 changed files with 1888 additions and 0 deletions

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 1: choose the control</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0001 · 15 minutes</p><h1>Choose the security control before you touch YAML</h1><p class="lede">A CKS task often feels large because several controls sound plausible. Your first win is narrowing the problem to the one mechanism that can enforce the stated outcome.</p><div class="callout"><strong>Mission connection:</strong> a security engineer must choose a control that is both effective and scoped correctly. This is how you become faster in the exam and more credible at work.</div><h2>The control map</h2><ul><li><strong>Who may call the Kubernetes API?</strong> RBAC: Role/ClusterRole plus binding.</li><li><strong>Which pod configuration may be created?</strong> Pod Security Admission or another admission policy.</li><li><strong>Which packets may flow?</strong> NetworkPolicy—provided the CNI enforces it.</li><li><strong>What may the process do inside its container?</strong> <code>securityContext</code>, capabilities, seccomp, AppArmor/SELinux.</li><li><strong>What happened through the API?</strong> Audit logging.</li></ul><p>This map follows the official <a href="https://kubernetes.io/docs/concepts/security/">Kubernetes security overview</a>. The practical exam is performance-based and currently runs for two hours; use the <a href="https://training.linuxfoundation.org/certification/certified-kubernetes-security-specialist/">Linux Foundation page</a> as the authority for changing logistics.</p><h2>Practice: classify first, configure second</h2><div class="quiz"><h3>Requirement</h3><p>“In namespace <code>payments</code>, only pods labelled <code>app=api</code> may reach TCP 5432 on pods labelled <code>app=db</code>. Everything else should remain denied.”</p><p>What is the primary control?</p><label><input type="radio" name="q" value="rbac"> RBAC Role and RoleBinding</label><label><input type="radio" name="q" value="psa"> Pod Security Admission labels</label><label><input type="radio" name="q" value="network"> NetworkPolicy with a selected ingress rule</label><label><input type="radio" name="q" value="audit"> Kubernetes audit policy</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Deliberate drill (10 minutes)</h2><ol><li>Without looking up syntax, write a NetworkPolicy that selects <code>app=db</code>, permits TCP 5432 only from <code>app=api</code>, and applies in <code>payments</code>.</li><li>Validate it: confirm the selectors, port, protocol, and namespace; then test an allowed and a disallowed source if your lab supports it.</li><li>Compare with the official <a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">NetworkPolicy documentation</a>. Add the first thing you missed to your error log.</li></ol><p>When you finish, paste your manifest or tell me what felt uncertain. I’ll review it as your teacher and choose the next just-right drill.</p><footer>Next reference: <a href="../reference/cks-four-week-field-guide.html">CKS four-week field guide</a></footer></main><script>function check(){const chosen=document.querySelector('input[name=q]:checked');const f=document.getElementById('feedback');if(!chosen){f.textContent='Choose one control first.';f.className='feedback bad';return;}if(chosen.value==='network'){f.textContent='Correct. This is a traffic-flow requirement; NetworkPolicy expresses the allowed ingress precisely.';f.className='feedback good';}else{f.textContent='Not this time. The requirement controls pod-to-pod network traffic, so start with NetworkPolicy.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 2: verify NetworkPolicy</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0002 · 20 minutes</p><h1>NetworkPolicy: prove what is denied</h1><p class="lede">Using documentation is a strength, not a weakness. In CKS and in real operations, the differentiator is proving that a policy has the intended effect—not merely applying YAML without errors.</p><div class="callout"><strong>Confidence move:</strong> after every security change, state one traffic flow that must work and one that must fail. Then test both.</div><h2>The verification sequence</h2><ol><li><strong>Confirm selection.</strong> <code>kubectl get pods -n payments --show-labels</code>. A correct rule selecting the wrong labels does nothing useful.</li><li><strong>Confirm policy type and direction.</strong> Read <code>spec.policyTypes</code>: ingress and egress are independent. An isolated pod may need explicit DNS egress.</li><li><strong>Test the allow path.</strong> From an <code>app=api</code> pod, connect to the database Service on TCP 5432.</li><li><strong>Test the deny path.</strong> From a deliberately different-labelled pod in the same namespace, repeat the identical connection.</li><li><strong>Check enforcement assumptions.</strong> NetworkPolicy only works when the installed CNI supports and enforces it.</li></ol><p>These mechanics are grounded in the official <a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">NetworkPolicy documentation</a>. It explicitly warns that behavior depends on the networking solution.</p><h2>Retrieval check</h2><div class="quiz"><h3>Scenario</h3><p>A policy selects <code>app=db</code> and has an ingress rule permitting <code>app=api</code> to TCP 5432. The API pod still cannot resolve <code>db.payments.svc.cluster.local</code> after you add an egress-isolating policy to it. What did you most likely omit?</p><label><input type="radio" name="q" value="rbac"> A RoleBinding for the API ServiceAccount</label><label><input type="radio" name="q" value="dns"> An egress allowance to cluster DNS on port 53</label><label><input type="radio" name="q" value="psa"> A restricted Pod Security Admission label</label><label><input type="radio" name="q" value="audit"> An audit-policy rule for DNS queries</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Timed drill: build a test matrix (12 minutes)</h2><p>For the policy you wrote in Lesson 1, make this table in a note before touching the cluster. Then run every row your lab permits.</p><table><thead><tr><th>Source</th><th>Destination</th><th>Expected</th><th>Observed</th></tr></thead><tbody><tr><td><code>app=api</code></td><td><code>app=db:5432</code></td><td>allowed</td><td></td></tr><tr><td>other labelled pod</td><td><code>app=db:5432</code></td><td>denied</td><td></td></tr><tr><td><code>app=api</code></td><td>cluster DNS :53</td><td>allowed if egress isolated</td><td></td></tr></tbody></table><p>Write one command you used to test each row and record any mismatch. Then ask me with the result; I’ll help you diagnose it, as you would in a security incident.</p><footer>Review: <a href="../reference/cks-four-week-field-guide.html">CKS four-week field guide</a> · Next: RBAC scope and escalation paths.</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose one answer.';f.className='feedback bad';return;}if(c.value==='dns'){f.textContent='Correct. Egress isolation can block DNS; allow the required DNS destination and port explicitly.';f.className='feedback good';}else{f.textContent='Not quite. The symptom is name resolution after egress isolation, so inspect DNS traffic first.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1,8 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 3: RBAC least privilege</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0003 · 20 minutes</p><h1>RBAC: grant the minimum—and prove the boundary</h1><p class="lede">Security work is not “make it work.” It is “make the required action work while every unnecessary action stays blocked.”</p><div class="callout"><strong>Career connection:</strong> least-privilege reviews are a routine platform-security responsibility. The fastest path is always: identify subject → scope → resource → verb → verification.</div><h2>Timed task: 12 minutes</h2><p>In namespace <code>cks-rbac</code>, grant ServiceAccount <code>report-reader</code> permission to <strong>get</strong> and <strong>list</strong> only <code>configmaps</code>. It must <strong>not</strong> create ConfigMaps or read Secrets. Use a namespace-scoped Role and RoleBinding.</p><ol><li>Set up only the challenge objects: <code>kubectl --context kind-cks-lab apply -f .\lab\rbac-starter.yaml</code>.</li><li>Create your answer in <code>lab/rbac-answer.yaml</code>. Use the official <a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">RBAC reference</a>—documentation use is expected.</li><li>Apply it to the isolated lab.</li><li>Run the four verification commands below. Do not stop after the first success.</li></ol><h2>Proof commands</h2><pre><code>kubectl --context kind-cks-lab auth can-i get configmaps \
-n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader
kubectl --context kind-cks-lab auth can-i list configmaps \
-n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader
kubectl --context kind-cks-lab auth can-i create configmaps \
-n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader
kubectl --context kind-cks-lab auth can-i get secrets \
-n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader</code></pre><p>Expected result sequence: <strong>yes, yes, no, no</strong>.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>A requirement is confined to one namespace. Which binding usually best supports least privilege?</p><label><input type="radio" name="q" value="cluster"> ClusterRoleBinding</label><label><input type="radio" name="q" value="role"> RoleBinding in that namespace</label><label><input type="radio" name="q" value="admin"> Binding to cluster-admin</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Debrief</h2><p>Send me your four results and your YAML. I’ll review scope, subject, resource names, verbs, and whether a narrower permission is possible. That review is the feedback loop that builds confidence.</p><footer>Primary source: <a href="https://kubernetes.io/docs/concepts/security/rbac-good-practices/">Kubernetes RBAC good practices</a> · Review: <a href="../reference/cks-four-week-field-guide.html">CKS four-week field guide</a></footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='role'){f.textContent='Correct. A RoleBinding lets you scope authorization to the namespace where it is needed.';f.className='feedback good';}else{f.textContent='Not quite. Prefer namespace scope when the requirement does not need cluster-wide access.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 4: harden a workload</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0004 · 25 minutes</p><h1>Harden a workload without breaking it</h1><p class="lede">You already know how to write YAML. This drill trains the security-engineering question: which process-level privileges does this workload actually need?</p><div class="callout"><strong>Rule of thumb:</strong> every privilege you do not explicitly need is attack surface. Start restrictive, then add only what the workload demonstrably requires.</div><h2>The hardening controls</h2><ul><li><code>runAsNonRoot: true</code> and <code>runAsUser: 1000</code>: prevents the process from running as UID 0.</li><li><code>allowPrivilegeEscalation: false</code>: blocks gaining more privilege through mechanisms such as setuid binaries.</li><li><code>capabilities.drop: ["ALL"]</code>: removes Linux capabilities unless one is explicitly justified.</li><li><code>readOnlyRootFilesystem: true</code>: prevents writes to the image filesystem.</li><li><code>seccompProfile.type: RuntimeDefault</code>: applies the container runtime’s default syscall filter.</li></ul><p>Use the official <a href="https://kubernetes.io/docs/concepts/security/linux-kernel-security-constraints/">Linux kernel security constraints</a> and <a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/">SecurityContext task guide</a> if you need syntax or a deeper explanation.</p><h2>Timed task: 15 minutes</h2><p>The existing <code>inventory</code> pod in namespace <code>cks-workload</code> is intentionally insecure. Create <code>lab/workload-hardening-answer.yaml</code> that preserves the pod name, namespace, image, command, and label while adding every control from the list above.</p><ol><li>Inspect the current manifest: <code>kubectl --context kind-cks-lab -n cks-workload get pod inventory -o yaml</code>.</li><li>Write your replacement manifest from memory. The fast way to replace a Pod is <code>kubectl --context kind-cks-lab replace --force -f .\lab\workload-hardening-answer.yaml</code>.</li><li>Wait for it: <code>kubectl --context kind-cks-lab -n cks-workload wait --for=condition=Ready pod/inventory --timeout=60s</code>.</li><li>Verify identity: <code>kubectl --context kind-cks-lab -n cks-workload exec inventory -- id</code>. It must show a non-zero UID.</li></ol><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>A container needs no special kernel privilege. What is the secure default for Linux capabilities?</p><label><input type="radio" name="q" value="add"> Add NET_ADMIN for troubleshooting</label><label><input type="radio" name="q" value="drop"> Drop ALL capabilities</label><label><input type="radio" name="q" value="default"> Leave image defaults unchanged</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>What to send me</h2><p>Paste your YAML plus the output of <code>id</code>. I’ll verify the security context and show you one exam-speed inspection command.</p><footer>Previous: <a href="0003-rbac-least-privilege.html">RBAC least privilege</a> · Reference: <a href="../reference/cks-four-week-field-guide.html">CKS four-week field guide</a></footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='drop'){f.textContent='Correct. Drop ALL, then add back only a specifically required capability.';f.className='feedback good';}else{f.textContent='Not quite. Least privilege starts by dropping capabilities that are not explicitly required.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1,4 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 5: Pod Security Standards</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0005 · 25 minutes · Minimize Microservice Vulnerabilities</p><h1>Enforce a restricted workload boundary</h1><p class="lede">A hardened Pod is useful; preventing the next insecure Pod is better. This CKS-style task combines namespace labels, admission behavior, and a compliant workload.</p><div class="callout"><strong>Exam reflex:</strong> first identify the enforcement scope. Pod Security Admission is configured with labels on the Namespace, not annotations on an individual Pod.</div><h2>Timed task: 15 minutes</h2><p>In namespace <code>cks-pss</code>, enforce the <code>restricted</code> Pod Security Standard for the Kubernetes version running in the cluster. Then make the existing <code>orders</code> Pod compliant and running. Create your answer as <code>lab/pod-security-standards-answer.yaml</code>.</p><p>Start with <code>kubectl --context kind-cks-lab apply -f .\lab\pod-security-standards-starter.yaml</code>. Your answer must add the required Namespace labels and preserve the Pod name, namespace, image, command, and label.</p><h2>Proof, not assumption</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\pod-security-standards-answer.yaml
kubectl --context kind-cks-lab -n cks-pss wait --for=condition=Ready pod/orders --timeout=60s
kubectl --context kind-cks-lab -n cks-pss get pod orders -o jsonpath='{.spec.securityContext.seccompProfile.type}{"\n"}{.spec.containers[0].securityContext}'
kubectl --context kind-cks-lab -n cks-pss run root-test --image=busybox:1.37 --restart=Never -- sh -c 'sleep 60'</code></pre><p>The final command should be rejected by admission. Remove the failed test Pod if one was created.</p><h2>Tricky retrieval check</h2><div class="quiz"><h3>Question</h3><p>A Pod has <code>spec.securityContext.runAsNonRoot: true</code>, but its only container sets <code>securityContext.runAsUser: 0</code>. In a namespace enforcing <code>restricted</code>, what should you expect?</p><label><input type="radio" name="q" value="inherit">It runs as a non-root user because the Pod-level value always wins.</label><label><input type="radio" name="q" value="reject">It is rejected because the container-level setting overrides the Pod-level setting and violates the policy.</label><label><input type="radio" name="q" value="warn">It is admitted with a warning because UID 0 is allowed under restricted.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/">Pod Security Standards</a> reference. Look up the exact Namespace label keys and restricted requirements rather than relying on memory.</p><h2>What to send me</h2><p>Paste your answer YAML plus the rejection message from <code>root-test</code>. I’ll check both the policy scope and whether your workload satisfies it.</p><footer>Previous: <a href="0004-workload-hardening.html">workload hardening</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='reject'){f.textContent='Correct. A container securityContext takes precedence where it is set. UID 0 fails the restricted runAsNonRoot requirement.';f.className='feedback good';}else{f.textContent='Not quite. Container-level securityContext values override the Pod-level values for that container; UID 0 violates restricted.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 6: Read-only filesystems</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0006 · 25 minutes · Minimize Microservice Vulnerabilities</p><h1>Harden a web workload without breaking its writable paths</h1><p class="lede">A read-only root filesystem limits post-compromise persistence, but applications still need deliberate places for transient state. This drill builds that boundary.</p><div class="callout"><strong>Exam reflex:</strong> when you set <code>readOnlyRootFilesystem: true</code>, identify every path the process must write to and mount only those paths as temporary writable volumes.</div><h2>Timed task: 15 minutes</h2><p>In namespace <code>payments-prod</code>, harden the <code>payments</code> Pod while keeping it available on port <code>8080</code>. Begin with <code>kubectl --context kind-cks-lab apply -f .\lab\payments-hardening-starter.yaml</code> and write the completed manifest to <code>lab/payments-hardening-answer.yaml</code>.</p><p>Preserve the Pod name, namespace, image, label, and container port. Add the following controls:</p><ul><li>Run as non-root UID <code>101</code>.</li><li>Set <code>allowPrivilegeEscalation: false</code>.</li><li>Drop all Linux capabilities.</li><li>Use the <code>RuntimeDefault</code> seccomp profile.</li><li>Set <code>readOnlyRootFilesystem: true</code>.</li><li>Mount one writable <code>emptyDir</code> volume at <code>/tmp</code>.</li></ul><p>This uses the unprivileged NGINX image, which listens on <code>8080</code>. A non-root process that has dropped all capabilities cannot generally bind port 80; changing the port is the correct secure design rather than adding <code>NET_BIND_SERVICE</code>.</p><h2>Proof, not assumption</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\payments-hardening-answer.yaml&#10;kubectl --context kind-cks-lab -n payments-prod wait --for=condition=Ready pod/payments --timeout=60s&#10;kubectl --context kind-cks-lab -n payments-prod exec payments -- id&#10;kubectl --context kind-cks-lab -n payments-prod exec payments -- sh -c 'touch /tmp/ok &amp;&amp; test -f /tmp/ok'&#10;kubectl --context kind-cks-lab -n payments-prod exec payments -- sh -c 'touch /root/nope'</code></pre><p>The identity must have a non-zero UID, the <code>/tmp</code> check must succeed, and the final command must fail with a read-only filesystem error.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is an <code>emptyDir</code> volume usually preferable to leaving the image filesystem writable for runtime scratch data?</p><label><input type="radio" name="q" value="isolated">It limits writes to an explicit, ephemeral path while keeping the image filesystem immutable.</label><label><input type="radio" name="q" value="faster">It makes all application writes faster.</label><label><input type="radio" name="q" value="permanent">It preserves the data across Pod replacement.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/">Configure a Security Context for a Pod or Container</a> guide to confirm security-context placement and volume-mount syntax.</p><h2>What to send me</h2><p>Paste your answer YAML and the three proof outputs. I’ll check the writable boundary and show you a fast inspection command.</p><footer>Previous: <a href="0005-pod-security-standards.html">Pod Security Standards</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='isolated'){f.textContent='Correct. The writable area is explicit and ephemeral, while the rest of the image filesystem stays protected.';f.className='feedback good';}else{f.textContent='Not quite. emptyDir is useful because it narrows writable state to a defined, temporary location.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 7: ServiceAccount token exposure</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0007 · 20 minutes · Minimize Microservice Vulnerabilities</p><h1>Remove API credentials a workload does not need</h1><p class="lede">Every automatically mounted ServiceAccount token is a potential credential exposed through a container compromise. Disable it when the workload does not call the Kubernetes API.</p><div class="callout"><strong>Exam reflex:</strong> distinguish identity from credential mounting. A Pod can name a ServiceAccount for policy clarity while still opting out of its token volume.</div><h2>Timed task: 12 minutes</h2><p>The <code>checkout</code> Pod runs in the <code>checkout</code> namespace and does not need to call the Kubernetes API. Apply <code>lab/serviceaccount-token-starter.yaml</code>, then create <code>lab/serviceaccount-token-answer.yaml</code> that preserves its name, namespace, label, image, command, and <code>serviceAccountName</code> while preventing the token from being mounted into this Pod.</p><p>Use the smallest-scoped native control: make the change at the Pod specification, rather than changing the ServiceAccount globally.</p><h2>Proof, not assumption</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\serviceaccount-token-answer.yaml&#10;kubectl --context kind-cks-lab -n checkout wait --for=condition=Ready pod/checkout --timeout=60s&#10;kubectl --context kind-cks-lab -n checkout exec checkout -- sh -c 'test ! -e /var/run/secrets/kubernetes.io/serviceaccount/token'&#10;kubectl --context kind-cks-lab -n checkout get pod checkout -o jsonpath='{.spec.automountServiceAccountToken}{"\n"}'</code></pre><p>The token-path check must return success and the final command must print <code>false</code>.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Where should you set the control when only one Pod must not receive a ServiceAccount token, while other Pods using the same ServiceAccount might still need one?</p><label><input type="radio" name="q" value="pod">On that Pod’s spec.</label><label><input type="radio" name="q" value="namespace">On the Namespace.</label><label><input type="radio" name="q" value="cluster">On the API server.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/">Configure Service Accounts for Pods</a> guide to confirm the precedence of Pod and ServiceAccount token-mount settings.</p><h2>What to send me</h2><p>Paste your answer YAML and the output of both proof commands. I’ll check the scope and the runtime credential boundary.</p><footer>Previous: <a href="0006-read-only-root-filesystem.html">Read-only root filesystem</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='pod'){f.textContent='Correct. The Pod-level field limits the change to this workload.';f.className='feedback good';}else{f.textContent='Not quite. Set the Pod-level control when the exception is specific to one workload.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 8: NetworkPolicy default deny</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0008 · 35 minutes · Minimize Microservice Vulnerabilities</p><h1>Build a default-deny network boundary from scratch</h1><p class="lede">This is intentionally closer to an exam task: the workload fixture is supplied, but no NetworkPolicy skeleton or selector hints are provided.</p><div class="callout"><strong>Exam reflex:</strong> NetworkPolicies are additive. Start by naming which traffic must survive, then express the smallest set of ingress and egress exceptions after default denial.</div><h2>Timed task: 25 minutes</h2><p>Apply <code>lab/lesson-8-fixture.yaml</code>. Then create <code>lab/lesson-8-answer.yaml</code> containing the NetworkPolicies needed to meet every requirement below. Do not change the fixture.</p><ul><li>In namespace <code>cks-net-hard</code>, default-deny both ingress and egress for every Pod.</li><li>Only the Pod labelled <code>app=checkout</code> may call the <code>ledger</code> Service on TCP port <code>8080</code>.</li><li><code>checkout</code> must be able to resolve Kubernetes Service names through cluster DNS.</li><li><code>ledger</code> must not have any egress permission.</li><li><code>intruder</code> must not be able to reach <code>ledger</code>.</li></ul><p>Your answer must use label selectors. Do not use IP blocks, and do not allow all traffic to or from the namespace.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-8-fixture.yaml&#10;kubectl --context kind-cks-lab -n cks-net-hard wait --for=condition=Ready pod/ledger pod/checkout pod/intruder --timeout=90s&#10;kubectl --context kind-cks-lab apply -f .\lab\lesson-8-answer.yaml&#10;kubectl --context kind-cks-lab -n cks-net-hard exec checkout -- wget -qO- -T 5 http://ledger:8080&#10;kubectl --context kind-cks-lab -n cks-net-hard exec intruder -- wget -qO- -T 5 http://ledger:8080&#10;kubectl --context kind-cks-lab -n cks-net-hard exec ledger -- wget -qO- -T 5 http://kubernetes.default</code></pre><p>Expected results: <code>checkout</code> prints <code>ledger</code>; <code>intruder</code> times out; <code>ledger</code> times out. The two denied commands should have a non-zero exit code.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>After a default-deny egress policy is applied to <code>checkout</code>, why can its request to <code>http://ledger:8080</code> fail before a TCP connection is attempted?</p><label><input type="radio" name="q" value="dns">The Pod also needs an egress allowance for DNS resolution.</label><label><input type="radio" name="q" value="service">Services cannot work with NetworkPolicies.</label><label><input type="radio" name="q" value="ingress">The ledger Pod needs unrestricted egress.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">Network Policies</a> reference. Inspect the CoreDNS labels in your own cluster before writing its selector.</p><h2>What to send me</h2><p>Paste only your policy YAML and the three command results. I’ll assess selector scope, DNS handling, and whether your default deny is genuinely complete.</p><footer>Previous: <a href="0007-serviceaccount-token-exposure.html">ServiceAccount token exposure</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='dns'){f.textContent='Correct. Service names require DNS first, so default-deny egress needs a specific DNS exception.';f.className='feedback good';}else{f.textContent='Not quite. The service name must first be resolved through DNS, which egress policy can block.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 9: Precise RBAC</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0009 · 35 minutes · Minimize Microservice Vulnerabilities</p><h1>Grant incident access without granting escalation paths</h1><p class="lede">A useful incident identity often needs narrow read access across a namespace. The trap is solving that requirement with a wildcard or by forgetting that logs are a separate subresource.</p><div class="callout"><strong>Exam reflex:</strong> translate every requested action into an API resource, verb, and scope. Treat <code>pods/log</code> as distinct from <code>pods</code>, and use <code>resourceNames</code> when the task names a single object.</div><h2>Timed task: 25 minutes</h2><p>Apply <code>lab/lesson-9-fixture.yaml</code>. Then create <code>lab/lesson-9-answer.yaml</code> from scratch.</p><p>In the <code>incident-response</code> namespace, grant the ServiceAccount <code>audit-bot</code> from namespace <code>security-ops</code> exactly these permissions:</p><ul><li><code>get</code>, <code>list</code>, and <code>watch</code> Pods.</li><li><code>get</code> Pod logs.</li><li><code>get</code> only the ConfigMap named <code>incident-playbook</code>.</li></ul><p>It must not be able to read Secrets, create Pods, execute into Pods, or read any other ConfigMap. Use a namespace-scoped role and binding; do not use a ClusterRole or wildcard resources/verbs.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-9-fixture.yaml&#10;kubectl --context kind-cks-lab apply -f .\lab\lesson-9-answer.yaml&#10;$sa = 'system:serviceaccount:security-ops:audit-bot'&#10;kubectl --context kind-cks-lab auth can-i get pods --as=$sa -n incident-response&#10;kubectl --context kind-cks-lab auth can-i watch pods --as=$sa -n incident-response&#10;kubectl --context kind-cks-lab auth can-i get pods/log --as=$sa -n incident-response&#10;kubectl --context kind-cks-lab auth can-i get configmap/incident-playbook --as=$sa -n incident-response&#10;kubectl --context kind-cks-lab auth can-i get configmap/another-config --as=$sa -n incident-response&#10;kubectl --context kind-cks-lab auth can-i get secrets --as=$sa -n incident-response&#10;kubectl --context kind-cks-lab auth can-i create pods --as=$sa -n incident-response&#10;kubectl --context kind-cks-lab auth can-i create pods/exec --as=$sa -n incident-response</code></pre><p>Expected answers, in order: <code>yes yes yes yes no no no no</code>.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why does granting <code>get</code> on <code>pods</code> not automatically permit <code>kubectl logs</code>?</p><label><input type="radio" name="q" value="subresource">Logs use the distinct <code>pods/log</code> subresource.</label><label><input type="radio" name="q" value="namespace">Pod logs are always cluster-scoped.</label><label><input type="radio" name="q" value="secret">Logs require Secret access.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">RBAC reference</a>. Confirm the subject format for a cross-namespace ServiceAccount and the behavior of <code>resourceNames</code>.</p><h2>What to send me</h2><p>Paste only your Role and RoleBinding YAML, then the eight <code>can-i</code> results. I’ll review least privilege—not merely whether the allowed commands return yes.</p><footer>Previous: <a href="0008-networkpolicy-default-deny.html">NetworkPolicy default deny</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='subresource'){f.textContent='Correct. RBAC authorizes the pods/log subresource independently.';f.className='feedback good';}else{f.textContent='Not quite. Pod logs use their own API subresource, pods/log.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 10: Minimize Secret exposure</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0010 · 35 minutes · Minimize Microservice Vulnerabilities</p><h1>Expose only the Secret data an application needs</h1><p class="lede">Mounting a Secret is not automatically least privilege: every key becomes readable to the process. This drill combines a single-key Secret projection with the workload controls you should now retrieve from memory.</p><div class="callout"><strong>Exam reflex:</strong> secure the data path, not just the Secret object. Limit which keys are projected, where they appear, who can read them, and whether the Pod receives unrelated API credentials.</div><h2>Timed task: 25 minutes</h2><p>Apply <code>lab/lesson-10-fixture.yaml</code>. In namespace <code>billing</code>, create <code>lab/lesson-10-answer.yaml</code> from scratch containing a Pod named <code>reconciler</code>.</p><p>The Pod must use image <code>busybox:1.37</code> with command <code>sh -c 'sleep 3600'</code>, run as UID <code>1000</code>, and satisfy all requirements below:</p><ul><li>Mount only the <code>api-key</code> key from Secret <code>billing-credentials</code>.</li><li>Expose it as the file <code>/var/run/credentials/api-key</code>; the Secret’s <code>legacy-password</code> must not be present in the container.</li><li>Make the projected Secret volume read-only for the container.</li><li>Do not mount a ServiceAccount token.</li><li>Use <code>RuntimeDefault</code> seccomp, prevent privilege escalation, and drop all Linux capabilities.</li><li>Use a read-only root filesystem while still allowing the process to use <code>/tmp</code>.</li></ul><p>Do not use environment variables for the Secret and do not use wildcard-like broad mounts of the Secret volume.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-10-fixture.yaml&#10;kubectl --context kind-cks-lab apply -f .\lab\lesson-10-answer.yaml&#10;kubectl --context kind-cks-lab -n billing wait --for=condition=Ready pod/reconciler --timeout=60s&#10;kubectl --context kind-cks-lab -n billing exec reconciler -- id&#10;kubectl --context kind-cks-lab -n billing exec reconciler -- cat /var/run/credentials/api-key&#10;kubectl --context kind-cks-lab -n billing exec reconciler -- sh -c 'test ! -e /var/run/credentials/legacy-password &amp;&amp; touch /tmp/proof'&#10;kubectl --context kind-cks-lab -n billing exec reconciler -- sh -c 'test ! -e /var/run/secrets/kubernetes.io/serviceaccount/token'</code></pre><p>Expected: a non-zero UID, <code>test-key</code>, and success from each final two commands.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Which Secret-volume mechanism prevents an unneeded key from being mounted at all?</p><label><input type="radio" name="q" value="items">Specify the allowed key under the volume’s <code>items</code> list.</label><label><input type="radio" name="q" value="readonly">Set only <code>readOnly: true</code> on the volume mount.</label><label><input type="radio" name="q" value="mode">Set a restrictive file mode alone.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the Kubernetes <a href="https://kubernetes.io/docs/concepts/configuration/secret/">Secret</a> reference for selective key projection and the <a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/">Security Context</a> guide for the runtime controls.</p><h2>What to send me</h2><p>Paste your Pod YAML and the four proof results. I’ll inspect both the visible behavior and whether the manifest prevents the broader Secret exposure.</p><footer>Previous: <a href="0009-rbac-subresources-and-resourcenames.html">Precise RBAC</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='items'){f.textContent='Correct. items controls which Secret keys are projected into the volume.';f.className='feedback good';}else{f.textContent='Not quite. Read-only and file mode help protect mounted data, but items limits which keys appear at all.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 11: AppArmor confinement</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0011 · 35 minutes · System Hardening</p><h1>Apply runtime confinement to a multi-container Pod</h1><p class="lede">Linux capabilities and seccomp reduce a container’s power. AppArmor adds a path- and operation-oriented confinement layer. The difficult part is ensuring every container receives the intended profile without relying on deprecated annotations.</p><div class="callout"><strong>Exam reflex:</strong> prefer structured <code>securityContext</code> fields over legacy annotations. A container-level setting overrides the Pod-level setting, so inspect both levels when validating a multi-container workload.</div><h2>Timed task: 25 minutes</h2><p>Apply <code>lab/lesson-11-fixture.yaml</code>. Create <code>lab/lesson-11-answer.yaml</code> from scratch containing the Pod described below.</p><p>Create Pod <code>event-forwarder</code> in namespace <code>runtime-hardening</code> with two containers, both using <code>busybox:1.37</code> and command <code>sh -c 'sleep 3600'</code>:</p><ul><li>Container names: <code>forwarder</code> and <code>metrics</code>.</li><li>Both containers must receive the runtime’s default AppArmor profile. Use the current structured API field—not an AppArmor annotation.</li><li>Both containers must prevent privilege escalation and drop all capabilities.</li><li>Run the Pod as a non-root UID <code>1000</code>, with RuntimeDefault seccomp.</li><li>Do not mount a ServiceAccount token.</li><li>Use a read-only root filesystem for both containers, while allowing each to write under <code>/tmp</code>.</li><li>Do not enable host networking, host PID, host IPC, privileged mode, or hostPath volumes.</li></ul><p>You decide whether a Pod-level or container-level field is the most concise correct way to satisfy the AppArmor requirement. Your manifest must not contain a conflicting override.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-11-fixture.yaml&#10;kubectl --context kind-cks-lab apply -f .\lab\lesson-11-answer.yaml&#10;kubectl --context kind-cks-lab -n runtime-hardening wait --for=condition=Ready pod/event-forwarder --timeout=60s&#10;kubectl --context kind-cks-lab -n runtime-hardening exec event-forwarder -c forwarder -- id&#10;kubectl --context kind-cks-lab -n runtime-hardening exec event-forwarder -c metrics -- sh -c 'touch /tmp/proof'&#10;kubectl --context kind-cks-lab -n runtime-hardening get pod event-forwarder -o jsonpath='{.spec.securityContext.appArmorProfile.type}{"\n"}{.spec.securityContext.seccompProfile.type}{"\n"}{.spec.automountServiceAccountToken}{"\n"}'</code></pre><p>Both containers must be Ready, run as a non-zero UID, and write successfully to <code>/tmp</code>. If you set AppArmor at container level, inspect the equivalent container field instead of expecting the final command’s first value.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>A Pod-level AppArmor profile is <code>RuntimeDefault</code>, but one container sets its own profile to <code>Unconfined</code>. Which profile applies to that container?</p><label><input type="radio" name="q" value="container">The container-level <code>Unconfined</code> profile, because it overrides the Pod-level setting.</label><label><input type="radio" name="q" value="pod">The Pod-level RuntimeDefault profile.</label><label><input type="radio" name="q" value="merge">Both profiles merge into a stricter profile.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/tutorials/security/apparmor/">AppArmor tutorial</a> and inspect the API schema with <code>kubectl explain pod.spec.securityContext.appArmorProfile</code>.</p><h2>What to send me</h2><p>Paste your Pod YAML and the proof outputs. I’ll inspect effective profile scope as well as the usual workload-hardening controls.</p><footer>Previous: <a href="0010-minimize-secret-exposure.html">Minimize Secret exposure</a> · CKS domain: System Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='container'){f.textContent='Correct. A container-level AppArmor profile overrides the Pod-level profile for that container.';f.className='feedback good';}else{f.textContent='Not quite. The explicit container-level profile takes precedence for that container.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1,4 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 12: Audit investigation</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0012 · 30 minutes · Monitoring, Logging, and Runtime Security</p><h1>Reconstruct an RBAC escalation from audit evidence</h1><p class="lede">Audit logs record API activity, not intent. Your job is to filter noise, correlate a principal and source address, and identify the attack sequence with evidence.</p><div class="callout"><strong>Incident reflex:</strong> begin with high-impact API resources and verbs—RBAC writes, Secret reads, and exec activity—then pivot by username, source IP, and timestamps.</div><h2>Timed task: 20 minutes</h2><p>Analyze <code>lab/lesson-12-audit.jsonl</code>. Do not edit the file. Send me a concise incident note answering all of these:</p><ol><li>Which principal is suspicious?</li><li>What source IP is associated with the activity?</li><li>What is the earliest suspicious timestamp?</li><li>Which ClusterRoleBinding was created?</li><li>Which Secret was accessed?</li><li>What runtime action followed the Secret access?</li><li>Give two immediate containment actions, in order.</li></ol><p>Use PowerShell, <code>jq</code>, or another structured JSON tool. Do not rely on free-text visual scanning alone.</p><h2>Useful investigation shapes</h2><pre><code>Get-Content .\lab\lesson-12-audit.jsonl | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object { $_.objectRef.resource -in 'clusterrolebindings','secrets' } | Select-Object requestReceivedTimestamp, verb, user, sourceIPs, objectRef
# Linux exam-style equivalent
jq -c 'select(.objectRef.resource == "clusterrolebindings" or .objectRef.resource == "secrets")' lesson-12-audit.jsonl</code></pre><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Which audit event is the strongest initial indicator of privilege escalation?</p><label><input type="radio" name="q" value="binding">Creating a ClusterRoleBinding.</label><label><input type="radio" name="q" value="service">Reading a Service.</label><label><input type="radio" name="q" value="pod">Creating an ordinary application Pod.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/">auditing</a> documentation to review audit stages, levels, and policy behavior.</p><h2>What to send me</h2><p>Send your seven answers and the exact filter command you used. I’ll check the timeline and the containment priorities.</p><footer>Previous: <a href="0011-apparmor-runtime-confinement.html">AppArmor runtime confinement</a> · CKS domain: Monitoring, Logging, and Runtime Security</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='binding'){f.textContent='Correct. Creating a ClusterRoleBinding can grant broad authority and warrants immediate investigation.';f.className='feedback good';}else{f.textContent='Not quite. An RBAC binding write is a high-impact control-plane action and a strong escalation signal.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 13: Image digest admission</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0013 · 45 minutes · Supply Chain Security</p><h1>Enforce immutable container images at admission</h1><p class="lede">A tag can be moved after review; a digest identifies immutable content. This drill combines workload remediation with a native admission control that blocks the next mutable image.</p><div class="callout"><strong>Exam reflex:</strong> distinguish detecting an unsafe image from preventing its reintroduction. First remediate the workload; then enforce the intended rule at the narrowest practical scope.</div><h2>Timed task: 35 minutes</h2><p>Apply <code>lab/lesson-13-fixture.yaml</code>. Create <code>lab/lesson-13-answer.yaml</code> from scratch. Do not alter the fixture.</p><p>Your answer must contain all resources needed to meet these requirements:</p><ul><li>Update Deployment <code>frontend</code> so its container image is an immutable digest reference from the official Docker Hub library. Preserve its name, namespace, replica count, labels, container name, and command.</li><li>Create a native <code>ValidatingAdmissionPolicy</code> and <code>ValidatingAdmissionPolicyBinding</code>.</li><li>Within only the <code>supply-chain</code> namespace, reject Pod creates and updates unless every regular container image starts with <code>docker.io/library/</code> and contains an image digest (<code>@sha256:</code>).</li><li>The policy must deny invalid workloads with a helpful message. Do not use a webhook, Kyverno, or a cluster-wide binding.</li></ul><p>Retrieve the digest from the running fixture Pod rather than guessing it: inspect <code>.status.containerStatuses[0].imageID</code>. The admission expression must validate all containers, not merely the first one.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-13-fixture.yaml&#10;kubectl --context kind-cks-lab -n supply-chain get pod -l app=frontend -o jsonpath='{.items[0].status.containerStatuses[0].imageID}{"\n"}'&#10;kubectl --context kind-cks-lab apply -f .\lab\lesson-13-answer.yaml&#10;kubectl --context kind-cks-lab -n supply-chain rollout status deployment/frontend --timeout=90s&#10;kubectl --context kind-cks-lab -n supply-chain run mutable-test --image=busybox:1.37 --restart=Never&#10;kubectl --context kind-cks-lab -n default run outside-scope --image=busybox:1.37 --restart=Never</code></pre><p>Expected: the rollout succeeds; <code>mutable-test</code> is rejected by admission; <code>outside-scope</code> is admitted because the binding is limited to <code>supply-chain</code>. Remove <code>outside-scope</code> after testing.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is <code>busybox:1.37</code> weaker supply-chain evidence than <code>docker.io/library/busybox@sha256:…</code>?</p><label><input type="radio" name="q" value="digest">A tag can be repointed, while a digest identifies immutable image content.</label><label><input type="radio" name="q" value="size">A digest always makes an image smaller.</label><label><input type="radio" name="q" value="network">A digest removes the need for registry access.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the Kubernetes <a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/">Validating Admission Policy</a> reference and inspect the schema locally with <code>kubectl explain validatingadmissionpolicy.spec.validations</code>.</p><h2>What to send me</h2><p>Paste your policy, binding, and Deployment update, followed by the two admission outcomes. I’ll evaluate the CEL logic, namespace scope, and image-reference enforcement.</p><footer>Previous: <a href="0012-audit-log-investigation.html">Audit log investigation</a> · CKS domain: Supply Chain Security</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='digest'){f.textContent='Correct. Tags are mutable references; digests identify a specific immutable manifest.';f.className='feedback good';}else{f.textContent='Not quite. The key difference is immutability and traceability, not image size or network access.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1,4 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 14: Audit policy</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0014 · 35 minutes · Monitoring, Logging, and Runtime Security</p><h1>Write an audit policy that captures escalation evidence</h1><p class="lede">Audit logs are only useful when the policy records enough detail for high-risk actions without turning every request into expensive noise. This is a from-scratch configuration task—there is no starter YAML.</p><div class="callout"><strong>Exam reflex:</strong> audit-policy rules are first-match wins. Put the most specific, high-value rules first and end with a deliberate catch-all.</div><h2>Timed task: 25 minutes</h2><p>Create <code>lab/lesson-14-audit-policy.yaml</code>. It must be a valid Kubernetes audit policy that meets every requirement:</p><ul><li>Use <code>audit.k8s.io/v1</code> and kind <code>Policy</code>.</li><li>Never record the <code>RequestReceived</code> stage.</li><li>Record at <code>RequestResponse</code> level for all Secret requests in namespace <code>payments</code>.</li><li>Record at <code>Metadata</code> level for <code>create</code>, <code>update</code>, <code>patch</code>, <code>delete</code>, and <code>deletecollection</code> operations on Roles, RoleBindings, ClusterRoles, and ClusterRoleBindings, regardless of namespace.</li><li>Record every other request at <code>Metadata</code> level.</li></ul><p>Do not use <code>None</code> to hide any requests beyond the omitted stage. Do not place the catch-all rule above the specific rules.</p><h2>Validation</h2><p>An audit policy is an API-server configuration file, not a Kubernetes object. <code>kubectl apply</code> cannot validate or install it. Check its YAML syntax with your editor, then inspect the rule ordering manually. In a real cluster, the API server must be started with <code>--audit-policy-file</code> pointing to this file.</p><pre><code>Get-Content .\lab\lesson-14-audit-policy.yaml
# On a Linux control-plane node, examine the API-server manifest/configuration:
grep -R -- '--audit-policy-file' /etc/kubernetes/manifests /etc/kubernetes 2&gt;/dev/null</code></pre><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>A catch-all <code>Metadata</code> rule is first in the rule list, followed by a Secret <code>RequestResponse</code> rule. What level is recorded for a Secret request?</p><label><input type="radio" name="q" value="metadata">Metadata, because the first matching audit-policy rule wins.</label><label><input type="radio" name="q" value="response">RequestResponse, because it is more specific.</label><label><input type="radio" name="q" value="both">Both levels are recorded.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/">auditing</a> documentation. Confirm the resource-group names for RBAC objects.</p><h2>What to send me</h2><p>Paste the policy YAML. I’ll evaluate ordering, resource matching, verb coverage, and whether it records the necessary response bodies.</p><footer>Previous: <a href="0013-image-digest-admission-policy.html">Image digest admission</a> · CKS domain: Monitoring, Logging, and Runtime Security</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='metadata'){f.textContent='Correct. Audit-policy rules are evaluated in order; the first match determines the result.';f.className='feedback good';}else{f.textContent='Not quite. The first matching rule wins, so ordering is part of the security design.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1,2 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 15: Kubelet endpoint hardening</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0015 · 35 minutes · Cluster Setup and System Hardening</p><h1>Remove anonymous access to the kubelet</h1><p class="lede">The kubelet is a node-level control point. An exposed read-only or anonymously accessible endpoint can reveal workload and node information, so secure its authentication and authorization before investigating higher-level policy.</p><div class="callout"><strong>Exam reflex:</strong> distinguish kubelet authentication from authorization. Disabling anonymous access does not make authorization safe by itself; both controls must be configured.</div><h2>Timed task: 25 minutes</h2><p>Review <code>lab/lesson-15-kubelet-config-insecure.yaml</code>. Create a complete replacement configuration as <code>lab/lesson-15-kubelet-config-answer.yaml</code>.</p><p>Preserve the API version and kind. Your completed configuration must:</p><ul><li>Reject unauthenticated kubelet requests.</li><li>Use webhook authentication and webhook authorization for authenticated API requests.</li><li>Disable the legacy read-only kubelet port.</li><li>Protect kernel defaults.</li><li>Enable kubelet client-certificate rotation.</li></ul><p>Do not add unrelated fields or change API versions. This is a node configuration artifact; do not run <code>kubectl apply</code> against it.</p><h2>Verification</h2><pre><code>Get-Content .\lab\lesson-15-kubelet-config-answer.yaml
</code></pre><p>The completed configuration should leave nothing listening on TCP 10255. In a real cluster, deploy it through the node’s kubelet configuration-management path and restart the kubelet only under the approved maintenance procedure.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is setting <code>authorization.mode: Webhook</code> insufficient if <code>authentication.anonymous.enabled</code> remains true?</p><label><input type="radio" name="q" value="anonymous">Anonymous requests can still reach the kubelet and rely on whatever authorization is granted to the anonymous identity.</label><label><input type="radio" name="q" value="port">Webhook authorization automatically closes port 10255.</label><label><input type="radio" name="q" value="cert">Authorization automatically rotates client certificates.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/">KubeletConfiguration API reference</a> and the kubelet authentication/authorization documentation.</p><h2>What to send me</h2><p>Paste the replacement configuration. I’ll check every required field, then explain how the settings combine to secure the endpoint.</p><footer>Previous: <a href="0014-kubernetes-audit-policy.html">Kubernetes audit policy</a> · CKS domains: Cluster Setup and System Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='anonymous'){f.textContent='Correct. Authentication decides whether and as whom the request enters; authorization then evaluates that identity.';f.className='feedback good';}else{f.textContent='Not quite. Authentication and authorization are separate controls; configure both.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1,3 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 16: TLS Ingress</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0016 · 30 minutes · Cluster Setup</p><h1>Route a service through a TLS-enabled Ingress</h1><p class="lede">Ingress TLS is a three-part relationship: the hostname in the rule, the hostname in the TLS section, and the Secret that holds the certificate. A correct backend alone does not secure the public route.</p><div class="callout"><strong>Exam reflex:</strong> use <code>networking.k8s.io/v1</code>. In every HTTP path, specify <code>pathType</code> and the Service backend name and port explicitly.</div><h2>Timed task: 20 minutes</h2><p>Apply <code>lab/lesson-16-fixture.yaml</code>. Create <code>lab/lesson-16-answer.yaml</code> from scratch with a single Ingress in namespace <code>ingress-drill</code>.</p><p>The Ingress must meet all requirements:</p><ul><li>Name: <code>orders</code>; API version: <code>networking.k8s.io/v1</code>.</li><li>Use ingress class <code>nginx</code>.</li><li>Serve only host <code>orders.training.local</code>.</li><li>Configure TLS for that same host using Secret <code>orders-tls</code>.</li><li>Route <code>/api</code> with <code>Prefix</code> path type to Service <code>orders</code> on port <code>8080</code>.</li><li>Route <code>/healthz</code> with <code>Exact</code> path type to the same Service and port.</li></ul><p>Do not add a wildcard host, default backend, or controller-specific annotations. The supplied cluster has no Ingress controller, so this drill validates the API manifest and routing intent rather than an external HTTPS request.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-16-fixture.yaml
kubectl --context kind-cks-lab apply --dry-run=server -f .\lab\lesson-16-answer.yaml
kubectl --context kind-cks-lab -n ingress-drill get ingress orders -o yaml</code></pre><p>After server-side dry run succeeds, apply the answer manifest for inspection. Do not expect an address or TLS handshake without an Ingress controller.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>What must match for the TLS host in an Ingress to cover the public route?</p><label><input type="radio" name="q" value="host">The hostname in <code>spec.tls.hosts</code> must match the hostname in the Ingress rule.</label><label><input type="radio" name="q" value="path">The TLS Secret name must match the backend Service name.</label><label><input type="radio" name="q" value="port">The TLS host must include the backend Service port.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/">Ingress</a> reference to verify the v1 backend and TLS schema.</p><h2>What to send me</h2><p>Paste the Ingress YAML and the server-side dry-run result. I’ll check host/TLS alignment, path semantics, and backend syntax.</p><footer>Previous: <a href="0015-kubelet-endpoint-hardening.html">Kubelet endpoint hardening</a> · CKS domain: Cluster Setup</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='host'){f.textContent='Correct. The TLS section and rule host must align for the certificate to cover the requested hostname.';f.className='feedback good';}else{f.textContent='Not quite. TLS applies to the requested hostname, so the TLS and rule hosts must match.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 17: Tenant isolation admission</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0017 · 45 minutes · Minimize Microservice Vulnerabilities</p><h1>Block host-escape settings before scheduling</h1><p class="lede">Namespace isolation is weak if a tenant can request host namespaces, host networking, privileged containers, or hostPath volumes. This drill uses native CEL admission controls to make those settings unrepresentable in one tenant namespace.</p><div class="callout"><strong>Exam reflex:</strong> protect optional fields with CEL <code>has(...)</code> checks. A policy must handle an omitted securityContext as safely as an explicit <code>false</code>.</div><h2>Timed task: 35 minutes</h2><p>Apply <code>lab/lesson-17-fixture.yaml</code>. Create <code>lab/lesson-17-answer.yaml</code> from scratch containing a <code>ValidatingAdmissionPolicy</code> and <code>ValidatingAdmissionPolicyBinding</code>.</p><p>Only in namespace <code>tenant-a</code>, deny Pod creates and updates when any of the following is true:</p><ul><li><code>hostNetwork</code>, <code>hostPID</code>, or <code>hostIPC</code> is true.</li><li>Any volume is a <code>hostPath</code> volume.</li><li>Any regular container is privileged.</li></ul><p>The policy must allow a normal unprivileged Pod that omits all of these fields. Use <code>failurePolicy: Fail</code>, a useful denial message, and <code>Deny</code> in the binding. Scope the binding with the supplied namespace label; do not apply it cluster-wide or use a webhook.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-17-fixture.yaml&#10;kubectl --context kind-cks-lab apply -f .\lab\lesson-17-answer.yaml&#10;kubectl --context kind-cks-lab -n tenant-a run normal --image=busybox:1.37 --restart=Never -- sh -c 'sleep 60'&#10;kubectl --context kind-cks-lab -n tenant-a run host-net --image=busybox:1.37 --restart=Never --overrides '{"apiVersion":"v1","spec":{"hostNetwork":true}}' -- sh -c 'sleep 60'&#10;kubectl --context kind-cks-lab -n default run host-net-outside --image=busybox:1.37 --restart=Never --overrides '{"apiVersion":"v1","spec":{"hostNetwork":true}}' -- sh -c 'sleep 60'</code></pre><p>Expected: <code>normal</code> is admitted, <code>host-net</code> is rejected, and <code>host-net-outside</code> is admitted. Clean up the two admitted test Pods afterward. Also inspect your CEL carefully: the commands prove host networking, while the policy must cover all four listed controls.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is a plain expression such as <code>c.securityContext.privileged == false</code> unsafe for this task?</p><label><input type="radio" name="q" value="optional">A container can omit <code>securityContext</code>; the expression must handle optional fields without an evaluation error.</label><label><input type="radio" name="q" value="scope">Privileged is a namespace-level field.</label><label><input type="radio" name="q" value="port">Privileged containers always require a host port.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the Kubernetes <a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/">Validating Admission Policy</a> reference and inspect CEL field availability with <code>kubectl explain pod.spec</code>.</p><h2>What to send me</h2><p>Paste the policy and binding, plus the three admission outcomes. I’ll inspect scope, optional-field handling, and every escape path.</p><footer>Previous: <a href="0016-tls-ingress.html">TLS Ingress</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='optional'){f.textContent='Correct. CEL expressions must explicitly handle fields that are optional in the Kubernetes API.';f.className='feedback good';}else{f.textContent='Not quite. securityContext can be absent, so a robust policy must account for it.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1,9 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 18: Encrypt Secrets at rest</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0018 · 40 minutes · Cluster Hardening</p><h1>Configure encryption before data reaches etcd</h1><p class="lede">Kubernetes Secrets are base64-encoded in API responses, not encrypted by default in etcd. Encryption at rest is controlled by an API-server configuration file and provider order matters.</p><div class="callout"><strong>Exam reflex:</strong> this is an API-server configuration, not a Kubernetes object. Do not use <code>kubectl apply</code> for the encryption configuration. First providers encrypt new writes; <code>identity</code> is a fallback and belongs last.</div><h2>Timed task: 30 minutes</h2><p>Apply <code>lab/lesson-18-fixture.yaml</code> only to create the sample Secret. Then write <code>lab/lesson-18-encryption-config.yaml</code> from scratch.</p><p>Your configuration must:</p><ul><li>Use <code>apiserver.config.k8s.io/v1</code> and kind <code>EncryptionConfiguration</code>.</li><li>Encrypt only the core <code>secrets</code> resource.</li><li>Use <code>aescbc</code> as the first provider, with one key named <code>key1</code>.</li><li>Use a random, valid 32-byte key encoded in base64.</li><li>Include <code>identity</code> only as the final provider.</li></ul><p>Do not place <code>identity</code> first, do not use a plaintext key, and do not add an etcd object or Secret to store this configuration.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-18-fixture.yaml
# Generate a valid 32-byte base64 key in PowerShell:
$bytes = [byte[]]::new(32)
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
[Convert]::ToBase64String($bytes)
# Check your generated key decodes to 32 bytes:
[Convert]::FromBase64String('&lt;your-key&gt;').Length</code></pre><p>In a production control plane, place the completed file securely on every API-server node, configure <code>--encryption-provider-config</code>, restart one API server at a time, then rewrite existing Secrets so they are stored with the new provider.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why must <code>identity</code> be last in the provider list?</p><label><input type="radio" name="q" value="order">The first provider encrypts new writes; placing identity first would keep new data unencrypted.</label><label><input type="radio" name="q" value="speed">Identity is slower than AES-CBC.</label><label><input type="radio" name="q" value="api">Identity is only valid for non-core API resources.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/">Encrypting Confidential Data at Rest</a> guide. Review the operational steps for rotating keys and rewriting already-stored objects.</p><h2>What to send me</h2><p>Paste the configuration only—not the actual production key. I’ll validate provider order, resource scope, and base64 key length in this local training file.</p><footer>Previous: <a href="0017-tenant-isolation-admission.html">Tenant isolation admission</a> · CKS domain: Cluster Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='order'){f.textContent='Correct. Provider order controls how new data is written; identity is a compatibility fallback.';f.className='feedback good';}else{f.textContent='Not quite. Encryption providers are evaluated in order, and the first one writes new data.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 19: Binary verification</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0019 · 25 minutes · Cluster Setup</p><h1>Verify a platform binary before deployment</h1><p class="lede">A checksum does not prove who produced an artifact, but it detects whether the bytes match a checksum obtained through a trusted channel. Treat a mismatch as a release blocker, not a warning to work around.</p><div class="callout"><strong>Exam reflex:</strong> verify the artifact before copying it into a privileged path, making it executable, or restarting a control-plane component. A failed checksum means stop and investigate.</div><h2>Timed task: 15 minutes</h2><p>In <code>lab/lesson-19-artifacts</code>, the trusted-release team supplied <code>SHA256SUMS</code> separately from the two received artifacts. Verify both artifacts using a Linux checksum command. Then create <code>lab/lesson-19-answer.md</code> containing:</p><ol><li>The exact verification command you ran.</li><li>Which artifact is verified and may proceed to the next deployment-review stage.</li><li>Which artifact must not be deployed.</li><li>Two appropriate next actions for the mismatched artifact.</li></ol><p>Do not edit either artifact or the checksum file. The artifacts are small training stand-ins, not real Kubernetes executables.</p><h2>Verification</h2><pre><code>cd lab/lesson-19-artifacts&#10;sha256sum -c SHA256SUMS&#10;&#10;# PowerShell equivalent for a single artifact:&#10;Get-FileHash .\kubelet-v1.35.0-linux-amd64 -Algorithm SHA256</code></pre><p>One result should be <code>OK</code>; one should be <code>FAILED</code>. A command can return non-zero when any verification fails—that non-zero result is expected evidence, not a reason to skip the check.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>What is the correct immediate response to a checksum mismatch for a proposed kubelet binary?</p><label><input type="radio" name="q" value="stop">Do not deploy it; quarantine the artifact and obtain or verify a trusted replacement.</label><label><input type="radio" name="q" value="retry">Install it, then re-download the checksum file later.</label><label><input type="radio" name="q" value="ignore">Ignore the mismatch if the version tag is correct.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Review the official Kubernetes release process and the security guidance for obtaining release artifacts. In production, pair checksums with signed provenance and trusted release channels.</p><h2>What to send me</h2><p>Send the four requested items from your Markdown answer. I’ll check your operational decision, not just the command syntax.</p><footer>Previous: <a href="0018-secrets-encryption-at-rest.html">Secrets encryption at rest</a> · CKS domain: Cluster Setup</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='stop'){f.textContent='Correct. A mismatch is a supply-chain incident signal; fail closed and re-establish artifact trust.';f.className='feedback good';}else{f.textContent='Not quite. Integrity verification must fail closed; do not deploy an artifact whose bytes do not match trusted evidence.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 20: Runtime shell detection</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0020 · 35 minutes · Monitoring, Logging, and Runtime Security</p><h1>Detect an interactive shell in a production container</h1><p class="lede">Runtime detection turns suspicious behavior into an actionable alert. A shell inside a workload is not automatically malicious, but in a defined production scope it deserves timely investigation—especially when it touches mounted credentials.</p><div class="callout"><strong>Incident reflex:</strong> make detection rules precise enough to reduce noise, but include enough event context in the alert that responders can identify the workload without re-running the search.</div><h2>Timed task: 25 minutes</h2><p>Review <code>lab/lesson-20-runtime-event.json</code>. Create <code>lab/lesson-20-falco-rule.yaml</code> from scratch containing one custom Falco rule.</p><p>The rule must:</p><ul><li>Use source <code>syscall</code>.</li><li>Match a process named <code>sh</code> or <code>bash</code> only when it runs in a container in Kubernetes namespace <code>payments</code>.</li><li>Use priority <code>WARNING</code>.</li><li>Include the process command line, Kubernetes namespace, Pod name, and container name in its output.</li><li>Include tags <code>container</code> and <code>mitre_execution</code>.</li></ul><p>Write the condition so it does not match a host-level shell or a shell in another namespace. Do not suppress the alert because the process is running as root; identity is evidence for triage, not a reason to skip detection.</p><h2>Evidence and response</h2><p>In <code>lab/lesson-20-answer.md</code>, give:</p><ol><li>The exact suspicious command from the supplied event.</li><li>The affected namespace, Pod, and container.</li><li>Two immediate containment actions in priority order.</li></ol><p>This environment does not run Falco, so validate the YAML structure in your editor. On a Falco-equipped Linux node, use <code>falco --validate /path/to/rule.yaml</code> before reloading the rules.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Which condition component prevents a rule from alerting on every shell launched directly on a Kubernetes node?</p><label><input type="radio" name="q" value="container">The <code>container</code> condition.</label><label><input type="radio" name="q" value="process">The <code>proc.name</code> condition.</label><label><input type="radio" name="q" value="priority">The <code>priority</code> field.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://falco.org/docs/reference/rules/">Falco rules reference</a> to confirm condition syntax and available output fields.</p><h2>What to send me</h2><p>Paste the rule and your three incident answers. I’ll assess false-positive scope, alert context, and containment quality.</p><footer>Previous: <a href="0019-platform-binary-verification.html">Platform binary verification</a> · CKS domain: Monitoring, Logging, and Runtime Security</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='container'){f.textContent='Correct. container limits the rule to containerized processes instead of host-level activity.';f.className='feedback good';}else{f.textContent='Not quite. proc.name matches host processes too; container scopes the event to a container.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 21: Pod Security Admission modes</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0021 · 40 minutes · Minimize Microservice Vulnerabilities</p><h1>Introduce restricted policy without losing visibility</h1><p class="lede">Pod Security Admission has three independent modes: enforce blocks a workload, audit records violations, and warn informs the requesting user. Mature rollouts often use all three during a migration.</p><div class="callout"><strong>Exam reflex:</strong> these are Namespace labels. Each mode needs its own label key and can use a different target level or version. Enforcement alone is not the whole rollout story.</div><h2>Timed task: 30 minutes</h2><p>Apply <code>lab/lesson-21-fixture.yaml</code>. Create <code>lab/lesson-21-answer.yaml</code> from scratch. Do not change the fixture.</p><p>Deliver a safe migration for namespace <code>partner-workloads</code>:</p><ul><li>Existing Pods must remain running.</li><li>New or updated Pods must be rejected unless they meet the <code>restricted</code> Pod Security Standard.</li><li>Violations must also be visible through audit and user-facing warnings at <code>restricted</code>.</li><li>Pin every mode to the Kubernetes minor version running in this cluster.</li><li>Create a compliant replacement Pod named <code>exporter-v2</code> in the same namespace, using image <code>busybox:1.37</code>, command <code>sh -c 'sleep 3600'</code>, and label <code>app=exporter</code>.</li></ul><p>The replacement must run as non-root UID <code>1000</code>, use RuntimeDefault seccomp, prevent privilege escalation, and drop all capabilities. Do not mount a ServiceAccount token. No starter Pod or namespace-label YAML is provided.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab version --output=yaml&#10;kubectl --context kind-cks-lab apply -f .\lab\lesson-21-fixture.yaml&#10;kubectl --context kind-cks-lab apply -f .\lab\lesson-21-answer.yaml&#10;kubectl --context kind-cks-lab -n partner-workloads get pod legacy-exporter exporter-v2&#10;kubectl --context kind-cks-lab -n partner-workloads run rejected-test --image=busybox:1.37 --restart=Never -- sh -c 'sleep 60'&#10;kubectl --context kind-cks-lab get namespace partner-workloads --show-labels</code></pre><p>Expected: <code>legacy-exporter</code> remains running, <code>exporter-v2</code> is Ready, and <code>rejected-test</code> is denied. The final command must show six PSA labels: enforce, audit, and warn levels plus their three version labels.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Which Pod Security Admission mode blocks a non-compliant new Pod?</p><label><input type="radio" name="q" value="enforce">Enforce.</label><label><input type="radio" name="q" value="audit">Audit.</label><label><input type="radio" name="q" value="warn">Warn.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/concepts/security/pod-security-admission/">Pod Security Admission</a> reference. Confirm the exact label names and version formatting instead of relying on memory.</p><h2>What to send me</h2><p>Paste your namespace and replacement-Pod YAML, plus the rejected admission message. I’ll check all three modes, version pinning, and workload compliance.</p><footer>Previous: <a href="0020-runtime-shell-detection.html">Runtime shell detection</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='enforce'){f.textContent='Correct. Enforce rejects non-compliant workloads; audit records and warn reports them.';f.className='feedback good';}else{f.textContent='Not quite. Only enforce prevents admission; audit and warn create visibility.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1,4 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 22: API server hardening</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0022 · 45 minutes · Cluster Hardening</p><h1>Remove weak API-server access paths</h1><p class="lede">The API server is the cluster’s security boundary. A permissive flag can bypass all the careful RBAC and workload policy built above it, so secure the control-plane configuration first.</p><div class="callout"><strong>Exam reflex:</strong> inspect every API-server flag before changing it. Preserve unrelated required flags, remove or replace weak settings, and make audit logging explicit.</div><h2>Timed task: 35 minutes</h2><p>Review <code>lab/lesson-22-kube-apiserver-insecure.yaml</code>. Create <code>lab/lesson-22-kube-apiserver-answer.yaml</code> as a complete replacement static-Pod manifest.</p><p>Preserve the API version, kind, Pod name, namespace, host networking, container name, image, <code>--etcd-servers</code>, and <code>--service-cluster-ip-range</code>. Harden the API server so it:</p><ul><li>Rejects anonymous API requests.</li><li>Uses <code>Node,RBAC</code> authorization rather than <code>AlwaysAllow</code>.</li><li>Validates ServiceAccount tokens against the API server.</li><li>Disables profiling endpoints.</li><li>Writes audit events to <code>/var/log/kubernetes/audit/audit.log</code> instead of standard output.</li></ul><p>Do not add a new API server, change the image, or use an API object such as a Deployment. This is a static control-plane Pod manifest. It must not be applied to the Kind lab.</p><h2>Verification</h2><pre><code>Get-Content .\lab\lesson-22-kube-apiserver-answer.yaml
# On a real kubeadm control-plane node, inspect the active static Pod:
sudo grep -E -- '--anonymous-auth|--authorization-mode|--service-account-lookup|--profiling|--audit-log-path' /etc/kubernetes/manifests/kube-apiserver.yaml</code></pre><p>Production changes to the API-server manifest require the approved control-plane maintenance process. A kubelet automatically restarts its static Pod when the manifest changes, so do not make experimental edits on a live production control plane.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is <code>--authorization-mode=AlwaysAllow</code> dangerous even if Roles and RoleBindings are carefully designed?</p><label><input type="radio" name="q" value="always">It allows requests without applying RBAC authorization decisions.</label><label><input type="radio" name="q" value="logs">It disables audit logs.</label><label><input type="radio" name="q" value="pods">It makes every Pod privileged.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the Kubernetes <a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/">kube-apiserver command reference</a> to confirm the security-flag behavior.</p><h2>What to send me</h2><p>Paste your complete command list. I’ll review one flag at a time and preserve your request not to reveal the next correction early.</p><footer>Previous: <a href="0021-pod-security-admission-modes.html">Pod Security Admission modes</a> · CKS domain: Cluster Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='always'){f.textContent='Correct. AlwaysAllow bypasses the authorization controls that RBAC is meant to enforce.';f.className='feedback good';}else{f.textContent='Not quite. The authorization mode decides whether RBAC is evaluated for a request.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 23: Cilium WireGuard encryption</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0023 · 35 minutes · Minimize Microservice Vulnerabilities</p><h1>Encrypt Pod-to-Pod traffic with Cilium WireGuard</h1><p class="lede">NetworkPolicy controls who may communicate. Transparent encryption controls whether traffic can be read while it travels between nodes. They solve different security problems and commonly work together.</p><div class="callout"><strong>Exam reflex:</strong> identify the encryption scope before enabling options. Default WireGuard encryption protects traffic between Cilium-managed Pods on different nodes; node-to-node encryption is a separate option and is not required here.</div><h2>Timed task: 25 minutes</h2><p>Read <code>lab/lesson-23-cilium-scenario.md</code>. Create <code>lab/lesson-23-answer.md</code> with these four items:</p><ol><li>One exact <code>helm upgrade</code> command that retains the existing release values while enabling transparent WireGuard encryption for Cilium 1.19.6.</li><li>The required firewall change, including protocol and port.</li><li>Two preflight checks you would run before changing the cluster.</li><li>Two post-change validation commands, and what each confirms.</li></ol><p>Do not enable node-to-node encryption. Do not propose IPsec, a CNI change, or CNI-chaining MTU settings; the scenario does not require them.</p><h2>Expected reasoning</h2><p>For this scenario, WireGuard’s required Helm options enable transparent encryption and select the WireGuard implementation. The nodes must reach one another on WireGuard’s UDP port <code>51871</code>. WireGuard keys are generated per node and distributed through Cilium’s Kubernetes resources; do not create a shared Kubernetes Secret for them.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Does Cilium WireGuard encrypt traffic between two Pods that are scheduled on the same node?</p><label><input type="radio" name="q" value="no">No. Same-node traffic is not encrypted by the inter-node WireGuard tunnel.</label><label><input type="radio" name="q" value="yes">Yes. Every Pod-to-Pod packet always enters a WireGuard tunnel.</label><label><input type="radio" name="q" value="policy">Only if a NetworkPolicy allows it.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use Cilium’s official <a href="https://docs.cilium.io/en/stable/security/network/encryption-wireguard/">WireGuard transparent encryption</a> guide. Confirm the Helm settings, kernel prerequisite, port, scope, and validation procedure.</p><h2>What to send me</h2><p>Paste your four-item rollout note. I’ll review it one issue at a time, without giving the next answer early.</p><footer>Previous: <a href="0022-kube-apiserver-hardening.html">API server hardening</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='no'){f.textContent='Correct. Inter-node encryption does not add protection for traffic that never leaves the node.';f.className='feedback good';}else{f.textContent='Not quite. WireGuard encrypts traffic travelling between nodes; same-node traffic does not use that tunnel.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 24: RBAC remediation</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0024 · 20 minutes · Cluster Hardening</p><h1>Remove an unnecessary Secret-read permission</h1><p class="lede">This is an exam-style remediation: an existing workload identity has more access than it needs. Change only the permission rule, preserve the established identity and binding, and prove the boundary.</p><div class="callout"><strong>Exam reflex:</strong> before creating new RBAC objects, inspect the existing Role and RoleBinding. Often the smallest safe fix is simply to narrow one Role rule.</div><h2>Timed task: 12 minutes</h2><p>Apply <code>lab/lesson-24-fixture.yaml</code>. The <code>release-bot</code> ServiceAccount in namespace <code>release</code> is overprivileged.</p><p>Create <code>lab/lesson-24-answer.yaml</code> containing a replacement for only the existing Role <code>release-bot-reader</code>. Preserve its name and namespace. After your change, the ServiceAccount must:</p><ul><li>Be able to <code>get</code>, <code>list</code>, and <code>watch</code> ConfigMaps in <code>release</code>.</li><li>Have no permission to read Secrets.</li><li>Have no permission to create ConfigMaps.</li></ul><p>Do not create another Role, RoleBinding, ClusterRole, or ServiceAccount. The RoleBinding already connects the correct identity to the correct Role.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-24-fixture.yaml&#10;kubectl --context kind-cks-lab apply -f .\lab\lesson-24-answer.yaml&#10;$sa = 'system:serviceaccount:release:release-bot'&#10;kubectl --context kind-cks-lab auth can-i get configmaps --as=$sa -n release&#10;kubectl --context kind-cks-lab auth can-i list configmaps --as=$sa -n release&#10;kubectl --context kind-cks-lab auth can-i watch configmaps --as=$sa -n release&#10;kubectl --context kind-cks-lab auth can-i get secrets --as=$sa -n release&#10;kubectl --context kind-cks-lab auth can-i create configmaps --as=$sa -n release</code></pre><p>Expected results: <code>yes yes yes no no</code>.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is it better to remove the Secret permission from the existing Role than to create a second, narrower Role and binding?</p><label><input type="radio" name="q" value="additive">RBAC permissions are additive; the original Role would still grant Secret access.</label><label><input type="radio" name="q" value="names">A ServiceAccount can only have one RoleBinding.</label><label><input type="radio" name="q" value="namespace">Roles cannot grant Secret access.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">RBAC reference</a> for rules, bindings, and ServiceAccount subject names.</p><h2>What to send me</h2><p>Paste only the replacement Role and the five <code>can-i</code> results. I’ll review it one issue at a time and will not reveal the next correction early.</p><footer>Previous: <a href="0023-cilium-wireguard-encryption.html">Cilium WireGuard encryption</a> · CKS domain: Cluster Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='additive'){f.textContent='Correct. RBAC allows the union of permissions granted by all applicable bindings.';f.className='feedback good';}else{f.textContent='Not quite. Adding a narrower Role does not subtract the broader permission that already exists.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1,4 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Supplemental lesson 25: Istio strict mTLS</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Supplemental Lesson 0025 · 20 minutes · Istio service-mesh security</p><h1>Require mTLS for one application namespace</h1><p class="lede">Istio sidecars can automatically use mTLS, but permissive acceptance may still allow plaintext requests. <code>PeerAuthentication</code> with <code>STRICT</code> tells receiving workloads to accept only mTLS traffic.</p><div class="callout"><strong>Security reflex:</strong> PeerAuthentication controls what the destination workload accepts. For namespace-wide policy, use no workload selector; a selector narrows the policy to only matching Pods.</div><h2>Timed task: 12 minutes</h2><p>Read <code>lab/lesson-25-fixture.yaml</code>. Create <code>lab/lesson-25-answer.yaml</code> from scratch with one Istio <code>PeerAuthentication</code> resource.</p><p>The policy must:</p><ul><li>Use API version <code>security.istio.io/v1</code>.</li><li>Be named <code>default</code> in namespace <code>payments-mesh</code>.</li><li>Require <code>STRICT</code> mTLS for every workload in that namespace.</li><li>Contain no workload selector and no port exceptions.</li></ul><p>The local Kind cluster does not have Istio installed, so do not apply this manifest there. This is API and policy-scope practice; on an Istio cluster, first apply the supplied fixture so the Namespace receives sidecar injection.</p><h2>Verification on an Istio cluster</h2><pre><code>kubectl apply -f .\lab\lesson-25-fixture.yaml
kubectl apply -f .\lab\lesson-25-answer.yaml
kubectl -n payments-mesh get peerauthentication default -o yaml
istioctl x describe pod -n payments-mesh -l app=payments-api</code></pre><p>The first command creates the injection-enabled namespace and workload. The final command confirms the workload is in the mesh and helps diagnose mTLS policy state.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why should this namespace-wide policy omit <code>spec.selector</code>?</p><label><input type="radio" name="q" value="scope">Without a selector, it applies to every workload in its own namespace.</label><label><input type="radio" name="q" value="tls">Selectors disable automatic certificates.</label><label><input type="radio" name="q" value="cluster">A selector would make the policy cluster-wide.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use Istio’s official <a href="https://istio.io/latest/docs/reference/config/security/peer_authentication/">PeerAuthentication reference</a> and <a href="https://istio.io/latest/docs/tasks/security/authentication/authn-policy/">mTLS authentication task</a>.</p><h2>What to send me</h2><p>Paste the single policy YAML. I’ll check it one issue at a time and won’t reveal the next correction early.</p><footer>Supplemental path: Istio mTLS · Next: workload-scoped mTLS and authorization</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='scope'){f.textContent='Correct. A PeerAuthentication without a selector applies to all workloads in that policy namespace.';f.className='feedback good';}else{f.textContent='Not quite. Omitting the selector is what makes this a namespace-wide destination policy.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Supplemental lesson 26: Istio workload authorization</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Supplemental Lesson 0026 · 25 minutes · Istio service-mesh security</p><h1>Allow one workload identity to call the payments API</h1><p class="lede">Strict mTLS proves the client has a mesh identity. An Istio AuthorizationPolicy decides which authenticated identity may actually call a workload. Together, they turn encrypted traffic into least-privilege service access.</p><div class="callout"><strong>Security reflex:</strong> use a SPIFFE-style principal when access must be tied to one ServiceAccount. A Namespace name alone is broader: every workload in that Namespace could match it.</div><h2>Timed task: 15 minutes</h2><p>Read <code>lab/lesson-26-fixture.yaml</code>. Assume Lesson 25 strict mTLS is already active in <code>payments-mesh</code>. Create <code>lab/lesson-26-answer.yaml</code> from scratch with one Istio <code>AuthorizationPolicy</code>.</p><p>The policy must:</p><ul><li>Be named <code>payments-api-only-frontend</code> in namespace <code>payments-mesh</code>.</li><li>Select only workloads labelled <code>app=payments-api</code>.</li><li>Use action <code>ALLOW</code>.</li><li>Allow only the ServiceAccount <code>frontend</code> in namespace <code>web-mesh</code>.</li><li>Allow only TCP port <code>8080</code>.</li></ul><p>Do not allow an entire Namespace, use a wildcard principal, add an HTTP method/path rule, or create a PeerAuthentication policy. The local Kind cluster does not have Istio, so this is a manifest-reading exercise.</p><h2>Identity shape</h2><pre><code>cluster.local/ns/&lt;namespace&gt;/sa/&lt;serviceaccount&gt;</code></pre><p>Istio obtains that identity from mTLS. This is why strict mTLS is the foundation for this type of workload authorization.</p><h2>Verification on an Istio cluster</h2><pre><code>kubectl apply -f .\lab\lesson-26-fixture.yaml&#10;kubectl apply -f .\lab\lesson-25-answer.yaml&#10;kubectl apply -f .\lab\lesson-26-answer.yaml&#10;kubectl -n payments-mesh get authorizationpolicy payments-api-only-frontend -o yaml&#10;istioctl x describe pod -n payments-mesh -l app=payments-api</code></pre><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is the source principal safer here than allowing all clients from <code>web-mesh</code>?</p><label><input type="radio" name="q" value="identity">It limits access to the exact frontend ServiceAccount instead of every workload in that namespace.</label><label><input type="radio" name="q" value="tls">It disables mTLS for unrelated clients.</label><label><input type="radio" name="q" value="port">It automatically chooses port 8080.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use Istio’s official <a href="https://istio.io/latest/docs/reference/config/security/authorization-policy/">AuthorizationPolicy reference</a> and <a href="https://istio.io/latest/docs/concepts/security/">security concepts</a>.</p><h2>What to send me</h2><p>Paste the single policy YAML. I’ll review it one issue at a time and will not reveal the next correction early.</p><footer>Supplemental path: Istio authorization · Previous: <a href="0025-istio-strict-mtls.html">Strict mTLS</a></footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='identity'){f.textContent='Correct. The ServiceAccount principal is the narrowest workload identity in this scenario.';f.className='feedback good';}else{f.textContent='Not quite. A Namespace is a group of workloads; the principal names one exact authenticated workload identity.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lesson 27: Static manifest analysis</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0027 · 25 minutes · Supply Chain Security</p><h1>Find and prioritize insecure workload configuration</h1><p class="lede">A static scan examines manifests before they reach the cluster. It is useful in CI because it catches risky workload settings early, but its findings still need human prioritization.</p><h2>Timed task: 15 minutes</h2><p>Scan <code>lab/lesson-27-fixture.yaml</code> with Trivy. Docker Desktop is sufficient; run:</p><pre><code>docker run --rm -v ${PWD}:/work aquasec/trivy:0.68.1 config /work/lab/lesson-27-fixture.yaml</code></pre><p>Create <code>lab/lesson-27-answer.md</code>. Do not modify the fixture.</p><p>Your answer must contain:</p><ul><li>the exact scan command you ran;</li><li>three distinct findings from the manifest, in priority order;</li><li>one short sentence for each explaining why it increases risk; and</li><li>one concrete remediation for each finding.</li></ul><div class="callout"><strong>Scope:</strong> report only manifest/configuration findings. Do not claim image-CVE results unless you also scanned an image. A tag such as <code>latest</code> is a deployment reproducibility and supply-chain issue, not proof of a CVE.</div><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why should CI scan both manifests and images?</p><label><input type="radio" name="q" value="both">They reveal different risks: manifests expose configuration risk, while images can expose package vulnerabilities and insecure contents.</label><label><input type="radio" name="q" value="same">They always produce the same findings.</label><label><input type="radio" name="q" value="replace">A manifest scan replaces runtime monitoring.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://trivy.dev/latest/docs/target/config/">Trivy configuration scanning documentation</a>.</p><h2>What to send me</h2><p>Save the Markdown answer, then say <em>done check</em>. I’ll review one finding at a time and will not supply the whole answer unless you request it.</p><footer>CKS path: Supply Chain Security · Previous: <a href="0024-rbac-remediate-overprivileged-role.html">RBAC remediation</a></footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='both'){f.textContent='Correct. Configuration and image contents are separate attack surfaces.';f.className='feedback good';}else{f.textContent='Not quite. Each scan type finds risks the other cannot see.';f.className='feedback bad';}}</script></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lesson 28: Kubelet CIS review</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0028 · 25 minutes · Cluster Setup</p><h1>Review and harden a kubelet configuration</h1><p class="lede">A kubelet runs on every node and exposes a sensitive management API. This exercise focuses on recognizing unsafe kubelet settings and producing a minimal secure configuration.</p><h2>Timed task: 15 minutes</h2><p>Read <code>lab/lesson-28-kubelet-review.yaml</code>. Create <code>lab/lesson-28-answer.yaml</code> from scratch. It must be a complete <code>KubeletConfiguration</code> that corrects every insecure setting present in the fixture.</p><p>Keep the same API version and kind. Do not add unrelated kubelet options.</p><h2>Required security outcome</h2><ul><li>No anonymous kubelet authentication.</li><li>Webhook authentication enabled.</li><li>Webhook authorization selected.</li><li>The legacy read-only port disabled.</li><li>Kernel defaults protected.</li><li>Kubelet client certificates rotate.</li></ul><div class="callout"><strong>Exam reflex:</strong> distinguish a Boolean switch from a mode. The kubelet authorization choice is a value under <code>authorization.mode</code>; disabling the old HTTP endpoint is done by assigning its port the disabling value.</div><h2>Safe verification</h2><pre><code>kubectl apply --dry-run=client -f .\lab\lesson-28-answer.yaml</code></pre><p>Do not apply this directly to a live node without following your distribution’s kubelet configuration and restart procedure.</p><h2>Documentation target</h2><p>Use the Kubernetes <a href="https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/">KubeletConfiguration API reference</a> and the CIS Kubernetes Benchmark for your environment.</p><h2>What to send me</h2><p>Save the YAML, then say <em>check</em>. I’ll review only the first issue at a time.</p></main></body></html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lesson 29: Node metadata egress isolation</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0029 · 25 minutes · Cluster Setup</p><h1>Prevent a compromised frontend from reaching node metadata</h1><p class="lede">Cloud instance-metadata endpoints commonly use a link-local address. A compromised Pod can attempt to reach them unless its egress is tightly allowlisted. An egress default-deny policy with narrow allowances is a dependable defence.</p><h2>Timed task: 15 minutes</h2><p>Read <code>lab/lesson-29-fixture.yaml</code>. Create <code>lab/lesson-29-answer.yaml</code> with one <code>NetworkPolicy</code> in namespace <code>storefront</code>.</p><p>It must select only Pods labelled <code>app=frontend</code> and permit egress only to:</p><ul><li>Pods labelled <code>app=payments-api</code> in namespace <code>payments-mesh</code>, TCP port <code>8080</code>;</li><li>CoreDNS Pods in <code>kube-system</code>, UDP and TCP port <code>53</code>.</li></ul><p>All other frontend egress must be denied, including node metadata and the public internet. Do not use an IP block, a wildcard namespace selector, or a second NetworkPolicy.</p><div class="callout"><strong>Reasoning hint:</strong> once a Pod is selected by an egress policy, traffic is denied unless an egress rule allows it. The payment destination requires both a Namespace selector and a Pod selector. DNS must be separately allowed because Service-name resolution needs it.</div><h2>Verification</h2><pre><code>kubectl apply --dry-run=client -f .\lab\lesson-29-answer.yaml</code></pre><h2>What to send me</h2><p>Save the policy, then say <em>check</em>. I will review one issue at a time.</p></main></body></html>