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

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)

  1. Without looking up syntax, write a NetworkPolicy that selects app=db, permits TCP 5432 only from app=api, and applies in payments.
  2. Validate it: confirm the selectors, port, protocol, and namespace; then test an allowed and a disallowed source if your lab supports it.
  3. Compare with the official NetworkPolicy documentation. Add the first thing you missed to your error log.

When you finish, paste your manifest or tell me what felt uncertain. I’ll review it as your teacher and choose the next just-right drill.

diff --git a/lessons/0002-networkpolicy-verification.html b/lessons/0002-networkpolicy-verification.html new file mode 100644 index 0000000..41d7a64 --- /dev/null +++ b/lessons/0002-networkpolicy-verification.html @@ -0,0 +1 @@ +CKS lesson 2: verify NetworkPolicy

Lesson 0002 · 20 minutes

NetworkPolicy: prove what is denied

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

  1. Confirm selection. kubectl get pods -n payments --show-labels. A correct rule selecting the wrong labels does nothing useful.
  2. Confirm policy type and direction. Read spec.policyTypes: ingress and egress are independent. An isolated pod may need explicit DNS egress.
  3. Test the allow path. From an app=api pod, connect to the database Service on TCP 5432.
  4. Test the deny path. From a deliberately different-labelled pod in the same namespace, repeat the identical connection.
  5. 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.

SourceDestinationExpectedObserved
app=apiapp=db:5432allowed
other labelled podapp=db:5432denied
app=apicluster DNS :53allowed 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.

  1. Set up only the challenge objects: kubectl --context kind-cks-lab apply -f .\lab\rbac-starter.yaml.
  2. Create your answer in lab/rbac-answer.yaml. Use the official RBAC reference—documentation use is expected.
  3. Apply it to the isolated lab.
  4. Run the four verification commands below. Do not stop after the first success.

Proof commands

kubectl --context kind-cks-lab auth can-i get configmaps \
+  -n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader
+kubectl --context kind-cks-lab auth can-i list configmaps \
+  -n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader
+kubectl --context kind-cks-lab auth can-i create configmaps \
+  -n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader
+kubectl --context kind-cks-lab auth can-i get secrets \
+  -n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader

Expected result sequence: yes, yes, no, no.

Retrieval check

Question

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

Use the official Linux kernel security constraints and SecurityContext task guide if you need syntax or a deeper explanation.

Timed task: 15 minutes

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.

  1. Inspect the current manifest: kubectl --context kind-cks-lab -n cks-workload get pod inventory -o yaml.
  2. 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.
  3. Wait for it: kubectl --context kind-cks-lab -n cks-workload wait --for=condition=Ready pod/inventory --timeout=60s.
  4. Verify identity: kubectl --context kind-cks-lab -n cks-workload exec inventory -- id. It must show a non-zero UID.

Retrieval check

Question

A container needs no special kernel privilege. What is the secure default for Linux capabilities?

What to send me

Paste your YAML plus the output of id. I’ll verify the security context and show you one exam-speed inspection command.

diff --git a/lessons/0005-pod-security-standards.html b/lessons/0005-pod-security-standards.html new file mode 100644 index 0000000..9c93a36 --- /dev/null +++ b/lessons/0005-pod-security-standards.html @@ -0,0 +1,4 @@ +CKS lesson 5: Pod Security Standards

Lesson 0005 · 25 minutes · Minimize Microservice Vulnerabilities

Enforce a restricted workload boundary

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.

diff --git a/lessons/0006-read-only-root-filesystem.html b/lessons/0006-read-only-root-filesystem.html new file mode 100644 index 0000000..e3aac9b --- /dev/null +++ b/lessons/0006-read-only-root-filesystem.html @@ -0,0 +1 @@ +CKS lesson 6: Read-only filesystems

Lesson 0006 · 25 minutes · Minimize Microservice Vulnerabilities

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:

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.

Proof, not assumption

kubectl --context kind-cks-lab apply -f .\lab\payments-hardening-answer.yaml
kubectl --context kind-cks-lab -n payments-prod wait --for=condition=Ready pod/payments --timeout=60s
kubectl --context kind-cks-lab -n payments-prod exec payments -- id
kubectl --context kind-cks-lab -n payments-prod exec payments -- sh -c 'touch /tmp/ok && test -f /tmp/ok'
kubectl --context kind-cks-lab -n payments-prod exec payments -- sh -c 'touch /root/nope'

