diff --git a/CKS-SYLLABUS.md b/CKS-SYLLABUS.md
new file mode 100644
index 0000000..f348147
--- /dev/null
+++ b/CKS-SYLLABUS.md
@@ -0,0 +1,62 @@
+# Certified Kubernetes Security Specialist (CKS) syllabus
+
+Official sources: [CNCF curriculum repository](https://github.com/cncf/curriculum) (current published curriculum: Kubernetes v1.34) and the [Linux Foundation CKS exam page](https://training.linuxfoundation.org/certification/certified-kubernetes-security-specialist/) (current exam environment: Kubernetes v1.35).
+
+## 1. Cluster Setup — 15%
+
+- Use NetworkPolicies to restrict cluster-level access.
+- Use the CIS Benchmark to review the security configuration of Kubernetes components: etcd, kubelet, CoreDNS, and kube-apiserver.
+- Set up Ingress with TLS correctly.
+- Protect node metadata and endpoints.
+- Verify platform binaries before deployment.
+
+## 2. Cluster Hardening — 15%
+
+- Use RBAC to minimize exposure.
+- Use ServiceAccounts carefully: disable defaults where appropriate and minimize new-account permissions.
+- Restrict Kubernetes API access.
+- Upgrade Kubernetes to avoid known vulnerabilities.
+
+## 3. System Hardening — 10%
+
+- Minimize the host operating-system footprint and attack surface.
+- Apply least-privilege identity and access management.
+- Minimize external network access.
+- Use kernel hardening tools appropriately, including AppArmor and seccomp.
+
+## 4. Minimize Microservice Vulnerabilities — 20%
+
+- Apply appropriate Pod Security Standards.
+- Manage Kubernetes Secrets securely.
+- Understand and implement isolation techniques, including multi-tenancy and sandboxed containers.
+- Implement Pod-to-Pod encryption using Cilium.
+
+## 5. Supply Chain Security — 20%
+
+- Minimize base-image footprint.
+- Understand the supply chain, including SBOMs, CI/CD, and artifact repositories.
+- Secure the supply chain through permitted registries, signing, and artifact validation.
+- Perform static analysis of user workloads and container images, for example with Kubesec or KubeLinter.
+
+## 6. Monitoring, Logging, and Runtime Security — 20%
+
+- Perform behavioral analytics to detect malicious activity.
+- Detect threats affecting infrastructure, applications, networks, data, users, and workloads.
+- Investigate attack phases and identify malicious actors.
+- Ensure runtime immutability of containers.
+- Use Kubernetes audit logs to monitor access.
+
+## Course coverage tracker
+
+| Domain | Lessons completed or available | Still to drill |
+| --- | --- | --- |
+| Cluster Setup | 2, 8, 15, 16, 22, 28 | CIS, node metadata, binary verification |
+| Cluster Hardening | 3, 7, 9 | API restriction, upgrades |
+| System Hardening | 4, 6, 11 | host footprint, external access; AppArmor needs a compatible kernel |
+| Microservice Vulnerabilities | 5, 6, 7, 8, 10 | multi-tenancy/sandboxes, Cilium encryption |
+| Supply Chain Security | 13, 27 | images, SBOM, signing, registries, image scanning |
+| Monitoring/Logging/Runtime | 12 | behavior analysis, runtime immutability, audit investigation |
+
+## Supplemental: Istio service-mesh security
+
+Istio is not a separately weighted CKS domain, but its mTLS and authorization controls reinforce the CKS objectives around workload isolation and encrypted service traffic. The supplemental path starts with Lesson 25: namespace-wide strict mTLS using `PeerAuthentication`, then will progress to workload-scoped mTLS and `AuthorizationPolicy`.
diff --git a/MISSION.md b/MISSION.md
new file mode 100644
index 0000000..aacdd52
--- /dev/null
+++ b/MISSION.md
@@ -0,0 +1,16 @@
+# Mission: CKS career readiness
+
+## Why
+Earn the Certified Kubernetes Security Specialist (CKS) certification within one month to advance into more technical, security-aware cloud-native work. Build the practical judgment to secure and troubleshoot Kubernetes workloads, not merely memorise exam tasks.
+
+## Success looks like
+- Complete timed, hands-on CKS-style tasks with a clear, repeatable workflow.
+- Explain and apply least privilege, workload hardening, network isolation, and runtime investigation in a realistic cluster.
+- Sit the CKS exam within one month with evidence from two full simulations and a focused error log.
+
+## Constraints
+- One-month preparation window.
+- Already completed CKA, CKAD, and KodeKloud CKS training; current gap is confidence and retrieval under time pressure.
+
+## Out of scope
+- Broad security theory not directly useful for Kubernetes operations or the CKS exam window.
diff --git a/NOTES.md b/NOTES.md
new file mode 100644
index 0000000..8270cc3
--- /dev/null
+++ b/NOTES.md
@@ -0,0 +1,5 @@
+# Teaching notes
+
+- Learner goal: career advancement and deeper technical security skills.
+- Background: CKA, CKAD, and KodeKloud CKS training completed.
+- Priority: build confidence through timed retrieval and hands-on feedback, not rewatching broad course material.
diff --git a/RESOURCES.md b/RESOURCES.md
new file mode 100644
index 0000000..09da359
--- /dev/null
+++ b/RESOURCES.md
@@ -0,0 +1,25 @@
+# CKS Career Readiness Resources
+
+## Knowledge
+
+- [Linux Foundation: CKS certification](https://training.linuxfoundation.org/certification/certified-kubernetes-security-specialist/)
+ Current exam format, eligibility, simulator access, and environment version. Use for: exam logistics and final readiness.
+- [CNCF: CKS curriculum](https://github.com/cncf/curriculum/blob/master/CKS_Curriculum%20v1.34.pdf)
+ The authoritative domain weighting and competency outline. Use for: allocating practice time.
+- [Kubernetes Security documentation](https://kubernetes.io/docs/concepts/security/)
+ Official map of control-plane, workload, policy, and audit controls. Use for: choosing the correct native security mechanism.
+- [Kubernetes RBAC good practices](https://kubernetes.io/docs/concepts/security/rbac-good-practices/)
+ Least-privilege patterns and escalation risks. Use for: access-control tasks and real-world review.
+- [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/)
+ Official Baseline and Restricted requirements. Use for: workload hardening and Pod Security Admission.
+- [Kubernetes auditing](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/)
+ Audit-policy concepts and event investigation. Use for: monitoring and incident-response tasks.
+
+## Wisdom (Communities)
+
+- [Kubernetes Slack: #certifications](https://kubernetes.slack.com/)
+ Community discussion for exam logistics and study accountability; do not seek or share exam content.
+
+## Gaps
+
+- Establish an evidence-based baseline from the user's first timed practical task before prescribing exact daily drills.
diff --git a/answers/lesson-3.partial-empty-copy.yaml b/answers/lesson-3.partial-empty-copy.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/answers/lesson-3.yaml b/answers/lesson-3.yaml
new file mode 100644
index 0000000..28915e6
--- /dev/null
+++ b/answers/lesson-3.yaml
@@ -0,0 +1,6 @@
+#Timed Task
+# command
+kubeclt create role reporter-role --namespace=cks-rbac --verbs="get,list" --resources="configmaps" --dry-run=client -o yaml
+
+kubectl create rolebinding reporter-rolebinding --namespace=cks-rbac --serviceaccount=cks-rbac:report-reader --role=reporter-role --dry-run=client -o yaml
+
diff --git a/answers/network-policy.yaml b/answers/network-policy.yaml
new file mode 100644
index 0000000..f304357
--- /dev/null
+++ b/answers/network-policy.yaml
@@ -0,0 +1,29 @@
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: app-control
+ namespace: database
+spec:
+ policyTypes:
+ - Ingress
+ - Egress
+ podSelector:
+ matchLabels:
+ app: db
+ ingress:
+ - from:
+ - nameSpaceSelector:
+ matchLabels:
+ kubernetes.io/metadata: payments
+ podSelector:
+ matchLabels:
+ app: payments
+ ports:
+ - port: 5432
+ protocol: TCP
+ egress:
+ - ports:
+ - protocol: TCP
+ port: 53
+ - protocol: UDP
+ port: 53
\ No newline at end of file
diff --git a/assets/course.css b/assets/course.css
new file mode 100644
index 0000000..12ba0fd
--- /dev/null
+++ b/assets/course.css
@@ -0,0 +1,9 @@
+:root { --ink:#132238; --muted:#526174; --line:#d9e1ea; --paper:#fff; --soft:#f3f7fb; --blue:#0759ad; --green:#0b6e4f; }
+* { box-sizing:border-box; }
+body { margin:0; color:var(--ink); background:var(--soft); font:17px/1.55 Georgia, serif; }
+main { max-width:850px; margin:32px auto; padding:42px 52px; background:var(--paper); box-shadow:0 3px 24px #13223814; }
+h1,h2,h3,button { font-family:Inter,Arial,sans-serif; } h1 { font-size:2.15rem; line-height:1.15; margin:.1rem 0 .45rem; } h2 { margin-top:2rem; font-size:1.3rem; border-bottom:1px solid var(--line); padding-bottom:.35rem; } h3 { font-size:1rem; margin-bottom:.25rem; }
+.eyebrow { color:var(--blue); text-transform:uppercase; letter-spacing:.09em; font:700 .78rem Inter,Arial,sans-serif; } .lede { font-size:1.15rem; color:var(--muted); }
+.callout { background:#edf6ff; border-left:4px solid var(--blue); padding:14px 18px; margin:1.2rem 0; } .plan { padding-left:1.2rem; } .plan li { margin:.45rem 0; } code { background:#f0f3f6; padding:.1em .3em; font: .9em Consolas,monospace; }
+.quiz { border:1px solid var(--line); padding:20px; margin:1.2rem 0; } label { display:block; padding:.38rem 0; cursor:pointer; } button { background:var(--blue); color:#fff; border:0; border-radius:4px; padding:.62rem .9rem; font-weight:700; cursor:pointer; } .feedback { min-height:1.4em; font-weight:700; margin-top:.8rem; } .good { color:var(--green); } .bad { color:#a32424; } footer { color:var(--muted); font-size:.88rem; border-top:1px solid var(--line); margin-top:2.5rem; padding-top:1rem; }
+@media (max-width:650px) { main { margin:0; padding:28px 22px; } }
diff --git a/lab/README.md b/lab/README.md
new file mode 100644
index 0000000..b534df0
--- /dev/null
+++ b/lab/README.md
@@ -0,0 +1,37 @@
+# Isolated CKS NetworkPolicy lab
+
+This lab runs in the local Kind context `kind-cks-lab`, separate from the pre-existing `monitoring-context` cluster.
+
+## Scenario
+
+`api` and `intruder` are clients. `db` serves HTTP on port 8080. The policy allows only pods labelled `app=api` to reach the database.
+
+## Repeat the proof
+
+```powershell
+kubectl --context kind-cks-lab -n cks-lab exec api -- wget -qO- -T 5 http://db:8080
+kubectl --context kind-cks-lab -n cks-lab exec intruder -- wget -qO- -T 5 http://db:8080
+```
+
+The first command must print `database`. The second must time out, which proves the denial.
+
+## Inspect the answer
+
+```powershell
+kubectl --context kind-cks-lab -n cks-lab get networkpolicy db-only-from-api -o yaml
+kubectl --context kind-cks-lab -n cks-lab get pods --show-labels
+```
+
+The policy is in `networkpolicy-lab.yaml`. Edit it for new drills, then run:
+
+```powershell
+kubectl --context kind-cks-lab apply -f .\lab\networkpolicy-lab.yaml
+```
+
+## Cleanup
+
+This is destructive and removes only the isolated Kind cluster:
+
+```powershell
+.\lab\bin\kind.exe delete cluster --name cks-lab
+```
diff --git a/lab/bin/kind.exe b/lab/bin/kind.exe
new file mode 100644
index 0000000..57e4216
Binary files /dev/null and b/lab/bin/kind.exe differ
diff --git a/lab/kind-calico.yaml b/lab/kind-calico.yaml
new file mode 100644
index 0000000..2b2a4ca
--- /dev/null
+++ b/lab/kind-calico.yaml
@@ -0,0 +1,9 @@
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+name: cks-lab
+nodes:
+ - role: control-plane
+ - role: worker
+networking:
+ disableDefaultCNI: true
+ podSubnet: 192.168.0.0/16
diff --git a/lab/lesson-10-answer.yaml b/lab/lesson-10-answer.yaml
new file mode 100644
index 0000000..d6f6ecc
--- /dev/null
+++ b/lab/lesson-10-answer.yaml
@@ -0,0 +1,57 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: billing
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: billing-credentials
+ namespace: billing
+type: Opaque
+stringData:
+ api-key: test-key
+ legacy-password: do-not-mount
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: reconciler
+ namespace: billing
+spec:
+ automountServiceAccountToken: false
+ securityContext:
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumes:
+ - name: api-key
+ secret:
+ secretName: billing-credentials
+ items:
+ - key: api-key
+ path: api-key
+ - name: temp
+ emptyDir: {}
+ containers:
+ - name: billing-container
+ image: busybox:1.37
+ command: ["sh","-c","sleep 3600"]
+ securityContext:
+ readOnlyRootFilesystem: true
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop: ["ALL"]
+ volumeMounts:
+ - name: api-key
+ readOnly: true
+ mountPath: /var/run/credentials
+ - name: temp
+ mountPath: /tmp
+ resources:
+ requests:
+ cpu: "100m"
+ memory: 128Mi
+ limits:
+ cpu: "100m"
+ memory: 128Mi
\ No newline at end of file
diff --git a/lab/lesson-10-fixture.yaml b/lab/lesson-10-fixture.yaml
new file mode 100644
index 0000000..b62eefe
--- /dev/null
+++ b/lab/lesson-10-fixture.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: billing
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: billing-credentials
+ namespace: billing
+type: Opaque
+stringData:
+ api-key: test-key
+ legacy-password: do-not-mount
diff --git a/lab/lesson-11-answer.yaml b/lab/lesson-11-answer.yaml
new file mode 100644
index 0000000..32f3769
--- /dev/null
+++ b/lab/lesson-11-answer.yaml
@@ -0,0 +1,42 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: runtime-hardening
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: event-forwarder
+ namespace: runtime-hardening
+
+spec:
+ volumes:
+ - name: tmp
+ emptyDir: {}
+ securityContext:
+ runAsUser: 1000
+ runAsNonRoot: true
+ seccompProfile:
+ type: RuntimeDefault
+ appArmorProfile:
+ type: RuntimeDefault
+ containers:
+ - name: forwarder
+ image: busybox:1.37
+ command: ["sh","-c","sleep 3600"]
+ volumeMounts:
+ - name: tmp
+ mountPath: /tmp
+ securityContext:
+ readOnlyRootFilesystem: true
+ - name: metrics
+ image: busybox:1.37
+ command: ["sh","-c","sleep 3600"]
+ volumeMounts:
+ - name: tmp
+ mountPath: /tmp
+ securityContext:
+ readOnlyRootFilesystem: true
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop: ["ALL"]
diff --git a/lab/lesson-11-fixture.yaml b/lab/lesson-11-fixture.yaml
new file mode 100644
index 0000000..5fc4b60
--- /dev/null
+++ b/lab/lesson-11-fixture.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: runtime-hardening
diff --git a/lab/lesson-12-audit-ans.jsonl b/lab/lesson-12-audit-ans.jsonl
new file mode 100644
index 0000000..51a7ea2
--- /dev/null
+++ b/lab/lesson-12-audit-ans.jsonl
@@ -0,0 +1,40 @@
+{"kind":"Event","apiVersion":"audit.k8s.io/v1",
+"level":"RequestResponse",
+"auditID":"a4",
+"stage":"ResponseComplete",
+"requestReceivedTimestamp":"2026-08-04T03:16:22Z",
+"verb":"create",
+"user":{"username":"system:serviceaccount:payments:api",
+"groups":["system:serviceaccounts"]},
+"sourceIPs":["10.244.3.77"],
+"objectRef":{"apiGroup":"rbac.authorization.k8s.io",
+"resource":"clusterrolebindings","name":"payments-api-admin"},
+"responseStatus":{"code":201}}
+
+{"kind":"Event",
+"apiVersion":"audit.k8s.io/v1",
+"level":"RequestResponse",
+"auditID":"a5",
+"stage":"ResponseComplete",
+"requestReceivedTimestamp":"2026-08-04T03:16:40Z",
+"verb":"get","user":{"username":"system:serviceaccount:payments:api",
+"groups":["system:serviceaccounts"]},
+"sourceIPs":["10.244.3.77"],
+"objectRef":{"resource":"secrets",
+"namespace":"payments",
+"name":"prod-db"},"responseStatus":{"code":200}}
+
+Which principal is suspicious?
+a4 and a5 a6
+What source IP is associated with the activity?
+10.244.3.77
+What is the earliest suspicious timestamp?
+"2026-08-04T03:16:22Z"
+Which ClusterRoleBinding was created?
+payments-api-admin
+Which Secret was accessed?
+"prod-db"
+What runtime action followed the Secret access?
+exec
+Give two immediate containment actions, in order.
+get secret, exec in pod
\ No newline at end of file
diff --git a/lab/lesson-12-audit.jsonl b/lab/lesson-12-audit.jsonl
new file mode 100644
index 0000000..cb4fbab
--- /dev/null
+++ b/lab/lesson-12-audit.jsonl
@@ -0,0 +1,7 @@
+{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"a1","stage":"ResponseComplete","requestReceivedTimestamp":"2026-08-04T03:10:12Z","verb":"get","user":{"username":"system:serviceaccount:observability:metrics-reader","groups":["system:serviceaccounts"]},"sourceIPs":["10.244.1.18"],"objectRef":{"resource":"pods","namespace":"payments","name":"api-7f9d"},"responseStatus":{"code":200}}
+{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"a2","stage":"ResponseComplete","requestReceivedTimestamp":"2026-08-04T03:12:46Z","verb":"create","user":{"username":"system:serviceaccount:payments:deployment-controller","groups":["system:serviceaccounts"]},"sourceIPs":["10.244.2.11"],"objectRef":{"resource":"pods","namespace":"payments","name":"api-7f9d-2"},"responseStatus":{"code":201}}
+{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"RequestResponse","auditID":"a3","stage":"ResponseComplete","requestReceivedTimestamp":"2026-08-04T03:14:03Z","verb":"get","user":{"username":"system:serviceaccount:observability:metrics-reader","groups":["system:serviceaccounts"]},"sourceIPs":["10.244.1.18"],"objectRef":{"resource":"services","namespace":"payments","name":"api"},"responseStatus":{"code":200}}
+{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"RequestResponse","auditID":"a4","stage":"ResponseComplete","requestReceivedTimestamp":"2026-08-04T03:16:22Z","verb":"create","user":{"username":"system:serviceaccount:payments:api","groups":["system:serviceaccounts"]},"sourceIPs":["10.244.3.77"],"objectRef":{"apiGroup":"rbac.authorization.k8s.io","resource":"clusterrolebindings","name":"payments-api-admin"},"responseStatus":{"code":201}}
+{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"RequestResponse","auditID":"a5","stage":"ResponseComplete","requestReceivedTimestamp":"2026-08-04T03:16:40Z","verb":"get","user":{"username":"system:serviceaccount:payments:api","groups":["system:serviceaccounts"]},"sourceIPs":["10.244.3.77"],"objectRef":{"resource":"secrets","namespace":"payments","name":"prod-db"},"responseStatus":{"code":200}}
+{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"a6","stage":"ResponseComplete","requestReceivedTimestamp":"2026-08-04T03:17:10Z","verb":"create","user":{"username":"system:serviceaccount:payments:api","groups":["system:serviceaccounts"]},"sourceIPs":["10.244.3.77"],"objectRef":{"subresource":"exec","resource":"pods","namespace":"payments","name":"api-7f9d"},"responseStatus":{"code":101}}
+{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"a7","stage":"ResponseComplete","requestReceivedTimestamp":"2026-08-04T03:19:20Z","verb":"get","user":{"username":"system:serviceaccount:observability:metrics-reader","groups":["system:serviceaccounts"]},"sourceIPs":["10.244.1.18"],"objectRef":{"resource":"nodes","name":"cks-lab-worker"},"responseStatus":{"code":200}}
diff --git a/lab/lesson-13-answer.yaml b/lab/lesson-13-answer.yaml
new file mode 100644
index 0000000..95292d6
--- /dev/null
+++ b/lab/lesson-13-answer.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: supply-chain
+ labels:
+ training.cks/lesson: supply-chain
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: frontend
+ namespace: supply-chain
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: frontend
+ template:
+ metadata:
+ labels:
+ app: frontend
+ spec:
+ containers:
+ - name: frontend
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
diff --git a/lab/lesson-13-fixture.yaml b/lab/lesson-13-fixture.yaml
new file mode 100644
index 0000000..95292d6
--- /dev/null
+++ b/lab/lesson-13-fixture.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: supply-chain
+ labels:
+ training.cks/lesson: supply-chain
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: frontend
+ namespace: supply-chain
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: frontend
+ template:
+ metadata:
+ labels:
+ app: frontend
+ spec:
+ containers:
+ - name: frontend
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
diff --git a/lab/lesson-13-solution.yaml b/lab/lesson-13-solution.yaml
new file mode 100644
index 0000000..124cc13
--- /dev/null
+++ b/lab/lesson-13-solution.yaml
@@ -0,0 +1,47 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: frontend
+ namespace: supply-chain
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: frontend
+ template:
+ metadata:
+ labels:
+ app: frontend
+ spec:
+ containers:
+ - name: frontend
+ image: docker.io/library/busybox@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028
+ command: ["sh", "-c", "sleep 3600"]
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingAdmissionPolicy
+metadata:
+ name: supply-chain-digest-only
+spec:
+ failurePolicy: Fail
+ matchConstraints:
+ resourceRules:
+ - apiGroups: [""]
+ apiVersions: ["v1"]
+ operations: ["CREATE", "UPDATE"]
+ resources: ["pods"]
+ validations:
+ - expression: "object.spec.containers.all(c, c.image.startsWith('docker.io/library/') && c.image.contains('@sha256:'))"
+ message: "all container images must use docker.io/library and an immutable sha256 digest"
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingAdmissionPolicyBinding
+metadata:
+ name: supply-chain-digest-only
+spec:
+ policyName: supply-chain-digest-only
+ validationActions: [Deny]
+ matchResources:
+ namespaceSelector:
+ matchLabels:
+ training.cks/lesson: supply-chain
diff --git a/lab/lesson-14-audit-policy.yaml b/lab/lesson-14-audit-policy.yaml
new file mode 100644
index 0000000..b7f930d
--- /dev/null
+++ b/lab/lesson-14-audit-policy.yaml
@@ -0,0 +1,27 @@
+apiVersion: audit.k8s.io/v1 # This is required.
+kind: Policy
+# Don't generate audit events for all requests in RequestReceived stage.
+omitStages:
+ - "RequestReceived"
+rules:
+ # Log pod changes at RequestResponse level
+ - level: RequestResponse
+ resources:
+ - group: ""
+ # Resource "pods" doesn't match requests to any subresource of pods,
+ # which is consistent with the RBAC policy.
+ resources: ["secrets"]
+ namespaces: ["payments"]
+
+ - level: Metadata
+ verbs:
+ - create
+ - update
+ - patch
+ - delete
+ - deletecollection
+ resources:
+ - group: "rbac.authorization.k8s.io" # core API group
+ resources: ["roles", "rolebindings","clusterroles","clusterrolebindings"]
+
+ - level: Metadata
diff --git a/lab/lesson-15-kubelet-config-answer.yaml b/lab/lesson-15-kubelet-config-answer.yaml
new file mode 100644
index 0000000..1c817ee
--- /dev/null
+++ b/lab/lesson-15-kubelet-config-answer.yaml
@@ -0,0 +1,13 @@
+apiVersion: kubelet.config.k8s.io/v1beta1
+kind: KubeletConfiguration
+authentication:
+ anonymous:
+ enabled: false
+ webhook:
+ enabled: true
+authorization:
+ mode: Webhook
+readOnlyPort: 0
+protectKernelDefaults: true
+rotateCertificates: true
+
diff --git a/lab/lesson-15-kubelet-config-insecure copy.yaml b/lab/lesson-15-kubelet-config-insecure copy.yaml
new file mode 100644
index 0000000..904b98e
--- /dev/null
+++ b/lab/lesson-15-kubelet-config-insecure copy.yaml
@@ -0,0 +1,12 @@
+apiVersion: kubelet.config.k8s.io/v1beta1
+kind: KubeletConfiguration
+authentication:
+ anonymous:
+ enabled: true
+ webhook:
+ enabled: false
+authorization:
+ mode: AlwaysAllow
+readOnlyPort: 10255
+protectKernelDefaults: false
+rotateCertificates: false
diff --git a/lab/lesson-16-answer.yaml b/lab/lesson-16-answer.yaml
new file mode 100644
index 0000000..bf529ab
--- /dev/null
+++ b/lab/lesson-16-answer.yaml
@@ -0,0 +1,60 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: ingress-drill
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: orders-tls
+ namespace: ingress-drill
+type: kubernetes.io/tls
+stringData:
+ tls.crt: placeholder-certificate-for-api-validation
+ tls.key: placeholder-key-for-api-validation
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: orders
+ namespace: ingress-drill
+spec:
+ selector:
+ app: orders
+ ports:
+ - name: http
+ port: 8080
+ targetPort: 8080
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: orders
+ namespace: ingress-drill
+spec:
+ ingressClassName: nginx
+ tls:
+ - hosts:
+ - orders.training.local
+ secretName: orders-tls
+ rules:
+ - host: orders.training.local
+ http:
+ paths:
+ - path: /api
+ pathType: Prefix
+ backend:
+ service:
+ name: orders
+ port:
+ number: 8080
+ - host: orders.training.local
+ http:
+ paths:
+ - path: /healthz
+ pathType: Exact
+ backend:
+ service:
+ name: orders
+ port:
+ number: 8080
diff --git a/lab/lesson-16-fixture.yaml b/lab/lesson-16-fixture.yaml
new file mode 100644
index 0000000..d1c2614
--- /dev/null
+++ b/lab/lesson-16-fixture.yaml
@@ -0,0 +1,27 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: ingress-drill
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: orders-tls
+ namespace: ingress-drill
+type: kubernetes.io/tls
+stringData:
+ tls.crt: placeholder-certificate-for-api-validation
+ tls.key: placeholder-key-for-api-validation
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: orders
+ namespace: ingress-drill
+spec:
+ selector:
+ app: orders
+ ports:
+ - name: http
+ port: 8080
+ targetPort: 8080
diff --git a/lab/lesson-17-answer.yaml b/lab/lesson-17-answer.yaml
new file mode 100644
index 0000000..7da21c9
--- /dev/null
+++ b/lab/lesson-17-answer.yaml
@@ -0,0 +1,39 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: tenant-a
+ labels:
+ training.cks/lesson: tenant-a
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingAdmissionPolicy
+metadata:
+ name: "validate-namespace-tenant-a"
+spec:
+ failurePolicy: Fail
+ matchConstraints:
+ resourceRules:
+ - apiGroups: [""]
+ apiVersions: ["v1"]
+ operations: ["CREATE", "UPDATE"]
+ resources: ["pods"]
+ validations:
+ - expression: >-
+ (!has(object.spec.hostNetwork) || object.spec.hostNetwork==false) &&
+ (!has(object.spec.hostPID) || object.spec.hostPID==false) &&
+ (!has(object.spec.hostIPC) || object.spec.hostIPC==false) &&
+ (!has(object.spec.volumes) || object.spec.volumes.all(v, !has(v.hostPath))) &&
+ object.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.privileged) || c.securityContext.privileged==false)
+ message: "host namespaces, hostPath volumes, and privileged containers are not allowed"
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingAdmissionPolicyBinding
+metadata:
+ name: validate-namespace-tenant-a
+spec:
+ policyName: validate-namespace-tenant-a
+ validationActions: [Deny]
+ matchResources:
+ namespaceSelector:
+ matchLabels:
+ training.cks/lesson: tenant-a
\ No newline at end of file
diff --git a/lab/lesson-17-fixture.yaml b/lab/lesson-17-fixture.yaml
new file mode 100644
index 0000000..c82bda9
--- /dev/null
+++ b/lab/lesson-17-fixture.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: tenant-a
+ labels:
+ training.cks/lesson: tenant-a
diff --git a/lab/lesson-17-solution.yaml b/lab/lesson-17-solution.yaml
new file mode 100644
index 0000000..a2659ee
--- /dev/null
+++ b/lab/lesson-17-solution.yaml
@@ -0,0 +1,32 @@
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingAdmissionPolicy
+metadata:
+ name: validate-namespace-tenant-a
+spec:
+ failurePolicy: Fail
+ matchConstraints:
+ resourceRules:
+ - apiGroups: [""]
+ apiVersions: ["v1"]
+ operations: ["CREATE", "UPDATE"]
+ resources: ["pods"]
+ validations:
+ - expression: >-
+ (!has(object.spec.hostNetwork) || object.spec.hostNetwork == false) &&
+ (!has(object.spec.hostPID) || object.spec.hostPID == false) &&
+ (!has(object.spec.hostIPC) || object.spec.hostIPC == false) &&
+ (!has(object.spec.volumes) || object.spec.volumes.all(v, !has(v.hostPath))) &&
+ object.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.privileged) || c.securityContext.privileged == false)
+ message: host namespaces, hostPath volumes, and privileged containers are not allowed
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingAdmissionPolicyBinding
+metadata:
+ name: validate-namespace-tenant-a
+spec:
+ policyName: validate-namespace-tenant-a
+ validationActions: [Deny]
+ matchResources:
+ namespaceSelector:
+ matchLabels:
+ training.cks/lesson: tenant-a
diff --git a/lab/lesson-18-answer.yaml b/lab/lesson-18-answer.yaml
new file mode 100644
index 0000000..1026a4c
--- /dev/null
+++ b/lab/lesson-18-answer.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: data-protection
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: payment-token
+ namespace: data-protection
+type: Opaque
+stringData:
+ token: replace-me-after-encryption-is-enabled
+---
diff --git a/lab/lesson-18-encryption-config.yaml b/lab/lesson-18-encryption-config.yaml
new file mode 100644
index 0000000..6e65307
--- /dev/null
+++ b/lab/lesson-18-encryption-config.yaml
@@ -0,0 +1,11 @@
+apiVersion: apiserver.config.k8s.io/v1
+kind: EncryptionConfiguration
+resources:
+ - resources:
+ - secrets
+ providers:
+ - aescbc:
+ keys:
+ - name: key1
+ secret: H5avyAWUn8F0kWtQURZoHGBE1BaBcwKZgtmKu5SAUi4=
+ - identity: {} # plain text, in other words NO encryption
diff --git a/lab/lesson-18-fixture.yaml b/lab/lesson-18-fixture.yaml
new file mode 100644
index 0000000..2b3a150
--- /dev/null
+++ b/lab/lesson-18-fixture.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: data-protection
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: payment-token
+ namespace: data-protection
+type: Opaque
+stringData:
+ token: replace-me-after-encryption-is-enabled
\ No newline at end of file
diff --git a/lab/lesson-19-answer.md b/lab/lesson-19-answer.md
new file mode 100644
index 0000000..6450c78
--- /dev/null
+++ b/lab/lesson-19-answer.md
@@ -0,0 +1,13 @@
+$ sha256sum.exe lab/lesson-19-artifacts/kubectl-v1.35.0-linux-amd64
+9bb6389a5f72cdb071541c40c8df41028a4dae8987f0af003d17e326ceb635d7 *lab/lesson-19-artifacts/kubectl-v1.35.0-linux-amd64
+
+$ sha256sum.exe lab/lesson-19-artifacts/kubelet-v1.35.0-linux-amd64
+e67f13c4f37958bd8fd975cb870b8d245db5655b26621f878af353e8d79ab0e7 *lab/lesson-19-artifacts/kubelet-v1.35.0-linux-amd64
+
+Which artifact is verified and may proceed to the next deployment-review stage.
+answer:
+kubelet-v1.35.0-linux-amd64
+
+
+Which artifact must not be deployed.
+kubectl-v1.35.0-linux-amd64
\ No newline at end of file
diff --git a/lab/lesson-19-artifacts/SHA256SUMS b/lab/lesson-19-artifacts/SHA256SUMS
new file mode 100644
index 0000000..b408fe4
--- /dev/null
+++ b/lab/lesson-19-artifacts/SHA256SUMS
@@ -0,0 +1,2 @@
+e67f13c4f37958bd8fd975cb870b8d245db5655b26621f878af353e8d79ab0e7 kubelet-v1.35.0-linux-amd64
+6d98653149dde123a1972890a6da297449f40606d70d38502aac8985e9b67eec kubectl-v1.35.0-linux-amd64
diff --git a/lab/lesson-19-artifacts/kubectl-v1.35.0-linux-amd64 b/lab/lesson-19-artifacts/kubectl-v1.35.0-linux-amd64
new file mode 100644
index 0000000..5412f96
--- /dev/null
+++ b/lab/lesson-19-artifacts/kubectl-v1.35.0-linux-amd64
@@ -0,0 +1 @@
+received-training-kubectl-artifact-v1.35.0-tampered
diff --git a/lab/lesson-19-artifacts/kubelet-v1.35.0-linux-amd64 b/lab/lesson-19-artifacts/kubelet-v1.35.0-linux-amd64
new file mode 100644
index 0000000..fbaa7e8
--- /dev/null
+++ b/lab/lesson-19-artifacts/kubelet-v1.35.0-linux-amd64
@@ -0,0 +1 @@
+trusted-training-kubelet-artifact-v1.35.0
diff --git a/lab/lesson-20-falco-rule.yaml b/lab/lesson-20-falco-rule.yaml
new file mode 100644
index 0000000..a8a2d30
--- /dev/null
+++ b/lab/lesson-20-falco-rule.yaml
@@ -0,0 +1,15 @@
+- rule: custom_rule
+ desc: custom_rule
+ source: syscall
+ condition: >
+ (evt.type in (execve, execveat)) and
+ container.id != host and
+ k8s.ns.name = "payments" and
+ (proc.name = bash or
+ proc.name = sh)
+ output: >
+ %k8s.pod.name,%k8s.ns.name,%proc.cmdline,%container.name
+ priority: WARNING
+ tags: [container,mitre_execution]
+
+
diff --git a/lab/lesson-20-runtime-event.json b/lab/lesson-20-runtime-event.json
new file mode 100644
index 0000000..1c5dbab
--- /dev/null
+++ b/lab/lesson-20-runtime-event.json
@@ -0,0 +1,4 @@
+{"evt.time":"2026-08-10T09:25:12.441Z","container":true,"proc.name":"sh",
+"proc.cmdline":"sh -c cat /var/run/secrets/kubernetes.io/serviceaccount/token",
+"user.name":"root","k8s.ns.name":"payments","k8s.pod.name":"api-7f9d","container.name":"api"}
+
\ No newline at end of file
diff --git a/lab/lesson-21-answer.yaml b/lab/lesson-21-answer.yaml
new file mode 100644
index 0000000..39a873b
--- /dev/null
+++ b/lab/lesson-21-answer.yaml
@@ -0,0 +1,34 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: partner-workloads
+ labels:
+ pod-security.kubernetes.io/enforce: restricted
+ pod-security.kubernetes.io/enforce-version: v1.35
+ pod-security.kubernetes.io/audit: restricted
+ pod-security.kubernetes.io/audit-version: v1.35
+ pod-security.kubernetes.io/warn: restricted
+ pod-security.kubernetes.io/warn-version: v1.35
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: exporter-v2
+ namespace: partner-workloads
+ labels:
+ app: exporter
+spec:
+ automountServiceAccountToken: false
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ containers:
+ - name: exporter
+ image: busybox:1.37
+ command: ["sh","-c","sleep 3600"]
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop: ["ALL"]
\ No newline at end of file
diff --git a/lab/lesson-21-fixture.yaml b/lab/lesson-21-fixture.yaml
new file mode 100644
index 0000000..fa38f26
--- /dev/null
+++ b/lab/lesson-21-fixture.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: partner-workloads
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: legacy-exporter
+ namespace: partner-workloads
+ labels:
+ app: legacy-exporter
+spec:
+ containers:
+ - name: exporter
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
diff --git a/lab/lesson-22-kube-apiserver-answer.yaml b/lab/lesson-22-kube-apiserver-answer.yaml
new file mode 100644
index 0000000..9177ea6
--- /dev/null
+++ b/lab/lesson-22-kube-apiserver-answer.yaml
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: kube-apiserver
+ namespace: kube-system
+spec:
+ hostNetwork: true
+ containers:
+ - name: kube-apiserver
+ image: registry.k8s.io/kube-apiserver:v1.35.0
+ command:
+ - kube-apiserver
+ - --etcd-servers=https://127.0.0.1:2379
+ - --service-cluster-ip-range=10.96.0.0/12
+ - --anonymous-auth=false
+ - --authorization-mode=Node,RBAC
+ - --profiling=false
+ - --audit-log-path=/var/log/kubernetes/audit/audit.log
+ - --service-account-lookup=true
diff --git a/lab/lesson-22-kube-apiserver-insecure.yaml b/lab/lesson-22-kube-apiserver-insecure.yaml
new file mode 100644
index 0000000..66e9617
--- /dev/null
+++ b/lab/lesson-22-kube-apiserver-insecure.yaml
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: kube-apiserver
+ namespace: kube-system
+spec:
+ hostNetwork: true
+ containers:
+ - name: kube-apiserver
+ image: registry.k8s.io/kube-apiserver:v1.35.0
+ command:
+ - kube-apiserver
+ - --etcd-servers=https://127.0.0.1:2379
+ - --service-cluster-ip-range=10.96.0.0/12
+ - --anonymous-auth=true
+ - --authorization-mode=AlwaysAllow
+ - --service-account-lookup=false
+ - --profiling=true
+ - --audit-log-path=-
diff --git a/lab/lesson-23-answer.md b/lab/lesson-23-answer.md
new file mode 100644
index 0000000..e69de29
diff --git a/lab/lesson-23-cilium-scenario.md b/lab/lesson-23-cilium-scenario.md
new file mode 100644
index 0000000..fc1f1ce
--- /dev/null
+++ b/lab/lesson-23-cilium-scenario.md
@@ -0,0 +1,7 @@
+# Cilium encryption rollout scenario
+
+Cluster `prod-east` already runs Cilium 1.19.6 from the official Helm chart in namespace `kube-system`.
+
+The security team requires transparent encryption for traffic between Cilium-managed Pods on different nodes. The team does **not** require node-to-node encryption. The worker-node firewall currently allows TCP 6443 and UDP 8472, but not WireGuard traffic.
+
+The cluster's operating-system image has WireGuard kernel support. There is no CNI chaining.
diff --git a/lab/lesson-23-solution.md b/lab/lesson-23-solution.md
new file mode 100644
index 0000000..e0e352a
--- /dev/null
+++ b/lab/lesson-23-solution.md
@@ -0,0 +1,34 @@
+# Cilium WireGuard rollout answer
+
+## 1. Helm upgrade command
+
+```bash
+helm upgrade cilium cilium/cilium \
+ --namespace kube-system \
+ --version 1.19.6 \
+ --reuse-values \
+ --set encryption.enabled=true \
+ --set encryption.type=wireguard
+```
+
+## 2. Firewall change
+
+Allow **UDP port 51871** between every cluster node. This is the WireGuard tunnel port used by Cilium.
+
+## 3. Preflight checks
+
+```bash
+cilium status --wait
+kubectl -n kube-system get daemonset cilium
+```
+
+The first confirms Cilium is healthy before the change. The second confirms that the Cilium agent is scheduled on the nodes that will participate in encrypted Pod networking. Also confirm the node firewall allows UDP 51871 in both directions before the upgrade.
+
+## 4. Post-change validation
+
+```bash
+cilium status --wait
+cilium encrypt status
+```
+
+`cilium status --wait` confirms the Cilium deployment is healthy after the rollout. `cilium encrypt status` confirms WireGuard encryption is enabled and reports the encryption state for the nodes.
diff --git a/lab/lesson-24-answer.yaml b/lab/lesson-24-answer.yaml
new file mode 100644
index 0000000..91c1a3c
--- /dev/null
+++ b/lab/lesson-24-answer.yaml
@@ -0,0 +1,34 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: release
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: release-bot
+ namespace: release
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: release-bot-reader
+ namespace: release
+rules:
+ - apiGroups: [""]
+ resources: ["configmaps"]
+ verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: release-bot-reader
+ namespace: release
+subjects:
+ - kind: ServiceAccount
+ name: release-bot
+ namespace: release
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: release-bot-reader
diff --git a/lab/lesson-24-fixture.yaml b/lab/lesson-24-fixture.yaml
new file mode 100644
index 0000000..693f3a5
--- /dev/null
+++ b/lab/lesson-24-fixture.yaml
@@ -0,0 +1,34 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: release
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: release-bot
+ namespace: release
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: release-bot-reader
+ namespace: release
+rules:
+ - apiGroups: [""]
+ resources: ["configmaps", "secrets"]
+ verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: release-bot-reader
+ namespace: release
+subjects:
+ - kind: ServiceAccount
+ name: release-bot
+ namespace: release
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: release-bot-reader
diff --git a/lab/lesson-25-answer.yaml b/lab/lesson-25-answer.yaml
new file mode 100644
index 0000000..1bbec68
--- /dev/null
+++ b/lab/lesson-25-answer.yaml
@@ -0,0 +1,8 @@
+apiVersion: security.istio.io/v1
+kind: PeerAuthentication
+metadata:
+ name: default
+ namespace: payments-mesh
+spec:
+ mtls:
+ mode: STRICT
\ No newline at end of file
diff --git a/lab/lesson-25-fixture.yaml b/lab/lesson-25-fixture.yaml
new file mode 100644
index 0000000..57fd910
--- /dev/null
+++ b/lab/lesson-25-fixture.yaml
@@ -0,0 +1,27 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: payments-mesh
+ labels:
+ istio-injection: enabled
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: payments-api
+ namespace: payments-mesh
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: payments-api
+ template:
+ metadata:
+ labels:
+ app: payments-api
+ spec:
+ containers:
+ - name: api
+ image: nginxinc/nginx-unprivileged:1.27
+ ports:
+ - containerPort: 8080
diff --git a/lab/lesson-26-answer.yaml b/lab/lesson-26-answer.yaml
new file mode 100644
index 0000000..f4639e6
--- /dev/null
+++ b/lab/lesson-26-answer.yaml
@@ -0,0 +1,17 @@
+apiVersion: security.istio.io/v1
+kind: AuthorizationPolicy
+metadata:
+ name: payments-api-only-frontend
+ namespace: payments-mesh
+spec:
+ selector:
+ matchLabels:
+ app: payments-api
+ action: ALLOW
+ rules:
+ - from:
+ - source:
+ principals: ["cluster.local/ns/web-mesh/sa/frontend"]
+ to:
+ - operation:
+ ports: ["8080"]
\ No newline at end of file
diff --git a/lab/lesson-26-fixture.yaml b/lab/lesson-26-fixture.yaml
new file mode 100644
index 0000000..cda050d
--- /dev/null
+++ b/lab/lesson-26-fixture.yaml
@@ -0,0 +1,47 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: web-mesh
+ labels:
+ istio-injection: enabled
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: frontend
+ namespace: web-mesh
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: payments-mesh
+ labels:
+ istio-injection: enabled
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: payments-api
+ namespace: payments-mesh
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: payments-api
+ namespace: payments-mesh
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: payments-api
+ template:
+ metadata:
+ labels:
+ app: payments-api
+ spec:
+ serviceAccountName: payments-api
+ containers:
+ - name: api
+ image: nginxinc/nginx-unprivileged:1.27
+ ports:
+ - containerPort: 8080
diff --git a/lab/lesson-27-answer.md b/lab/lesson-27-answer.md
new file mode 100644
index 0000000..97ef2d9
--- /dev/null
+++ b/lab/lesson-27-answer.md
@@ -0,0 +1,61 @@
+docker run --rm -v "${PWD}:/work" aquasec/trivy:0.68.1 config /work/lab/lesson-27-fixture.yaml
+
+
+
+AVD-KSV-0014 (HIGH): Container 'api' of Deployment 'checkout-api' should set 'securityContext.readOnlyRootFilesystem' to true
+════════════════════════════════════════
+────────────────────────────────────────
+ lesson-27-fixture.yaml:21-28
+────────────────────────────────────────
+ 21 ┌ containers:
+ 22 │ - name: api
+ 23 │ image: nginx:latest
+ 24 │ ports:
+ 25 │ - containerPort: 8080
+ 26 │ securityContext:
+ 27 │ privileged: true
+ 28 └ capabilities:
+────────────────────────────────────────
+explanation: rootFileSystem not readOnly so if the attacker gains access the the pod it can change the root directory
+remedy: enable readOnlyRootFilesystem in container security context
+
+
+AVD-KSV-0017 (HIGH): Container 'api' of Deployment 'checkout-api' should set 'securityContext.privileged' to false
+════════════════════════════════════════
+
+────────────────────────────────────────
+ lesson-27-fixture.yaml:21-28
+────────────────────────────────────────
+ 21 ┌ containers:
+ 22 │ - name: api
+ 23 │ image: nginx:latest
+ 24 │ ports:
+ 25 │ - containerPort: 8080
+ 26 │ securityContext:
+ 27 │ privileged: true
+ 28 └ capabilities:
+────────────────────────────────────────
+
+explanation: allow hacker to get privilege escalation inside the pod if the pod can get service token then it can hack the kubernetes system
+remedy: privileged: false
+
+AVD-KSV-0118 (HIGH): deployment checkout-api in checkout namespace is using the default security context, which allows root privileges
+════════════════════════════════════════
+
+────────────────────────────────────────
+ lesson-27-fixture.yaml:19-28
+────────────────────────────────────────
+ 19 ┌ app: checkout-api
+ 20 │ spec:
+ 21 │ containers:
+ 22 │ - name: api
+ 23 │ image: nginx:latest
+ 24 │ ports:
+ 25 │ - containerPort: 8080
+ 26 │ securityContext:
+ 27 │ privileged: true
+ 28 └ capabilities:
+────────────────────────────────────────
+
+explanation: allow hacker to get privilege escalation inside the pod if the pod can get service token then it can hack the kubernetes system
+remedy: runAsNonRoot: true
\ No newline at end of file
diff --git a/lab/lesson-27-fixture.yaml b/lab/lesson-27-fixture.yaml
new file mode 100644
index 0000000..7773de4
--- /dev/null
+++ b/lab/lesson-27-fixture.yaml
@@ -0,0 +1,29 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: checkout
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: checkout-api
+ namespace: checkout
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: checkout-api
+ template:
+ metadata:
+ labels:
+ app: checkout-api
+ spec:
+ containers:
+ - name: api
+ image: nginx:latest
+ ports:
+ - containerPort: 8080
+ securityContext:
+ privileged: true
+ capabilities:
+ add: ["NET_ADMIN"]
diff --git a/lab/lesson-28-answer.yaml b/lab/lesson-28-answer.yaml
new file mode 100644
index 0000000..c4e56b3
--- /dev/null
+++ b/lab/lesson-28-answer.yaml
@@ -0,0 +1,11 @@
+apiVersion: kubelet.config.k8s.io/v1beta1
+kind: KubeletConfiguration
+authentication:
+ anonymous:
+ enabled: false
+ webhook:
+ enabled: true
+authorization:
+ mode: Webhook
+protectKernelDefaults: true
+rotateCertificates: true
\ No newline at end of file
diff --git a/lab/lesson-28-kubelet-review.yaml b/lab/lesson-28-kubelet-review.yaml
new file mode 100644
index 0000000..904b98e
--- /dev/null
+++ b/lab/lesson-28-kubelet-review.yaml
@@ -0,0 +1,12 @@
+apiVersion: kubelet.config.k8s.io/v1beta1
+kind: KubeletConfiguration
+authentication:
+ anonymous:
+ enabled: true
+ webhook:
+ enabled: false
+authorization:
+ mode: AlwaysAllow
+readOnlyPort: 10255
+protectKernelDefaults: false
+rotateCertificates: false
diff --git a/lab/lesson-29-fixture.yaml b/lab/lesson-29-fixture.yaml
new file mode 100644
index 0000000..e3d0feb
--- /dev/null
+++ b/lab/lesson-29-fixture.yaml
@@ -0,0 +1,35 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: storefront
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: payments-mesh
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: frontend
+ namespace: storefront
+ labels:
+ app: frontend
+spec:
+ containers:
+ - name: app
+ image: nginxinc/nginx-unprivileged:1.27
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: payments-api
+ namespace: payments-mesh
+ labels:
+ app: payments-api
+spec:
+ containers:
+ - name: api
+ image: nginxinc/nginx-unprivileged:1.27
+ ports:
+ - containerPort: 8080
diff --git a/lab/lesson-8-answer.yaml b/lab/lesson-8-answer.yaml
new file mode 100644
index 0000000..6900254
--- /dev/null
+++ b/lab/lesson-8-answer.yaml
@@ -0,0 +1,93 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: cks-net-hard
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: ledger
+ namespace: cks-net-hard
+spec:
+ selector:
+ app: ledger
+ ports:
+ - port: 8080
+ targetPort: 8080
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: ledger
+ namespace: cks-net-hard
+ labels:
+ app: ledger
+spec:
+ containers:
+ - name: ledger
+ image: busybox:1.37
+ command: ["sh", "-c", "mkdir -p /www; echo ledger > /www/index.html; httpd -f -p 8080 -h /www"]
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: checkout
+ namespace: cks-net-hard
+ labels:
+ app: checkout
+spec:
+ containers:
+ - name: checkout
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: intruder
+ namespace: cks-net-hard
+ labels:
+ app: intruder
+spec:
+ containers:
+ - name: intruder
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
+---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: cks-net-hard
+ namespace: cks-net-hard
+spec:
+ podSelector: {}
+ policyTypes:
+ - Ingress
+ - Egress
+ ingress:
+ - from:
+ - podSelector:
+ matchLabels:
+ app: checkout
+ ports:
+ - protocol: TCP
+ port: 8080
+ egress:
+ - to:
+ - podSelector:
+ matchLabels:
+ app: ledger
+ ports:
+ - protocol: TCP
+ port: 8080
+
+ - to:
+ - podSelector:
+ matchLabels:
+ app: checkout
+ ports:
+ - port: 53
+ protocol: TCP
+ - port: 53
+ protocol: UDP
+
diff --git a/lab/lesson-8-fixture.yaml b/lab/lesson-8-fixture.yaml
new file mode 100644
index 0000000..2279aa5
--- /dev/null
+++ b/lab/lesson-8-fixture.yaml
@@ -0,0 +1,55 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: cks-net-hard
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: ledger
+ namespace: cks-net-hard
+spec:
+ selector:
+ app: ledger
+ ports:
+ - port: 8080
+ targetPort: 8080
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: ledger
+ namespace: cks-net-hard
+ labels:
+ app: ledger
+spec:
+ containers:
+ - name: ledger
+ image: busybox:1.37
+ command: ["sh", "-c", "mkdir -p /www; echo ledger > /www/index.html; httpd -f -p 8080 -h /www"]
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: checkout
+ namespace: cks-net-hard
+ labels:
+ app: checkout
+spec:
+ containers:
+ - name: checkout
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: intruder
+ namespace: cks-net-hard
+ labels:
+ app: intruder
+spec:
+ containers:
+ - name: intruder
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
diff --git a/lab/lesson-8-solution.yaml b/lab/lesson-8-solution.yaml
new file mode 100644
index 0000000..0fc50ed
--- /dev/null
+++ b/lab/lesson-8-solution.yaml
@@ -0,0 +1,62 @@
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: default-deny-all
+ namespace: cks-net-hard
+spec:
+ podSelector: {}
+ policyTypes:
+ - Ingress
+ - Egress
+---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: allow-ledger-ingress-from-checkout
+ namespace: cks-net-hard
+spec:
+ podSelector:
+ matchLabels:
+ app: ledger
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - podSelector:
+ matchLabels:
+ app: checkout
+ ports:
+ - protocol: TCP
+ port: 8080
+---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: allow-checkout-egress
+ namespace: cks-net-hard
+spec:
+ podSelector:
+ matchLabels:
+ app: checkout
+ policyTypes:
+ - Egress
+ egress:
+ - to:
+ - podSelector:
+ matchLabels:
+ app: ledger
+ ports:
+ - protocol: TCP
+ port: 8080
+ - to:
+ - namespaceSelector:
+ matchLabels:
+ kubernetes.io/metadata.name: kube-system
+ podSelector:
+ matchLabels:
+ k8s-app: kube-dns
+ ports:
+ - protocol: UDP
+ port: 53
+ - protocol: TCP
+ port: 53
diff --git a/lab/lesson-9-answer.yaml b/lab/lesson-9-answer.yaml
new file mode 100644
index 0000000..35d4311
--- /dev/null
+++ b/lab/lesson-9-answer.yaml
@@ -0,0 +1,59 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: incident-response
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: security-ops
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: audit-bot
+ namespace: security-ops
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: incidnet-response-role
+ namespace: incident-response
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - pods
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - pods/log
+ verbs:
+ - get
+
+- apiGroups:
+ - ""
+ resourceNames:
+ - incident-playbook
+ resources:
+ - configmaps
+ verbs:
+ - get
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: incident-response-rolebinding
+ namespace: incident-response
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: incidnet-response-role
+subjects:
+- kind: ServiceAccount
+ name: audit-bot
+ namespace: security-ops
\ No newline at end of file
diff --git a/lab/lesson-9-fixture.yaml b/lab/lesson-9-fixture.yaml
new file mode 100644
index 0000000..e8ca5a1
--- /dev/null
+++ b/lab/lesson-9-fixture.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: incident-response
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: security-ops
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: audit-bot
+ namespace: security-ops
diff --git a/lab/networkpolicy-lab.yaml b/lab/networkpolicy-lab.yaml
new file mode 100644
index 0000000..a32cbf5
--- /dev/null
+++ b/lab/networkpolicy-lab.yaml
@@ -0,0 +1,77 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: cks-lab
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: db
+ namespace: cks-lab
+ labels:
+ app: db
+spec:
+ containers:
+ - name: http
+ image: hashicorp/http-echo:1.0
+ args: ["-text=database", "-listen=:8080"]
+ ports:
+ - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: db
+ namespace: cks-lab
+spec:
+ selector:
+ app: db
+ ports:
+ - port: 8080
+ targetPort: 8080
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: api
+ namespace: cks-lab
+ labels:
+ app: api
+spec:
+ containers:
+ - name: tools
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: intruder
+ namespace: cks-lab
+ labels:
+ app: intruder
+spec:
+ containers:
+ - name: tools
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
+---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: db-only-from-api
+ namespace: cks-lab
+spec:
+ podSelector:
+ matchLabels:
+ app: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - podSelector:
+ matchLabels:
+ app: api
+ ports:
+ - protocol: TCP
+ port: 8080
diff --git a/lab/payments-hardening-sanswer.yaml b/lab/payments-hardening-sanswer.yaml
new file mode 100644
index 0000000..4548b9d
--- /dev/null
+++ b/lab/payments-hardening-sanswer.yaml
@@ -0,0 +1,35 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: payments-prod
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: payments
+ namespace: payments-prod
+ labels:
+ app: payments
+spec:
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 101
+ seccompProfile:
+ type: RuntimeDefault
+ volumes:
+ - name: secure-tmp
+ emptyDir: {}
+ containers:
+ - name: payments
+ image: nginxinc/nginx-unprivileged:1.27
+ volumeMounts:
+ - name: secure-tmp
+ mountPath: /tmp
+ ports:
+ - containerPort: 8080
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+
+ capabilities:
+ drop: ["ALL"]
diff --git a/lab/payments-hardening-starter.yaml b/lab/payments-hardening-starter.yaml
new file mode 100644
index 0000000..3ef72d0
--- /dev/null
+++ b/lab/payments-hardening-starter.yaml
@@ -0,0 +1,18 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: payments-prod
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: payments
+ namespace: payments-prod
+ labels:
+ app: payments
+spec:
+ containers:
+ - name: payments
+ image: nginxinc/nginx-unprivileged:1.27
+ ports:
+ - containerPort: 8080
diff --git a/lab/pod-security-standards-answer.yaml b/lab/pod-security-standards-answer.yaml
new file mode 100644
index 0000000..d37b97b
--- /dev/null
+++ b/lab/pod-security-standards-answer.yaml
@@ -0,0 +1,30 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: cks-pss
+ labels:
+ pod-security.kubernetes.io/enforce: restricted
+ pod-security.kubernetes.io/enforce-version: latest
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: orders
+ namespace: cks-pss
+ labels:
+ app: orders
+spec:
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ containers:
+ - name: orders
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
diff --git a/lab/pod-security-standards-starter.yaml b/lab/pod-security-standards-starter.yaml
new file mode 100644
index 0000000..df0040a
--- /dev/null
+++ b/lab/pod-security-standards-starter.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: cks-pss
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: orders
+ namespace: cks-pss
+ labels:
+ app: orders
+spec:
+ containers:
+ - name: orders
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
diff --git a/lab/rbac-answer.yaml b/lab/rbac-answer.yaml
new file mode 100644
index 0000000..ef1437d
--- /dev/null
+++ b/lab/rbac-answer.yaml
@@ -0,0 +1,28 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: reporter-role
+ namespace: cks-rbac
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: reporter-rolebinding
+ namespace: cks-rbac
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: reporter-role
+subjects:
+- kind: ServiceAccount
+ name: report-reader
+ namespace: cks-rbac
\ No newline at end of file
diff --git a/lab/rbac-starter.yaml b/lab/rbac-starter.yaml
new file mode 100644
index 0000000..bea1fab
--- /dev/null
+++ b/lab/rbac-starter.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: cks-rbac
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: report-reader
+ namespace: cks-rbac
diff --git a/lab/serviceaccount-token-answer.yaml b/lab/serviceaccount-token-answer.yaml
new file mode 100644
index 0000000..d1cb2e9
--- /dev/null
+++ b/lab/serviceaccount-token-answer.yaml
@@ -0,0 +1,25 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: checkout
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: checkout-api
+ namespace: checkout
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: checkout
+ namespace: checkout
+ labels:
+ app: checkout
+spec:
+ automountServiceAccountToken: false
+ serviceAccountName: checkout-api
+ containers:
+ - name: checkout
+ image: busybox:1.37
+ command: ["sh", "-c", "sleep 3600"]
diff --git a/lab/workload-hardening-answer.yaml b/lab/workload-hardening-answer.yaml
new file mode 100644
index 0000000..33f5b5a
--- /dev/null
+++ b/lab/workload-hardening-answer.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: cks-workload
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: inventory
+ namespace: cks-workload
+ labels:
+ app: inventory
+spec:
+ securityContext:
+ seccompProfile:
+ type: RuntimeDefault
+ containers:
+ - name: inventory
+ image: busybox:1.37
+ command: ["sh", "-c", "id; sleep 3600"]
+ securityContext:
+ runAsUser: 1000
+ runAsNonRoot: true
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop: ["ALL"]
diff --git a/lab/workload-hardening-starter.yaml b/lab/workload-hardening-starter.yaml
new file mode 100644
index 0000000..a7d4a3c
--- /dev/null
+++ b/lab/workload-hardening-starter.yaml
@@ -0,0 +1,22 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: cks-workload
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: inventory
+ namespace: cks-workload
+ labels:
+ app: inventory
+spec:
+ containers:
+ - name: inventory
+ image: busybox:1.37
+ command: ["sh", "-c", "id; sleep 3600"]
+ securityContext:
+ runAsUser: 0
+ allowPrivilegeEscalation: true
+ capabilities:
+ add: ["NET_ADMIN"]
diff --git a/learning-records/0001-existing-kubernetes-foundation.md b/learning-records/0001-existing-kubernetes-foundation.md
new file mode 100644
index 0000000..f3dbb8a
--- /dev/null
+++ b/learning-records/0001-existing-kubernetes-foundation.md
@@ -0,0 +1,3 @@
+# Existing Kubernetes foundation and confidence gap
+
+The learner has completed CKA, CKAD, and KodeKloud CKS training. Future lessons should assume Kubernetes fluency and concentrate on fast security decisions, practice under time constraints, and evidence-based confidence rather than introductory Kubernetes coverage.
diff --git a/learning-records/0002-documentation-assisted-yaml-fluency.md b/learning-records/0002-documentation-assisted-yaml-fluency.md
new file mode 100644
index 0000000..8ff4e43
--- /dev/null
+++ b/learning-records/0002-documentation-assisted-yaml-fluency.md
@@ -0,0 +1,3 @@
+# Documentation-assisted YAML fluency
+
+The learner can write CKS-relevant YAML by using official documentation and has successfully completed the NetworkPolicy drill that way. Lessons should now emphasize rapid requirement classification, deliberate verification, and troubleshooting rather than memorising manifest syntax.
diff --git a/learning-records/0003-rbac-retrieval-and-boundary-verification.md b/learning-records/0003-rbac-retrieval-and-boundary-verification.md
new file mode 100644
index 0000000..6b3ebe1
--- /dev/null
+++ b/learning-records/0003-rbac-retrieval-and-boundary-verification.md
@@ -0,0 +1,7 @@
+# RBAC retrieval and boundary verification
+
+The learner wrote the NetworkPolicy and namespace-scoped RBAC solution from memory, without consulting documentation. In the live lab, the `report-reader` ServiceAccount can get and list ConfigMaps but cannot create ConfigMaps or read Secrets, demonstrating correct least-privilege scope and verification discipline.
+
+## Evidence
+
+Live `kubectl auth can-i` results: `yes`, `yes`, `no`, `no` for the required four permissions.
diff --git a/learning-records/0004-retrieval-profile-through-lesson-13.md b/learning-records/0004-retrieval-profile-through-lesson-13.md
new file mode 100644
index 0000000..ca2990b
--- /dev/null
+++ b/learning-records/0004-retrieval-profile-through-lesson-13.md
@@ -0,0 +1,44 @@
+# Retrieval profile through Lessons 1–13
+
+## Demonstrated strengths
+
+- Can produce and prove basic NetworkPolicy, namespace-scoped RBAC, workload hardening, Pod Security Admission, and ServiceAccount-token controls.
+- Understands least-privilege RBAC after correction: namespace-scoped RoleBinding, `pods/log` as a subresource, and `resourceNames` for a single ConfigMap.
+- Can apply `RuntimeDefault` seccomp, non-root execution, capability dropping, read-only roots, targeted `emptyDir` writable paths, and selective Secret-key projection.
+- Understands the difference between an image tag and digest, and the roles of a ValidatingAdmissionPolicy versus its binding after a worked example.
+- Correctly reconstructed the audit-event sequence: RBAC escalation, Secret access, then Pod exec.
+- Completed the audit-policy design after correction: stage omission, ordered specific rules, Secret RequestResponse logging, RBAC API-group selection, and a Metadata catch-all.
+- Completed kubelet endpoint hardening: anonymous authentication disabled, webhook authentication and authorization enabled, read-only port disabled, kernel defaults protected, and certificate rotation enabled.
+- Completed TLS Ingress configuration after schema corrections: v1 Service backend uses singular `service`, TLS host aligns with the rule host, and route semantics distinguish `Prefix` from `Exact`.
+- Completed a namespace-scoped tenant-isolation admission policy: a normal Pod is admitted, host networking is denied in the tenant namespace, and the same request remains permitted outside the binding scope.
+- Completed Secrets encryption-at-rest configuration: a 32-byte base64 AES-CBC key is first for Secret writes, with `identity` retained only as the final read fallback.
+- Completed a Pod Security Admission migration: all enforce/audit/warn labels and version pins were set, the existing legacy Pod remained running, a compliant replacement became Ready, and a non-compliant new Pod was rejected.
+- Completed kube-apiserver static-Pod hardening: anonymous access disabled, Node/RBAC authorization selected, ServiceAccount lookup enabled, profiling disabled, and an explicit audit-log path configured while preserving required API-server flags.
+- Completed focused RBAC remediation independently: narrowed an existing Role so its ServiceAccount retains ConfigMap read/watch access but cannot read Secrets or create ConfigMaps.
+- Completed the first supplemental Istio security drill independently: namespace-wide `PeerAuthentication` uses the current v1 API and strict mTLS without an unnecessary selector or port exception.
+- Completed supplemental Istio workload authorization after incremental review: an `ALLOW` policy selects only `payments-api`, grants only the exact `web-mesh/frontend` principal, and limits it to port `8080`.
+
+## Retrieval gaps to keep drilling
+
+- SecurityContext field placement: `allowPrivilegeEscalation` and `readOnlyRootFilesystem` are container-level; seccomp, UID, and Pod-wide AppArmor can be Pod-level.
+- Exact API field spelling: `automountServiceAccountToken`; YAML schema errors should be checked with server-side or client dry runs early.
+- Secret projections: `items` requires both `key` and `path`; mount the volume directory rather than treating it as a single file path.
+- NetworkPolicy composition and scope: policies are additive; use a separate default deny and selector-scoped exceptions. Default-deny egress also requires an explicit CoreDNS allowance for Service-name resolution.
+- Runtime investigation: name the principal rather than audit IDs, and distinguish observed malicious actions from containment steps. First revoke the escalation path, then isolate the workload and rotate exposed credentials.
+- Validating Admission Policy CEL and binding scope need further independent practice; Lesson 13 required a reference solution.
+- Audit-policy resource spelling and groups required retrieval help: RBAC uses `rbac.authorization.k8s.io`; the resources are plural, including `clusterroles` and `clusterrolebindings`.
+- Kubelet configuration requires careful schema recall: `authorization` is top-level, the kind is `KubeletConfiguration`, and `readOnlyPort: 0` disables rather than configures the legacy endpoint.
+- Ingress v1 schema and nesting need continued retrieval practice: Service backend is singular; multiple paths belong under `http.paths`; use `Exact` only for a precise path match.
+- CEL admission policy practice exposed a key optional-field pattern: test the parent (`has(c.securityContext)`) before reading a child (`c.securityContext.privileged`); CEL short-circuiting prevents evaluation errors.
+- Encryption-provider ordering and key generation required assistance: AES-CBC keys must decode to 32 bytes; generate random bytes before base64 encoding and keep `identity` last.
+- Pod Security Admission retrieval required iterative correction: policy configuration belongs in Namespace labels (not Pod annotations), all mode labels need fixed version values, and security-context enum spelling is case-sensitive.
+- API-server flag review shows good preservation discipline; reinforce the operational meaning of `service-account-lookup=true` as token revocation/validation protection.
+- Continue using focused, one-concept practical drills before returning to multi-control configurations; this format is better calibrated for current retrieval confidence.
+
+## Environment limitation
+
+Lesson 11 AppArmor could not execute: the Docker Desktop / WSL2 kernel reports AppArmor disabled. The structured Kubernetes API fields were valid, but any RuntimeDefault AppArmor Pod is rejected by the kubelet. Keep AppArmor as a manifest-reading skill here; test enforcement later on an AppArmor-enabled Linux host.
+
+## Coaching adjustment
+
+Future lessons should remain from-scratch with only fixtures or requirements, emphasize exact API schemas and verification before applying, and revisit the retrieval gaps above in mixed scenarios.
diff --git a/lessons/0001-security-control-selection.html b/lessons/0001-security-control-selection.html
new file mode 100644
index 0000000..3feb976
--- /dev/null
+++ b/lessons/0001-security-control-selection.html
@@ -0,0 +1 @@
+
CKS lesson 1: choose the control
Lesson 0001 · 15 minutes
Choose the security control before you touch YAML
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.
Mission connection: 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.
The control map
Who may call the Kubernetes API? RBAC: Role/ClusterRole plus binding.
Which pod configuration may be created? Pod Security Admission or another admission policy.
Which packets may flow? NetworkPolicy—provided the CNI enforces it.
What may the process do inside its container?securityContext, capabilities, seccomp, AppArmor/SELinux.
What happened through the API? Audit logging.
This map follows the official Kubernetes security overview. The practical exam is performance-based and currently runs for two hours; use the Linux Foundation page as the authority for changing logistics.
Practice: classify first, configure second
Requirement
“In namespace payments, only pods labelled app=api may reach TCP 5432 on pods labelled app=db. Everything else should remain denied.”
What is the primary control?
Deliberate drill (10 minutes)
Without looking up syntax, write a NetworkPolicy that selects app=db, permits TCP 5432 only from app=api, and applies in payments.
Validate it: confirm the selectors, port, protocol, and namespace; then test an allowed and a disallowed source if your lab supports it.
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.
Confidence move: after every security change, state one traffic flow that must work and one that must fail. Then test both.
The verification sequence
Confirm selection.kubectl get pods -n payments --show-labels. A correct rule selecting the wrong labels does nothing useful.
Confirm policy type and direction. Read spec.policyTypes: ingress and egress are independent. An isolated pod may need explicit DNS egress.
Test the allow path. From an app=api pod, connect to the database Service on TCP 5432.
Test the deny path. From a deliberately different-labelled pod in the same namespace, repeat the identical connection.
Check enforcement assumptions. NetworkPolicy only works when the installed CNI supports and enforces it.
These mechanics are grounded in the official NetworkPolicy documentation. It explicitly warns that behavior depends on the networking solution.
Retrieval check
Scenario
A policy selects app=db and has an ingress rule permitting app=api to TCP 5432. The API pod still cannot resolve db.payments.svc.cluster.local after you add an egress-isolating policy to it. What did you most likely omit?
Timed drill: build a test matrix (12 minutes)
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.
Source
Destination
Expected
Observed
app=api
app=db:5432
allowed
other labelled pod
app=db:5432
denied
app=api
cluster DNS :53
allowed if egress isolated
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.
diff --git a/lessons/0003-rbac-least-privilege.html b/lessons/0003-rbac-least-privilege.html
new file mode 100644
index 0000000..69dec95
--- /dev/null
+++ b/lessons/0003-rbac-least-privilege.html
@@ -0,0 +1,8 @@
+CKS lesson 3: RBAC least privilege
Lesson 0003 · 20 minutes
RBAC: grant the minimum—and prove the boundary
Security work is not “make it work.” It is “make the required action work while every unnecessary action stays blocked.”
Career connection: least-privilege reviews are a routine platform-security responsibility. The fastest path is always: identify subject → scope → resource → verb → verification.
Timed task: 12 minutes
In namespace cks-rbac, grant ServiceAccount report-reader permission to get and list only configmaps. It must not create ConfigMaps or read Secrets. Use a namespace-scoped Role and RoleBinding.
Set up only the challenge objects: kubectl --context kind-cks-lab apply -f .\lab\rbac-starter.yaml.
Create your answer in lab/rbac-answer.yaml. Use the official RBAC reference—documentation use is expected.
Apply it to the isolated lab.
Run the four verification commands below. Do not stop after the first success.
A requirement is confined to one namespace. Which binding usually best supports least privilege?
Debrief
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.
diff --git a/lessons/0004-workload-hardening.html b/lessons/0004-workload-hardening.html
new file mode 100644
index 0000000..32fe23d
--- /dev/null
+++ b/lessons/0004-workload-hardening.html
@@ -0,0 +1 @@
+CKS lesson 4: harden a workload
Lesson 0004 · 25 minutes
Harden a workload without breaking it
You already know how to write YAML. This drill trains the security-engineering question: which process-level privileges does this workload actually need?
Rule of thumb: every privilege you do not explicitly need is attack surface. Start restrictive, then add only what the workload demonstrably requires.
The hardening controls
runAsNonRoot: true and runAsUser: 1000: prevents the process from running as UID 0.
allowPrivilegeEscalation: false: blocks gaining more privilege through mechanisms such as setuid binaries.
capabilities.drop: ["ALL"]: removes Linux capabilities unless one is explicitly justified.
readOnlyRootFilesystem: true: prevents writes to the image filesystem.
seccompProfile.type: RuntimeDefault: applies the container runtime’s default syscall filter.
The existing inventory pod in namespace cks-workload is intentionally insecure. Create lab/workload-hardening-answer.yaml that preserves the pod name, namespace, image, command, and label while adding every control from the list above.
Inspect the current manifest: kubectl --context kind-cks-lab -n cks-workload get pod inventory -o yaml.
Write your replacement manifest from memory. The fast way to replace a Pod is kubectl --context kind-cks-lab replace --force -f .\lab\workload-hardening-answer.yaml.
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.
Exam reflex: first identify the enforcement scope. Pod Security Admission is configured with labels on the Namespace, not annotations on an individual Pod.
Timed task: 15 minutes
In namespace cks-pss, enforce the restricted Pod Security Standard for the Kubernetes version running in the cluster. Then make the existing orders Pod compliant and running. Create your answer as lab/pod-security-standards-answer.yaml.
Start with kubectl --context kind-cks-lab apply -f .\lab\pod-security-standards-starter.yaml. Your answer must add the required Namespace labels and preserve the Pod name, namespace, image, command, and label.
Proof, not assumption
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'
The final command should be rejected by admission. Remove the failed test Pod if one was created.
Tricky retrieval check
Question
A Pod has spec.securityContext.runAsNonRoot: true, but its only container sets securityContext.runAsUser: 0. In a namespace enforcing restricted, what should you expect?
Documentation target
Use the official Pod Security Standards reference. Look up the exact Namespace label keys and restricted requirements rather than relying on memory.
What to send me
Paste your answer YAML plus the rejection message from root-test. I’ll check both the policy scope and whether your workload satisfies it.
Harden a web workload without breaking its writable paths
A read-only root filesystem limits post-compromise persistence, but applications still need deliberate places for transient state. This drill builds that boundary.
Exam reflex: when you set readOnlyRootFilesystem: true, identify every path the process must write to and mount only those paths as temporary writable volumes.
Timed task: 15 minutes
In namespace payments-prod, harden the payments Pod while keeping it available on port 8080. Begin with kubectl --context kind-cks-lab apply -f .\lab\payments-hardening-starter.yaml and write the completed manifest to lab/payments-hardening-answer.yaml.
Preserve the Pod name, namespace, image, label, and container port. Add the following controls:
Run as non-root UID 101.
Set allowPrivilegeEscalation: false.
Drop all Linux capabilities.
Use the RuntimeDefault seccomp profile.
Set readOnlyRootFilesystem: true.
Mount one writable emptyDir volume at /tmp.
This uses the unprivileged NGINX image, which listens on 8080. 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 NET_BIND_SERVICE.
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.
Exam reflex: distinguish identity from credential mounting. A Pod can name a ServiceAccount for policy clarity while still opting out of its token volume.
Timed task: 12 minutes
The checkout Pod runs in the checkout namespace and does not need to call the Kubernetes API. Apply lab/serviceaccount-token-starter.yaml, then create lab/serviceaccount-token-answer.yaml that preserves its name, namespace, label, image, command, and serviceAccountName while preventing the token from being mounted into this Pod.
Use the smallest-scoped native control: make the change at the Pod specification, rather than changing the ServiceAccount globally.
The token-path check must return success and the final command must print false.
Retrieval check
Question
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?
Build a default-deny network boundary from scratch
This is intentionally closer to an exam task: the workload fixture is supplied, but no NetworkPolicy skeleton or selector hints are provided.
Exam reflex: NetworkPolicies are additive. Start by naming which traffic must survive, then express the smallest set of ingress and egress exceptions after default denial.
Timed task: 25 minutes
Apply lab/lesson-8-fixture.yaml. Then create lab/lesson-8-answer.yaml containing the NetworkPolicies needed to meet every requirement below. Do not change the fixture.
In namespace cks-net-hard, default-deny both ingress and egress for every Pod.
Only the Pod labelled app=checkout may call the ledger Service on TCP port 8080.
checkout must be able to resolve Kubernetes Service names through cluster DNS.
ledger must not have any egress permission.
intruder must not be able to reach ledger.
Your answer must use label selectors. Do not use IP blocks, and do not allow all traffic to or from the namespace.
Expected results: checkout prints ledger; intruder times out; ledger times out. The two denied commands should have a non-zero exit code.
Retrieval check
Question
After a default-deny egress policy is applied to checkout, why can its request to http://ledger:8080 fail before a TCP connection is attempted?
Documentation target
Use the official Network Policies reference. Inspect the CoreDNS labels in your own cluster before writing its selector.
What to send me
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.
Grant incident access without granting escalation paths
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.
Exam reflex: translate every requested action into an API resource, verb, and scope. Treat pods/log as distinct from pods, and use resourceNames when the task names a single object.
Timed task: 25 minutes
Apply lab/lesson-9-fixture.yaml. Then create lab/lesson-9-answer.yaml from scratch.
In the incident-response namespace, grant the ServiceAccount audit-bot from namespace security-ops exactly these permissions:
get, list, and watch Pods.
get Pod logs.
get only the ConfigMap named incident-playbook.
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.
Expected answers, in order: yes yes yes yes no no no no.
Retrieval check
Question
Why does granting get on pods not automatically permit kubectl logs?
Documentation target
Use the official RBAC reference. Confirm the subject format for a cross-namespace ServiceAccount and the behavior of resourceNames.
What to send me
Paste only your Role and RoleBinding YAML, then the eight can-i results. I’ll review least privilege—not merely whether the allowed commands return yes.
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.
Exam reflex: 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.
Timed task: 25 minutes
Apply lab/lesson-10-fixture.yaml. In namespace billing, create lab/lesson-10-answer.yaml from scratch containing a Pod named reconciler.
The Pod must use image busybox:1.37 with command sh -c 'sleep 3600', run as UID 1000, and satisfy all requirements below:
Mount only the api-key key from Secret billing-credentials.
Expose it as the file /var/run/credentials/api-key; the Secret’s legacy-password must not be present in the container.
Make the projected Secret volume read-only for the container.
Do not mount a ServiceAccount token.
Use RuntimeDefault seccomp, prevent privilege escalation, and drop all Linux capabilities.
Use a read-only root filesystem while still allowing the process to use /tmp.
Do not use environment variables for the Secret and do not use wildcard-like broad mounts of the Secret volume.
Apply runtime confinement to a multi-container Pod
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.
Exam reflex: prefer structured securityContext fields over legacy annotations. A container-level setting overrides the Pod-level setting, so inspect both levels when validating a multi-container workload.
Timed task: 25 minutes
Apply lab/lesson-11-fixture.yaml. Create lab/lesson-11-answer.yaml from scratch containing the Pod described below.
Create Pod event-forwarder in namespace runtime-hardening with two containers, both using busybox:1.37 and command sh -c 'sleep 3600':
Container names: forwarder and metrics.
Both containers must receive the runtime’s default AppArmor profile. Use the current structured API field—not an AppArmor annotation.
Both containers must prevent privilege escalation and drop all capabilities.
Run the Pod as a non-root UID 1000, with RuntimeDefault seccomp.
Do not mount a ServiceAccount token.
Use a read-only root filesystem for both containers, while allowing each to write under /tmp.
Do not enable host networking, host PID, host IPC, privileged mode, or hostPath volumes.
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.
Both containers must be Ready, run as a non-zero UID, and write successfully to /tmp. If you set AppArmor at container level, inspect the equivalent container field instead of expecting the final command’s first value.
Retrieval check
Question
A Pod-level AppArmor profile is RuntimeDefault, but one container sets its own profile to Unconfined. Which profile applies to that container?
Documentation target
Use the official AppArmor tutorial and inspect the API schema with kubectl explain pod.spec.securityContext.appArmorProfile.
What to send me
Paste your Pod YAML and the proof outputs. I’ll inspect effective profile scope as well as the usual workload-hardening controls.
Reconstruct an RBAC escalation from audit evidence
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.
Incident reflex: begin with high-impact API resources and verbs—RBAC writes, Secret reads, and exec activity—then pivot by username, source IP, and timestamps.
Timed task: 20 minutes
Analyze lab/lesson-12-audit.jsonl. Do not edit the file. Send me a concise incident note answering all of these:
Which principal is suspicious?
What source IP is associated with the activity?
What is the earliest suspicious timestamp?
Which ClusterRoleBinding was created?
Which Secret was accessed?
What runtime action followed the Secret access?
Give two immediate containment actions, in order.
Use PowerShell, jq, or another structured JSON tool. Do not rely on free-text visual scanning alone.
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.
Exam reflex: distinguish detecting an unsafe image from preventing its reintroduction. First remediate the workload; then enforce the intended rule at the narrowest practical scope.
Timed task: 35 minutes
Apply lab/lesson-13-fixture.yaml. Create lab/lesson-13-answer.yaml from scratch. Do not alter the fixture.
Your answer must contain all resources needed to meet these requirements:
Update Deployment frontend 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.
Create a native ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding.
Within only the supply-chain namespace, reject Pod creates and updates unless every regular container image starts with docker.io/library/ and contains an image digest (@sha256:).
The policy must deny invalid workloads with a helpful message. Do not use a webhook, Kyverno, or a cluster-wide binding.
Retrieve the digest from the running fixture Pod rather than guessing it: inspect .status.containerStatuses[0].imageID. The admission expression must validate all containers, not merely the first one.
Verification
kubectl --context kind-cks-lab apply -f .\lab\lesson-13-fixture.yaml
kubectl --context kind-cks-lab -n supply-chain get pod -l app=frontend -o jsonpath='{.items[0].status.containerStatuses[0].imageID}{"\n"}'
kubectl --context kind-cks-lab apply -f .\lab\lesson-13-answer.yaml
kubectl --context kind-cks-lab -n supply-chain rollout status deployment/frontend --timeout=90s
kubectl --context kind-cks-lab -n supply-chain run mutable-test --image=busybox:1.37 --restart=Never
kubectl --context kind-cks-lab -n default run outside-scope --image=busybox:1.37 --restart=Never
Expected: the rollout succeeds; mutable-test is rejected by admission; outside-scope is admitted because the binding is limited to supply-chain. Remove outside-scope after testing.
Retrieval check
Question
Why is busybox:1.37 weaker supply-chain evidence than docker.io/library/busybox@sha256:…?
Documentation target
Use the Kubernetes Validating Admission Policy reference and inspect the schema locally with kubectl explain validatingadmissionpolicy.spec.validations.
What to send me
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.
Write an audit policy that captures escalation evidence
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.
Exam reflex: audit-policy rules are first-match wins. Put the most specific, high-value rules first and end with a deliberate catch-all.
Timed task: 25 minutes
Create lab/lesson-14-audit-policy.yaml. It must be a valid Kubernetes audit policy that meets every requirement:
Use audit.k8s.io/v1 and kind Policy.
Never record the RequestReceived stage.
Record at RequestResponse level for all Secret requests in namespace payments.
Record at Metadata level for create, update, patch, delete, and deletecollection operations on Roles, RoleBindings, ClusterRoles, and ClusterRoleBindings, regardless of namespace.
Record every other request at Metadata level.
Do not use None to hide any requests beyond the omitted stage. Do not place the catch-all rule above the specific rules.
Validation
An audit policy is an API-server configuration file, not a Kubernetes object. kubectl apply 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 --audit-policy-file pointing to this file.
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>/dev/null
Retrieval check
Question
A catch-all Metadata rule is first in the rule list, followed by a Secret RequestResponse rule. What level is recorded for a Secret request?
Documentation target
Use the official Kubernetes auditing documentation. Confirm the resource-group names for RBAC objects.
What to send me
Paste the policy YAML. I’ll evaluate ordering, resource matching, verb coverage, and whether it records the necessary response bodies.
Lesson 0015 · 35 minutes · Cluster Setup and System Hardening
Remove anonymous access to the kubelet
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.
Exam reflex: distinguish kubelet authentication from authorization. Disabling anonymous access does not make authorization safe by itself; both controls must be configured.
Timed task: 25 minutes
Review lab/lesson-15-kubelet-config-insecure.yaml. Create a complete replacement configuration as lab/lesson-15-kubelet-config-answer.yaml.
Preserve the API version and kind. Your completed configuration must:
Reject unauthenticated kubelet requests.
Use webhook authentication and webhook authorization for authenticated API requests.
Disable the legacy read-only kubelet port.
Protect kernel defaults.
Enable kubelet client-certificate rotation.
Do not add unrelated fields or change API versions. This is a node configuration artifact; do not run kubectl apply against it.
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.
Retrieval check
Question
Why is setting authorization.mode: Webhook insufficient if authentication.anonymous.enabled remains true?
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.
Exam reflex: use networking.k8s.io/v1. In every HTTP path, specify pathType and the Service backend name and port explicitly.
Timed task: 20 minutes
Apply lab/lesson-16-fixture.yaml. Create lab/lesson-16-answer.yaml from scratch with a single Ingress in namespace ingress-drill.
The Ingress must meet all requirements:
Name: orders; API version: networking.k8s.io/v1.
Use ingress class nginx.
Serve only host orders.training.local.
Configure TLS for that same host using Secret orders-tls.
Route /api with Prefix path type to Service orders on port 8080.
Route /healthz with Exact path type to the same Service and port.
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.
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.
Exam reflex: protect optional fields with CEL has(...) checks. A policy must handle an omitted securityContext as safely as an explicit false.
Timed task: 35 minutes
Apply lab/lesson-17-fixture.yaml. Create lab/lesson-17-answer.yaml from scratch containing a ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding.
Only in namespace tenant-a, deny Pod creates and updates when any of the following is true:
hostNetwork, hostPID, or hostIPC is true.
Any volume is a hostPath volume.
Any regular container is privileged.
The policy must allow a normal unprivileged Pod that omits all of these fields. Use failurePolicy: Fail, a useful denial message, and Deny in the binding. Scope the binding with the supplied namespace label; do not apply it cluster-wide or use a webhook.
Verification
kubectl --context kind-cks-lab apply -f .\lab\lesson-17-fixture.yaml
kubectl --context kind-cks-lab apply -f .\lab\lesson-17-answer.yaml
kubectl --context kind-cks-lab -n tenant-a run normal --image=busybox:1.37 --restart=Never -- sh -c 'sleep 60'
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'
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'
Expected: normal is admitted, host-net is rejected, and host-net-outside 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.
Retrieval check
Question
Why is a plain expression such as c.securityContext.privileged == false unsafe for this task?
Documentation target
Use the Kubernetes Validating Admission Policy reference and inspect CEL field availability with kubectl explain pod.spec.
What to send me
Paste the policy and binding, plus the three admission outcomes. I’ll inspect scope, optional-field handling, and every escape path.
diff --git a/lessons/0018-secrets-encryption-at-rest.html b/lessons/0018-secrets-encryption-at-rest.html
new file mode 100644
index 0000000..f170320
--- /dev/null
+++ b/lessons/0018-secrets-encryption-at-rest.html
@@ -0,0 +1,9 @@
+CKS lesson 18: Encrypt Secrets at rest
Lesson 0018 · 40 minutes · Cluster Hardening
Configure encryption before data reaches etcd
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.
Exam reflex: this is an API-server configuration, not a Kubernetes object. Do not use kubectl apply for the encryption configuration. First providers encrypt new writes; identity is a fallback and belongs last.
Timed task: 30 minutes
Apply lab/lesson-18-fixture.yaml only to create the sample Secret. Then write lab/lesson-18-encryption-config.yaml from scratch.
Your configuration must:
Use apiserver.config.k8s.io/v1 and kind EncryptionConfiguration.
Encrypt only the core secrets resource.
Use aescbc as the first provider, with one key named key1.
Use a random, valid 32-byte key encoded in base64.
Include identity only as the final provider.
Do not place identity first, do not use a plaintext key, and do not add an etcd object or Secret to store this configuration.
Verification
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('<your-key>').Length
In a production control plane, place the completed file securely on every API-server node, configure --encryption-provider-config, restart one API server at a time, then rewrite existing Secrets so they are stored with the new provider.
Retrieval check
Question
Why must identity be last in the provider list?
Documentation target
Use the official Kubernetes Encrypting Confidential Data at Rest guide. Review the operational steps for rotating keys and rewriting already-stored objects.
What to send me
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.
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.
Exam reflex: 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.
Timed task: 15 minutes
In lab/lesson-19-artifacts, the trusted-release team supplied SHA256SUMS separately from the two received artifacts. Verify both artifacts using a Linux checksum command. Then create lab/lesson-19-answer.md containing:
The exact verification command you ran.
Which artifact is verified and may proceed to the next deployment-review stage.
Which artifact must not be deployed.
Two appropriate next actions for the mismatched artifact.
Do not edit either artifact or the checksum file. The artifacts are small training stand-ins, not real Kubernetes executables.
Verification
cd lab/lesson-19-artifacts
sha256sum -c SHA256SUMS
# PowerShell equivalent for a single artifact:
Get-FileHash .\kubelet-v1.35.0-linux-amd64 -Algorithm SHA256
One result should be OK; one should be FAILED. A command can return non-zero when any verification fails—that non-zero result is expected evidence, not a reason to skip the check.
Retrieval check
Question
What is the correct immediate response to a checksum mismatch for a proposed kubelet binary?
Documentation target
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.
What to send me
Send the four requested items from your Markdown answer. I’ll check your operational decision, not just the command syntax.
Detect an interactive shell in a production container
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.
Incident reflex: 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.
Timed task: 25 minutes
Review lab/lesson-20-runtime-event.json. Create lab/lesson-20-falco-rule.yaml from scratch containing one custom Falco rule.
The rule must:
Use source syscall.
Match a process named sh or bash only when it runs in a container in Kubernetes namespace payments.
Use priority WARNING.
Include the process command line, Kubernetes namespace, Pod name, and container name in its output.
Include tags container and mitre_execution.
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.
Evidence and response
In lab/lesson-20-answer.md, give:
The exact suspicious command from the supplied event.
The affected namespace, Pod, and container.
Two immediate containment actions in priority order.
This environment does not run Falco, so validate the YAML structure in your editor. On a Falco-equipped Linux node, use falco --validate /path/to/rule.yaml before reloading the rules.
Retrieval check
Question
Which condition component prevents a rule from alerting on every shell launched directly on a Kubernetes node?
Documentation target
Use the official Falco rules reference to confirm condition syntax and available output fields.
What to send me
Paste the rule and your three incident answers. I’ll assess false-positive scope, alert context, and containment quality.
diff --git a/lessons/0021-pod-security-admission-modes.html b/lessons/0021-pod-security-admission-modes.html
new file mode 100644
index 0000000..5cd5540
--- /dev/null
+++ b/lessons/0021-pod-security-admission-modes.html
@@ -0,0 +1 @@
+CKS lesson 21: Pod Security Admission modes
Introduce restricted policy without losing visibility
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.
Exam reflex: 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.
Timed task: 30 minutes
Apply lab/lesson-21-fixture.yaml. Create lab/lesson-21-answer.yaml from scratch. Do not change the fixture.
Deliver a safe migration for namespace partner-workloads:
Existing Pods must remain running.
New or updated Pods must be rejected unless they meet the restricted Pod Security Standard.
Violations must also be visible through audit and user-facing warnings at restricted.
Pin every mode to the Kubernetes minor version running in this cluster.
Create a compliant replacement Pod named exporter-v2 in the same namespace, using image busybox:1.37, command sh -c 'sleep 3600', and label app=exporter.
The replacement must run as non-root UID 1000, 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.
Verification
kubectl --context kind-cks-lab version --output=yaml
kubectl --context kind-cks-lab apply -f .\lab\lesson-21-fixture.yaml
kubectl --context kind-cks-lab apply -f .\lab\lesson-21-answer.yaml
kubectl --context kind-cks-lab -n partner-workloads get pod legacy-exporter exporter-v2
kubectl --context kind-cks-lab -n partner-workloads run rejected-test --image=busybox:1.37 --restart=Never -- sh -c 'sleep 60'
kubectl --context kind-cks-lab get namespace partner-workloads --show-labels
Expected: legacy-exporter remains running, exporter-v2 is Ready, and rejected-test is denied. The final command must show six PSA labels: enforce, audit, and warn levels plus their three version labels.
Retrieval check
Question
Which Pod Security Admission mode blocks a non-compliant new Pod?
Documentation target
Use the official Kubernetes Pod Security Admission reference. Confirm the exact label names and version formatting instead of relying on memory.
What to send me
Paste your namespace and replacement-Pod YAML, plus the rejected admission message. I’ll check all three modes, version pinning, and workload compliance.
diff --git a/lessons/0022-kube-apiserver-hardening.html b/lessons/0022-kube-apiserver-hardening.html
new file mode 100644
index 0000000..017d218
--- /dev/null
+++ b/lessons/0022-kube-apiserver-hardening.html
@@ -0,0 +1,4 @@
+CKS lesson 22: API server hardening
Lesson 0022 · 45 minutes · Cluster Hardening
Remove weak API-server access paths
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.
Exam reflex: inspect every API-server flag before changing it. Preserve unrelated required flags, remove or replace weak settings, and make audit logging explicit.
Timed task: 35 minutes
Review lab/lesson-22-kube-apiserver-insecure.yaml. Create lab/lesson-22-kube-apiserver-answer.yaml as a complete replacement static-Pod manifest.
Preserve the API version, kind, Pod name, namespace, host networking, container name, image, --etcd-servers, and --service-cluster-ip-range. Harden the API server so it:
Rejects anonymous API requests.
Uses Node,RBAC authorization rather than AlwaysAllow.
Validates ServiceAccount tokens against the API server.
Disables profiling endpoints.
Writes audit events to /var/log/kubernetes/audit/audit.log instead of standard output.
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.
Verification
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
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.
Retrieval check
Question
Why is --authorization-mode=AlwaysAllow dangerous even if Roles and RoleBindings are carefully designed?
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.
Exam reflex: 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.
Timed task: 25 minutes
Read lab/lesson-23-cilium-scenario.md. Create lab/lesson-23-answer.md with these four items:
One exact helm upgrade command that retains the existing release values while enabling transparent WireGuard encryption for Cilium 1.19.6.
The required firewall change, including protocol and port.
Two preflight checks you would run before changing the cluster.
Two post-change validation commands, and what each confirms.
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.
Expected reasoning
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 51871. WireGuard keys are generated per node and distributed through Cilium’s Kubernetes resources; do not create a shared Kubernetes Secret for them.
Retrieval check
Question
Does Cilium WireGuard encrypt traffic between two Pods that are scheduled on the same node?
Documentation target
Use Cilium’s official WireGuard transparent encryption guide. Confirm the Helm settings, kernel prerequisite, port, scope, and validation procedure.
What to send me
Paste your four-item rollout note. I’ll review it one issue at a time, without giving the next answer early.
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.
Exam reflex: before creating new RBAC objects, inspect the existing Role and RoleBinding. Often the smallest safe fix is simply to narrow one Role rule.
Timed task: 12 minutes
Apply lab/lesson-24-fixture.yaml. The release-bot ServiceAccount in namespace release is overprivileged.
Create lab/lesson-24-answer.yaml containing a replacement for only the existing Role release-bot-reader. Preserve its name and namespace. After your change, the ServiceAccount must:
Be able to get, list, and watch ConfigMaps in release.
Have no permission to read Secrets.
Have no permission to create ConfigMaps.
Do not create another Role, RoleBinding, ClusterRole, or ServiceAccount. The RoleBinding already connects the correct identity to the correct Role.
Istio sidecars can automatically use mTLS, but permissive acceptance may still allow plaintext requests. PeerAuthentication with STRICT tells receiving workloads to accept only mTLS traffic.
Security reflex: PeerAuthentication controls what the destination workload accepts. For namespace-wide policy, use no workload selector; a selector narrows the policy to only matching Pods.
Timed task: 12 minutes
Read lab/lesson-25-fixture.yaml. Create lab/lesson-25-answer.yaml from scratch with one Istio PeerAuthentication resource.
The policy must:
Use API version security.istio.io/v1.
Be named default in namespace payments-mesh.
Require STRICT mTLS for every workload in that namespace.
Contain no workload selector and no port exceptions.
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.
Verification on an Istio cluster
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
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.
Retrieval check
Question
Why should this namespace-wide policy omit spec.selector?
Allow one workload identity to call the payments API
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.
Security reflex: 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.
Timed task: 15 minutes
Read lab/lesson-26-fixture.yaml. Assume Lesson 25 strict mTLS is already active in payments-mesh. Create lab/lesson-26-answer.yaml from scratch with one Istio AuthorizationPolicy.
The policy must:
Be named payments-api-only-frontend in namespace payments-mesh.
Select only workloads labelled app=payments-api.
Use action ALLOW.
Allow only the ServiceAccount frontend in namespace web-mesh.
Allow only TCP port 8080.
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.
Identity shape
cluster.local/ns/<namespace>/sa/<serviceaccount>
Istio obtains that identity from mTLS. This is why strict mTLS is the foundation for this type of workload authorization.
Verification on an Istio cluster
kubectl apply -f .\lab\lesson-26-fixture.yaml
kubectl apply -f .\lab\lesson-25-answer.yaml
kubectl apply -f .\lab\lesson-26-answer.yaml
kubectl -n payments-mesh get authorizationpolicy payments-api-only-frontend -o yaml
istioctl x describe pod -n payments-mesh -l app=payments-api
Retrieval check
Question
Why is the source principal safer here than allowing all clients from web-mesh?
Find and prioritize insecure workload configuration
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.
Timed task: 15 minutes
Scan lab/lesson-27-fixture.yaml with Trivy. Docker Desktop is sufficient; run:
docker run --rm -v ${PWD}:/work aquasec/trivy:0.68.1 config /work/lab/lesson-27-fixture.yaml
Create lab/lesson-27-answer.md. Do not modify the fixture.
Your answer must contain:
the exact scan command you ran;
three distinct findings from the manifest, in priority order;
one short sentence for each explaining why it increases risk; and
one concrete remediation for each finding.
Scope: report only manifest/configuration findings. Do not claim image-CVE results unless you also scanned an image. A tag such as latest is a deployment reproducibility and supply-chain issue, not proof of a CVE.
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.
Timed task: 15 minutes
Read lab/lesson-28-kubelet-review.yaml. Create lab/lesson-28-answer.yaml from scratch. It must be a complete KubeletConfiguration that corrects every insecure setting present in the fixture.
Keep the same API version and kind. Do not add unrelated kubelet options.
Required security outcome
No anonymous kubelet authentication.
Webhook authentication enabled.
Webhook authorization selected.
The legacy read-only port disabled.
Kernel defaults protected.
Kubelet client certificates rotate.
Exam reflex: distinguish a Boolean switch from a mode. The kubelet authorization choice is a value under authorization.mode; disabling the old HTTP endpoint is done by assigning its port the disabling value.
Prevent a compromised frontend from reaching node metadata
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.
Timed task: 15 minutes
Read lab/lesson-29-fixture.yaml. Create lab/lesson-29-answer.yaml with one NetworkPolicy in namespace storefront.
It must select only Pods labelled app=frontend and permit egress only to:
Pods labelled app=payments-api in namespace payments-mesh, TCP port 8080;
CoreDNS Pods in kube-system, UDP and TCP port 53.
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.
Reasoning hint: 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.