The identity must have a non-zero UID, the /tmp check must succeed, and the final command must fail with a read-only filesystem error.

Retrieval check

Question

Why is an emptyDir volume usually preferable to leaving the image filesystem writable for runtime scratch data?

Documentation target

Use the official Configure a Security Context for a Pod or Container guide to confirm security-context placement and volume-mount syntax.

What to send me

Paste your answer YAML and the three proof outputs. I’ll check the writable boundary and show you a fast inspection command.

diff --git a/lessons/0007-serviceaccount-token-exposure.html b/lessons/0007-serviceaccount-token-exposure.html new file mode 100644 index 0000000..f68ef6e --- /dev/null +++ b/lessons/0007-serviceaccount-token-exposure.html @@ -0,0 +1 @@ +CKS lesson 7: ServiceAccount token exposure

Lesson 0007 · 20 minutes · Minimize Microservice Vulnerabilities

Remove API credentials a workload does not need

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.

Proof, not assumption

kubectl --context kind-cks-lab apply -f .\lab\serviceaccount-token-answer.yaml
kubectl --context kind-cks-lab -n checkout wait --for=condition=Ready pod/checkout --timeout=60s
kubectl --context kind-cks-lab -n checkout exec checkout -- sh -c 'test ! -e /var/run/secrets/kubernetes.io/serviceaccount/token'
kubectl --context kind-cks-lab -n checkout get pod checkout -o jsonpath='{.spec.automountServiceAccountToken}{"\n"}'

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?

Documentation target

Use the official Configure Service Accounts for Pods guide to confirm the precedence of Pod and ServiceAccount token-mount settings.

What to send me

Paste your answer YAML and the output of both proof commands. I’ll check the scope and the runtime credential boundary.

diff --git a/lessons/0008-networkpolicy-default-deny.html b/lessons/0008-networkpolicy-default-deny.html new file mode 100644 index 0000000..f2ee2e2 --- /dev/null +++ b/lessons/0008-networkpolicy-default-deny.html @@ -0,0 +1 @@ +CKS lesson 8: NetworkPolicy default deny

Lesson 0008 · 35 minutes · Minimize Microservice Vulnerabilities

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.

Your answer must use label selectors. Do not use IP blocks, and do not allow all traffic to or from the namespace.

Verification

kubectl --context kind-cks-lab apply -f .\lab\lesson-8-fixture.yaml
kubectl --context kind-cks-lab -n cks-net-hard wait --for=condition=Ready pod/ledger pod/checkout pod/intruder --timeout=90s
kubectl --context kind-cks-lab apply -f .\lab\lesson-8-answer.yaml
kubectl --context kind-cks-lab -n cks-net-hard exec checkout -- wget -qO- -T 5 http://ledger:8080
kubectl --context kind-cks-lab -n cks-net-hard exec intruder -- wget -qO- -T 5 http://ledger:8080
kubectl --context kind-cks-lab -n cks-net-hard exec ledger -- wget -qO- -T 5 http://kubernetes.default

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.

diff --git a/lessons/0009-rbac-subresources-and-resourcenames.html b/lessons/0009-rbac-subresources-and-resourcenames.html new file mode 100644 index 0000000..3a635f0 --- /dev/null +++ b/lessons/0009-rbac-subresources-and-resourcenames.html @@ -0,0 +1 @@ +CKS lesson 9: Precise RBAC

Lesson 0009 · 35 minutes · Minimize Microservice Vulnerabilities

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:

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.

Verification

kubectl --context kind-cks-lab apply -f .\lab\lesson-9-fixture.yaml
kubectl --context kind-cks-lab apply -f .\lab\lesson-9-answer.yaml
$sa = 'system:serviceaccount:security-ops:audit-bot'
kubectl --context kind-cks-lab auth can-i get pods --as=$sa -n incident-response
kubectl --context kind-cks-lab auth can-i watch pods --as=$sa -n incident-response
kubectl --context kind-cks-lab auth can-i get pods/log --as=$sa -n incident-response
kubectl --context kind-cks-lab auth can-i get configmap/incident-playbook --as=$sa -n incident-response
kubectl --context kind-cks-lab auth can-i get configmap/another-config --as=$sa -n incident-response
kubectl --context kind-cks-lab auth can-i get secrets --as=$sa -n incident-response
kubectl --context kind-cks-lab auth can-i create pods --as=$sa -n incident-response
kubectl --context kind-cks-lab auth can-i create pods/exec --as=$sa -n incident-response

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.

diff --git a/lessons/0010-minimize-secret-exposure.html b/lessons/0010-minimize-secret-exposure.html new file mode 100644 index 0000000..5c80713 --- /dev/null +++ b/lessons/0010-minimize-secret-exposure.html @@ -0,0 +1 @@ +CKS lesson 10: Minimize Secret exposure

Lesson 0010 · 35 minutes · Minimize Microservice Vulnerabilities

Expose only the Secret data an application needs

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:

Do not use environment variables for the Secret and do not use wildcard-like broad mounts of the Secret volume.

Verification

kubectl --context kind-cks-lab apply -f .\lab\lesson-10-fixture.yaml
kubectl --context kind-cks-lab apply -f .\lab\lesson-10-answer.yaml
kubectl --context kind-cks-lab -n billing wait --for=condition=Ready pod/reconciler --timeout=60s
kubectl --context kind-cks-lab -n billing exec reconciler -- id
kubectl --context kind-cks-lab -n billing exec reconciler -- cat /var/run/credentials/api-key
kubectl --context kind-cks-lab -n billing exec reconciler -- sh -c 'test ! -e /var/run/credentials/legacy-password && touch /tmp/proof'
kubectl --context kind-cks-lab -n billing exec reconciler -- sh -c 'test ! -e /var/run/secrets/kubernetes.io/serviceaccount/token'

Expected: a non-zero UID, test-key, and success from each final two commands.

Retrieval check

Question

Which Secret-volume mechanism prevents an unneeded key from being mounted at all?

Documentation target

Use the Kubernetes Secret reference for selective key projection and the Security Context guide for the runtime controls.

What to send me

Paste your Pod YAML and the four proof results. I’ll inspect both the visible behavior and whether the manifest prevents the broader Secret exposure.

diff --git a/lessons/0011-apparmor-runtime-confinement.html b/lessons/0011-apparmor-runtime-confinement.html new file mode 100644 index 0000000..68ad4f9 --- /dev/null +++ b/lessons/0011-apparmor-runtime-confinement.html @@ -0,0 +1 @@ +CKS lesson 11: AppArmor confinement

Lesson 0011 · 35 minutes · System Hardening

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':

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.

Verification

kubectl --context kind-cks-lab apply -f .\lab\lesson-11-fixture.yaml
kubectl --context kind-cks-lab apply -f .\lab\lesson-11-answer.yaml
kubectl --context kind-cks-lab -n runtime-hardening wait --for=condition=Ready pod/event-forwarder --timeout=60s
kubectl --context kind-cks-lab -n runtime-hardening exec event-forwarder -c forwarder -- id
kubectl --context kind-cks-lab -n runtime-hardening exec event-forwarder -c metrics -- sh -c 'touch /tmp/proof'
kubectl --context kind-cks-lab -n runtime-hardening get pod event-forwarder -o jsonpath='{.spec.securityContext.appArmorProfile.type}{"\n"}{.spec.securityContext.seccompProfile.type}{"\n"}{.spec.automountServiceAccountToken}{"\n"}'

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.

diff --git a/lessons/0012-audit-log-investigation.html b/lessons/0012-audit-log-investigation.html new file mode 100644 index 0000000..66a02a0 --- /dev/null +++ b/lessons/0012-audit-log-investigation.html @@ -0,0 +1,4 @@ +CKS lesson 12: Audit investigation

Lesson 0012 · 30 minutes · Monitoring, Logging, and Runtime Security

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:

  1. Which principal is suspicious?
  2. What source IP is associated with the activity?
  3. What is the earliest suspicious timestamp?
  4. Which ClusterRoleBinding was created?
  5. Which Secret was accessed?
  6. What runtime action followed the Secret access?
  7. Give two immediate containment actions, in order.

Use PowerShell, jq, or another structured JSON tool. Do not rely on free-text visual scanning alone.

Useful investigation shapes

Get-Content .\lab\lesson-12-audit.jsonl | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object { $_.objectRef.resource -in 'clusterrolebindings','secrets' } | Select-Object requestReceivedTimestamp, verb, user, sourceIPs, objectRef
+
+# Linux exam-style equivalent
+jq -c 'select(.objectRef.resource == "clusterrolebindings" or .objectRef.resource == "secrets")' lesson-12-audit.jsonl

Retrieval check

Question

Which audit event is the strongest initial indicator of privilege escalation?

Documentation target

Use the official Kubernetes auditing documentation to review audit stages, levels, and policy behavior.

What to send me

Send your seven answers and the exact filter command you used. I’ll check the timeline and the containment priorities.

diff --git a/lessons/0013-image-digest-admission-policy.html b/lessons/0013-image-digest-admission-policy.html new file mode 100644 index 0000000..318fd73 --- /dev/null +++ b/lessons/0013-image-digest-admission-policy.html @@ -0,0 +1 @@ +CKS lesson 13: Image digest admission

Lesson 0013 · 45 minutes · Supply Chain Security

Enforce immutable container images at admission

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:

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.

diff --git a/lessons/0014-kubernetes-audit-policy.html b/lessons/0014-kubernetes-audit-policy.html new file mode 100644 index 0000000..661ac85 --- /dev/null +++ b/lessons/0014-kubernetes-audit-policy.html @@ -0,0 +1,4 @@ +CKS lesson 14: Audit policy

Lesson 0014 · 35 minutes · Monitoring, Logging, and Runtime Security

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:

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.

diff --git a/lessons/0015-kubelet-endpoint-hardening.html b/lessons/0015-kubelet-endpoint-hardening.html new file mode 100644 index 0000000..40404fd --- /dev/null +++ b/lessons/0015-kubelet-endpoint-hardening.html @@ -0,0 +1,2 @@ +CKS lesson 15: Kubelet endpoint hardening

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:

Do not add unrelated fields or change API versions. This is a node configuration artifact; do not run kubectl apply against it.

Verification

Get-Content .\lab\lesson-15-kubelet-config-answer.yaml
+

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?

Documentation target

Use the official Kubernetes KubeletConfiguration API reference and the kubelet authentication/authorization documentation.

What to send me

Paste the replacement configuration. I’ll check every required field, then explain how the settings combine to secure the endpoint.

diff --git a/lessons/0016-tls-ingress.html b/lessons/0016-tls-ingress.html new file mode 100644 index 0000000..fc35a05 --- /dev/null +++ b/lessons/0016-tls-ingress.html @@ -0,0 +1,3 @@ +CKS lesson 16: TLS Ingress

Lesson 0016 · 30 minutes · Cluster Setup

Route a service through a TLS-enabled Ingress

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:

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.

Verification

kubectl --context kind-cks-lab apply -f .\lab\lesson-16-fixture.yaml
+kubectl --context kind-cks-lab apply --dry-run=server -f .\lab\lesson-16-answer.yaml
+kubectl --context kind-cks-lab -n ingress-drill get ingress orders -o yaml

After server-side dry run succeeds, apply the answer manifest for inspection. Do not expect an address or TLS handshake without an Ingress controller.

Retrieval check

Question

What must match for the TLS host in an Ingress to cover the public route?

Documentation target

Use the official Kubernetes Ingress reference to verify the v1 backend and TLS schema.

What to send me

Paste the Ingress YAML and the server-side dry-run result. I’ll check host/TLS alignment, path semantics, and backend syntax.

diff --git a/lessons/0017-tenant-isolation-admission.html b/lessons/0017-tenant-isolation-admission.html new file mode 100644 index 0000000..a3ab761 --- /dev/null +++ b/lessons/0017-tenant-isolation-admission.html @@ -0,0 +1 @@ +CKS lesson 17: Tenant isolation admission

Lesson 0017 · 45 minutes · Minimize Microservice Vulnerabilities

Block host-escape settings before scheduling

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:

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:

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.

diff --git a/lessons/0019-platform-binary-verification.html b/lessons/0019-platform-binary-verification.html new file mode 100644 index 0000000..67a321e --- /dev/null +++ b/lessons/0019-platform-binary-verification.html @@ -0,0 +1 @@ +CKS lesson 19: Binary verification

Lesson 0019 · 25 minutes · Cluster Setup

Verify a platform binary before deployment

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:

  1. The exact verification command you ran.
  2. Which artifact is verified and may proceed to the next deployment-review stage.
  3. Which artifact must not be deployed.
  4. 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.

diff --git a/lessons/0020-runtime-shell-detection.html b/lessons/0020-runtime-shell-detection.html new file mode 100644 index 0000000..9e16161 --- /dev/null +++ b/lessons/0020-runtime-shell-detection.html @@ -0,0 +1 @@ +CKS lesson 20: Runtime shell detection

Lesson 0020 · 35 minutes · Monitoring, Logging, and Runtime Security

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:

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:

  1. The exact suspicious command from the supplied event.
  2. The affected namespace, Pod, and container.
  3. 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

Lesson 0021 · 40 minutes · Minimize Microservice Vulnerabilities

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:

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:

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?

Documentation target

Use the Kubernetes kube-apiserver command reference to confirm the security-flag behavior.

What to send me

Paste your complete command list. I’ll review one flag at a time and preserve your request not to reveal the next correction early.

diff --git a/lessons/0023-cilium-wireguard-encryption.html b/lessons/0023-cilium-wireguard-encryption.html new file mode 100644 index 0000000..cd9bb0a --- /dev/null +++ b/lessons/0023-cilium-wireguard-encryption.html @@ -0,0 +1 @@ +CKS lesson 23: Cilium WireGuard encryption

Lesson 0023 · 35 minutes · Minimize Microservice Vulnerabilities

Encrypt Pod-to-Pod traffic with Cilium WireGuard

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:

  1. One exact helm upgrade command that retains the existing release values while enabling transparent WireGuard encryption for Cilium 1.19.6.
  2. The required firewall change, including protocol and port.
  3. Two preflight checks you would run before changing the cluster.
  4. 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.

diff --git a/lessons/0024-rbac-remediate-overprivileged-role.html b/lessons/0024-rbac-remediate-overprivileged-role.html new file mode 100644 index 0000000..efc7f1b --- /dev/null +++ b/lessons/0024-rbac-remediate-overprivileged-role.html @@ -0,0 +1 @@ +CKS lesson 24: RBAC remediation

Lesson 0024 · 20 minutes · Cluster Hardening

Remove an unnecessary Secret-read permission

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:

Do not create another Role, RoleBinding, ClusterRole, or ServiceAccount. The RoleBinding already connects the correct identity to the correct Role.

Verification

kubectl --context kind-cks-lab apply -f .\lab\lesson-24-fixture.yaml
kubectl --context kind-cks-lab apply -f .\lab\lesson-24-answer.yaml
$sa = 'system:serviceaccount:release:release-bot'
kubectl --context kind-cks-lab auth can-i get configmaps --as=$sa -n release
kubectl --context kind-cks-lab auth can-i list configmaps --as=$sa -n release
kubectl --context kind-cks-lab auth can-i watch configmaps --as=$sa -n release
kubectl --context kind-cks-lab auth can-i get secrets --as=$sa -n release
kubectl --context kind-cks-lab auth can-i create configmaps --as=$sa -n release

Expected results: yes yes yes no no.

Retrieval check

Question

Why is it better to remove the Secret permission from the existing Role than to create a second, narrower Role and binding?

Documentation target

Use the official Kubernetes RBAC reference for rules, bindings, and ServiceAccount subject names.

What to send me

Paste only the replacement Role and the five can-i results. I’ll review it one issue at a time and will not reveal the next correction early.

diff --git a/lessons/0025-istio-strict-mtls.html b/lessons/0025-istio-strict-mtls.html new file mode 100644 index 0000000..4f6df63 --- /dev/null +++ b/lessons/0025-istio-strict-mtls.html @@ -0,0 +1,4 @@ +Supplemental lesson 25: Istio strict mTLS

Supplemental Lesson 0025 · 20 minutes · Istio service-mesh security

Require mTLS for one application namespace

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:

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?

Documentation target

Use Istio’s official PeerAuthentication reference and mTLS authentication task.

What to send me

Paste the single policy YAML. I’ll check it one issue at a time and won’t reveal the next correction early.

diff --git a/lessons/0026-istio-workload-authorization.html b/lessons/0026-istio-workload-authorization.html new file mode 100644 index 0000000..8443591 --- /dev/null +++ b/lessons/0026-istio-workload-authorization.html @@ -0,0 +1 @@ +Supplemental lesson 26: Istio workload authorization

Supplemental Lesson 0026 · 25 minutes · Istio service-mesh security

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:

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?

Documentation target

Use Istio’s official AuthorizationPolicy reference and security concepts.

What to send me

Paste the single policy YAML. I’ll review it one issue at a time and will not reveal the next correction early.

diff --git a/lessons/0027-static-manifest-analysis.html b/lessons/0027-static-manifest-analysis.html new file mode 100644 index 0000000..ec53edf --- /dev/null +++ b/lessons/0027-static-manifest-analysis.html @@ -0,0 +1 @@ +Lesson 27: Static manifest analysis

Lesson 0027 · 25 minutes · Supply Chain Security

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:

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.

Retrieval check

Question

Why should CI scan both manifests and images?

Documentation target

Use the official Trivy configuration scanning documentation.

What to send me

Save the Markdown answer, then say done check. I’ll review one finding at a time and will not supply the whole answer unless you request it.

diff --git a/lessons/0028-cis-kubelet-review.html b/lessons/0028-cis-kubelet-review.html new file mode 100644 index 0000000..ae2306a --- /dev/null +++ b/lessons/0028-cis-kubelet-review.html @@ -0,0 +1 @@ +Lesson 28: Kubelet CIS review

Lesson 0028 · 25 minutes · Cluster Setup

Review and harden a kubelet configuration

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

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.

Safe verification

kubectl apply --dry-run=client -f .\lab\lesson-28-answer.yaml

Do not apply this directly to a live node without following your distribution’s kubelet configuration and restart procedure.

Documentation target

Use the Kubernetes KubeletConfiguration API reference and the CIS Kubernetes Benchmark for your environment.

What to send me

Save the YAML, then say check. I’ll review only the first issue at a time.

diff --git a/lessons/0029-node-metadata-egress-isolation.html b/lessons/0029-node-metadata-egress-isolation.html new file mode 100644 index 0000000..d8b01ab --- /dev/null +++ b/lessons/0029-node-metadata-egress-isolation.html @@ -0,0 +1 @@ +Lesson 29: Node metadata egress isolation

Lesson 0029 · 25 minutes · Cluster Setup

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:

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.

Verification

kubectl apply --dry-run=client -f .\lab\lesson-29-answer.yaml

What to send me

Save the policy, then say check. I will review one issue at a time.

diff --git a/reference/cks-four-week-field-guide.html b/reference/cks-four-week-field-guide.html new file mode 100644 index 0000000..777e60c --- /dev/null +++ b/reference/cks-four-week-field-guide.html @@ -0,0 +1 @@ +CKS four-week field guide

Quick reference

CKS: four-week field guide

Your job is now practice-to-proof, not course-to-course.

Operating rhythm

Four-week sequence

  1. Week 1 — Workload security: Pod Security Admission, security contexts, capabilities, seccomp/AppArmor, service accounts, RBAC.
  2. Week 2 — Cluster and supply chain: API-server and kubelet hardening, encryption, audit policy, image provenance/scanning, admission controls.
  3. Week 3 — Network and runtime: NetworkPolicy (ingress + egress), mTLS concepts, Falco-style detection and incident triage.
  4. Week 4 — Performance: two full simulations, targeted re-drills from your error log, then light review and sleep protection.

Every task: the 5-step loop

  1. Read the exact requirement; identify the control plane, namespace, workload, or node scope.
  2. Inspect before changing: kubectl get, describe, auth can-i, logs, or the relevant manifest.
  3. Apply the smallest compliant change.
  4. Verify behavior, not only YAML syntax.
  5. Record one mistake or shortcut in an error log.
Confidence rule: confidence comes from completed, verified tasks. Track evidence: task, time used, first-try result, correction.

Use the official CKS curriculum to map each task, and the Linux Foundation exam page for current logistics.