Completed upto lesson 28
This commit is contained in:
62
CKS-SYLLABUS.md
Normal file
62
CKS-SYLLABUS.md
Normal file
@@ -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`.
|
||||||
16
MISSION.md
Normal file
16
MISSION.md
Normal file
@@ -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.
|
||||||
5
NOTES.md
Normal file
5
NOTES.md
Normal file
@@ -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.
|
||||||
25
RESOURCES.md
Normal file
25
RESOURCES.md
Normal file
@@ -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.
|
||||||
0
answers/lesson-3.partial-empty-copy.yaml
Normal file
0
answers/lesson-3.partial-empty-copy.yaml
Normal file
6
answers/lesson-3.yaml
Normal file
6
answers/lesson-3.yaml
Normal file
@@ -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
|
||||||
|
|
||||||
29
answers/network-policy.yaml
Normal file
29
answers/network-policy.yaml
Normal file
@@ -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
|
||||||
9
assets/course.css
Normal file
9
assets/course.css
Normal file
@@ -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; } }
|
||||||
37
lab/README.md
Normal file
37
lab/README.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
BIN
lab/bin/kind.exe
Normal file
BIN
lab/bin/kind.exe
Normal file
Binary file not shown.
9
lab/kind-calico.yaml
Normal file
9
lab/kind-calico.yaml
Normal file
@@ -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
|
||||||
57
lab/lesson-10-answer.yaml
Normal file
57
lab/lesson-10-answer.yaml
Normal file
@@ -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
|
||||||
14
lab/lesson-10-fixture.yaml
Normal file
14
lab/lesson-10-fixture.yaml
Normal file
@@ -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
|
||||||
42
lab/lesson-11-answer.yaml
Normal file
42
lab/lesson-11-answer.yaml
Normal file
@@ -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"]
|
||||||
4
lab/lesson-11-fixture.yaml
Normal file
4
lab/lesson-11-fixture.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: runtime-hardening
|
||||||
40
lab/lesson-12-audit-ans.jsonl
Normal file
40
lab/lesson-12-audit-ans.jsonl
Normal file
@@ -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
|
||||||
7
lab/lesson-12-audit.jsonl
Normal file
7
lab/lesson-12-audit.jsonl
Normal file
@@ -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}}
|
||||||
26
lab/lesson-13-answer.yaml
Normal file
26
lab/lesson-13-answer.yaml
Normal file
@@ -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"]
|
||||||
26
lab/lesson-13-fixture.yaml
Normal file
26
lab/lesson-13-fixture.yaml
Normal file
@@ -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"]
|
||||||
47
lab/lesson-13-solution.yaml
Normal file
47
lab/lesson-13-solution.yaml
Normal file
@@ -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
|
||||||
27
lab/lesson-14-audit-policy.yaml
Normal file
27
lab/lesson-14-audit-policy.yaml
Normal file
@@ -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
|
||||||
13
lab/lesson-15-kubelet-config-answer.yaml
Normal file
13
lab/lesson-15-kubelet-config-answer.yaml
Normal file
@@ -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
|
||||||
|
|
||||||
12
lab/lesson-15-kubelet-config-insecure copy.yaml
Normal file
12
lab/lesson-15-kubelet-config-insecure copy.yaml
Normal file
@@ -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
|
||||||
60
lab/lesson-16-answer.yaml
Normal file
60
lab/lesson-16-answer.yaml
Normal file
@@ -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
|
||||||
27
lab/lesson-16-fixture.yaml
Normal file
27
lab/lesson-16-fixture.yaml
Normal file
@@ -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
|
||||||
39
lab/lesson-17-answer.yaml
Normal file
39
lab/lesson-17-answer.yaml
Normal file
@@ -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
|
||||||
6
lab/lesson-17-fixture.yaml
Normal file
6
lab/lesson-17-fixture.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: tenant-a
|
||||||
|
labels:
|
||||||
|
training.cks/lesson: tenant-a
|
||||||
32
lab/lesson-17-solution.yaml
Normal file
32
lab/lesson-17-solution.yaml
Normal file
@@ -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
|
||||||
14
lab/lesson-18-answer.yaml
Normal file
14
lab/lesson-18-answer.yaml
Normal file
@@ -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
|
||||||
|
---
|
||||||
11
lab/lesson-18-encryption-config.yaml
Normal file
11
lab/lesson-18-encryption-config.yaml
Normal file
@@ -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
|
||||||
13
lab/lesson-18-fixture.yaml
Normal file
13
lab/lesson-18-fixture.yaml
Normal file
@@ -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
|
||||||
13
lab/lesson-19-answer.md
Normal file
13
lab/lesson-19-answer.md
Normal file
@@ -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
|
||||||
2
lab/lesson-19-artifacts/SHA256SUMS
Normal file
2
lab/lesson-19-artifacts/SHA256SUMS
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
e67f13c4f37958bd8fd975cb870b8d245db5655b26621f878af353e8d79ab0e7 kubelet-v1.35.0-linux-amd64
|
||||||
|
6d98653149dde123a1972890a6da297449f40606d70d38502aac8985e9b67eec kubectl-v1.35.0-linux-amd64
|
||||||
1
lab/lesson-19-artifacts/kubectl-v1.35.0-linux-amd64
Normal file
1
lab/lesson-19-artifacts/kubectl-v1.35.0-linux-amd64
Normal file
@@ -0,0 +1 @@
|
|||||||
|
received-training-kubectl-artifact-v1.35.0-tampered
|
||||||
1
lab/lesson-19-artifacts/kubelet-v1.35.0-linux-amd64
Normal file
1
lab/lesson-19-artifacts/kubelet-v1.35.0-linux-amd64
Normal file
@@ -0,0 +1 @@
|
|||||||
|
trusted-training-kubelet-artifact-v1.35.0
|
||||||
15
lab/lesson-20-falco-rule.yaml
Normal file
15
lab/lesson-20-falco-rule.yaml
Normal file
@@ -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]
|
||||||
|
|
||||||
|
|
||||||
4
lab/lesson-20-runtime-event.json
Normal file
4
lab/lesson-20-runtime-event.json
Normal file
@@ -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"}
|
||||||
|
|
||||||
34
lab/lesson-21-answer.yaml
Normal file
34
lab/lesson-21-answer.yaml
Normal file
@@ -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"]
|
||||||
17
lab/lesson-21-fixture.yaml
Normal file
17
lab/lesson-21-fixture.yaml
Normal file
@@ -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"]
|
||||||
19
lab/lesson-22-kube-apiserver-answer.yaml
Normal file
19
lab/lesson-22-kube-apiserver-answer.yaml
Normal file
@@ -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
|
||||||
19
lab/lesson-22-kube-apiserver-insecure.yaml
Normal file
19
lab/lesson-22-kube-apiserver-insecure.yaml
Normal file
@@ -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=-
|
||||||
0
lab/lesson-23-answer.md
Normal file
0
lab/lesson-23-answer.md
Normal file
7
lab/lesson-23-cilium-scenario.md
Normal file
7
lab/lesson-23-cilium-scenario.md
Normal file
@@ -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.
|
||||||
34
lab/lesson-23-solution.md
Normal file
34
lab/lesson-23-solution.md
Normal file
@@ -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.
|
||||||
34
lab/lesson-24-answer.yaml
Normal file
34
lab/lesson-24-answer.yaml
Normal file
@@ -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
|
||||||
34
lab/lesson-24-fixture.yaml
Normal file
34
lab/lesson-24-fixture.yaml
Normal file
@@ -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
|
||||||
8
lab/lesson-25-answer.yaml
Normal file
8
lab/lesson-25-answer.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: security.istio.io/v1
|
||||||
|
kind: PeerAuthentication
|
||||||
|
metadata:
|
||||||
|
name: default
|
||||||
|
namespace: payments-mesh
|
||||||
|
spec:
|
||||||
|
mtls:
|
||||||
|
mode: STRICT
|
||||||
27
lab/lesson-25-fixture.yaml
Normal file
27
lab/lesson-25-fixture.yaml
Normal file
@@ -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
|
||||||
17
lab/lesson-26-answer.yaml
Normal file
17
lab/lesson-26-answer.yaml
Normal file
@@ -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"]
|
||||||
47
lab/lesson-26-fixture.yaml
Normal file
47
lab/lesson-26-fixture.yaml
Normal file
@@ -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
|
||||||
61
lab/lesson-27-answer.md
Normal file
61
lab/lesson-27-answer.md
Normal file
@@ -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
|
||||||
29
lab/lesson-27-fixture.yaml
Normal file
29
lab/lesson-27-fixture.yaml
Normal file
@@ -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"]
|
||||||
11
lab/lesson-28-answer.yaml
Normal file
11
lab/lesson-28-answer.yaml
Normal file
@@ -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
|
||||||
12
lab/lesson-28-kubelet-review.yaml
Normal file
12
lab/lesson-28-kubelet-review.yaml
Normal file
@@ -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
|
||||||
35
lab/lesson-29-fixture.yaml
Normal file
35
lab/lesson-29-fixture.yaml
Normal file
@@ -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
|
||||||
93
lab/lesson-8-answer.yaml
Normal file
93
lab/lesson-8-answer.yaml
Normal file
@@ -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
|
||||||
|
|
||||||
55
lab/lesson-8-fixture.yaml
Normal file
55
lab/lesson-8-fixture.yaml
Normal file
@@ -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"]
|
||||||
62
lab/lesson-8-solution.yaml
Normal file
62
lab/lesson-8-solution.yaml
Normal file
@@ -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
|
||||||
59
lab/lesson-9-answer.yaml
Normal file
59
lab/lesson-9-answer.yaml
Normal file
@@ -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
|
||||||
15
lab/lesson-9-fixture.yaml
Normal file
15
lab/lesson-9-fixture.yaml
Normal file
@@ -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
|
||||||
77
lab/networkpolicy-lab.yaml
Normal file
77
lab/networkpolicy-lab.yaml
Normal file
@@ -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
|
||||||
35
lab/payments-hardening-sanswer.yaml
Normal file
35
lab/payments-hardening-sanswer.yaml
Normal file
@@ -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"]
|
||||||
18
lab/payments-hardening-starter.yaml
Normal file
18
lab/payments-hardening-starter.yaml
Normal file
@@ -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
|
||||||
30
lab/pod-security-standards-answer.yaml
Normal file
30
lab/pod-security-standards-answer.yaml
Normal file
@@ -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
|
||||||
17
lab/pod-security-standards-starter.yaml
Normal file
17
lab/pod-security-standards-starter.yaml
Normal file
@@ -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"]
|
||||||
28
lab/rbac-answer.yaml
Normal file
28
lab/rbac-answer.yaml
Normal file
@@ -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
|
||||||
10
lab/rbac-starter.yaml
Normal file
10
lab/rbac-starter.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: cks-rbac
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: report-reader
|
||||||
|
namespace: cks-rbac
|
||||||
25
lab/serviceaccount-token-answer.yaml
Normal file
25
lab/serviceaccount-token-answer.yaml
Normal file
@@ -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"]
|
||||||
26
lab/workload-hardening-answer.yaml
Normal file
26
lab/workload-hardening-answer.yaml
Normal file
@@ -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"]
|
||||||
22
lab/workload-hardening-starter.yaml
Normal file
22
lab/workload-hardening-starter.yaml
Normal file
@@ -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"]
|
||||||
3
learning-records/0001-existing-kubernetes-foundation.md
Normal file
3
learning-records/0001-existing-kubernetes-foundation.md
Normal file
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
44
learning-records/0004-retrieval-profile-through-lesson-13.md
Normal file
44
learning-records/0004-retrieval-profile-through-lesson-13.md
Normal file
@@ -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.
|
||||||
1
lessons/0001-security-control-selection.html
Normal file
1
lessons/0001-security-control-selection.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 1: choose the control</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0001 · 15 minutes</p><h1>Choose the security control before you touch YAML</h1><p class="lede">A CKS task often feels large because several controls sound plausible. Your first win is narrowing the problem to the one mechanism that can enforce the stated outcome.</p><div class="callout"><strong>Mission connection:</strong> a security engineer must choose a control that is both effective and scoped correctly. This is how you become faster in the exam and more credible at work.</div><h2>The control map</h2><ul><li><strong>Who may call the Kubernetes API?</strong> RBAC: Role/ClusterRole plus binding.</li><li><strong>Which pod configuration may be created?</strong> Pod Security Admission or another admission policy.</li><li><strong>Which packets may flow?</strong> NetworkPolicy—provided the CNI enforces it.</li><li><strong>What may the process do inside its container?</strong> <code>securityContext</code>, capabilities, seccomp, AppArmor/SELinux.</li><li><strong>What happened through the API?</strong> Audit logging.</li></ul><p>This map follows the official <a href="https://kubernetes.io/docs/concepts/security/">Kubernetes security overview</a>. The practical exam is performance-based and currently runs for two hours; use the <a href="https://training.linuxfoundation.org/certification/certified-kubernetes-security-specialist/">Linux Foundation page</a> as the authority for changing logistics.</p><h2>Practice: classify first, configure second</h2><div class="quiz"><h3>Requirement</h3><p>“In namespace <code>payments</code>, only pods labelled <code>app=api</code> may reach TCP 5432 on pods labelled <code>app=db</code>. Everything else should remain denied.”</p><p>What is the primary control?</p><label><input type="radio" name="q" value="rbac"> RBAC Role and RoleBinding</label><label><input type="radio" name="q" value="psa"> Pod Security Admission labels</label><label><input type="radio" name="q" value="network"> NetworkPolicy with a selected ingress rule</label><label><input type="radio" name="q" value="audit"> Kubernetes audit policy</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Deliberate drill (10 minutes)</h2><ol><li>Without looking up syntax, write a NetworkPolicy that selects <code>app=db</code>, permits TCP 5432 only from <code>app=api</code>, and applies in <code>payments</code>.</li><li>Validate it: confirm the selectors, port, protocol, and namespace; then test an allowed and a disallowed source if your lab supports it.</li><li>Compare with the official <a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">NetworkPolicy documentation</a>. Add the first thing you missed to your error log.</li></ol><p>When you finish, paste your manifest or tell me what felt uncertain. I’ll review it as your teacher and choose the next just-right drill.</p><footer>Next reference: <a href="../reference/cks-four-week-field-guide.html">CKS four-week field guide</a></footer></main><script>function check(){const chosen=document.querySelector('input[name=q]:checked');const f=document.getElementById('feedback');if(!chosen){f.textContent='Choose one control first.';f.className='feedback bad';return;}if(chosen.value==='network'){f.textContent='Correct. This is a traffic-flow requirement; NetworkPolicy expresses the allowed ingress precisely.';f.className='feedback good';}else{f.textContent='Not this time. The requirement controls pod-to-pod network traffic, so start with NetworkPolicy.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0002-networkpolicy-verification.html
Normal file
1
lessons/0002-networkpolicy-verification.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 2: verify NetworkPolicy</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0002 · 20 minutes</p><h1>NetworkPolicy: prove what is denied</h1><p class="lede">Using documentation is a strength, not a weakness. In CKS and in real operations, the differentiator is proving that a policy has the intended effect—not merely applying YAML without errors.</p><div class="callout"><strong>Confidence move:</strong> after every security change, state one traffic flow that must work and one that must fail. Then test both.</div><h2>The verification sequence</h2><ol><li><strong>Confirm selection.</strong> <code>kubectl get pods -n payments --show-labels</code>. A correct rule selecting the wrong labels does nothing useful.</li><li><strong>Confirm policy type and direction.</strong> Read <code>spec.policyTypes</code>: ingress and egress are independent. An isolated pod may need explicit DNS egress.</li><li><strong>Test the allow path.</strong> From an <code>app=api</code> pod, connect to the database Service on TCP 5432.</li><li><strong>Test the deny path.</strong> From a deliberately different-labelled pod in the same namespace, repeat the identical connection.</li><li><strong>Check enforcement assumptions.</strong> NetworkPolicy only works when the installed CNI supports and enforces it.</li></ol><p>These mechanics are grounded in the official <a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">NetworkPolicy documentation</a>. It explicitly warns that behavior depends on the networking solution.</p><h2>Retrieval check</h2><div class="quiz"><h3>Scenario</h3><p>A policy selects <code>app=db</code> and has an ingress rule permitting <code>app=api</code> to TCP 5432. The API pod still cannot resolve <code>db.payments.svc.cluster.local</code> after you add an egress-isolating policy to it. What did you most likely omit?</p><label><input type="radio" name="q" value="rbac"> A RoleBinding for the API ServiceAccount</label><label><input type="radio" name="q" value="dns"> An egress allowance to cluster DNS on port 53</label><label><input type="radio" name="q" value="psa"> A restricted Pod Security Admission label</label><label><input type="radio" name="q" value="audit"> An audit-policy rule for DNS queries</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Timed drill: build a test matrix (12 minutes)</h2><p>For the policy you wrote in Lesson 1, make this table in a note before touching the cluster. Then run every row your lab permits.</p><table><thead><tr><th>Source</th><th>Destination</th><th>Expected</th><th>Observed</th></tr></thead><tbody><tr><td><code>app=api</code></td><td><code>app=db:5432</code></td><td>allowed</td><td></td></tr><tr><td>other labelled pod</td><td><code>app=db:5432</code></td><td>denied</td><td></td></tr><tr><td><code>app=api</code></td><td>cluster DNS :53</td><td>allowed if egress isolated</td><td></td></tr></tbody></table><p>Write one command you used to test each row and record any mismatch. Then ask me with the result; I’ll help you diagnose it, as you would in a security incident.</p><footer>Review: <a href="../reference/cks-four-week-field-guide.html">CKS four-week field guide</a> · Next: RBAC scope and escalation paths.</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose one answer.';f.className='feedback bad';return;}if(c.value==='dns'){f.textContent='Correct. Egress isolation can block DNS; allow the required DNS destination and port explicitly.';f.className='feedback good';}else{f.textContent='Not quite. The symptom is name resolution after egress isolation, so inspect DNS traffic first.';f.className='feedback bad';}}</script></body></html>
|
||||||
8
lessons/0003-rbac-least-privilege.html
Normal file
8
lessons/0003-rbac-least-privilege.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 3: RBAC least privilege</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0003 · 20 minutes</p><h1>RBAC: grant the minimum—and prove the boundary</h1><p class="lede">Security work is not “make it work.” It is “make the required action work while every unnecessary action stays blocked.”</p><div class="callout"><strong>Career connection:</strong> least-privilege reviews are a routine platform-security responsibility. The fastest path is always: identify subject → scope → resource → verb → verification.</div><h2>Timed task: 12 minutes</h2><p>In namespace <code>cks-rbac</code>, grant ServiceAccount <code>report-reader</code> permission to <strong>get</strong> and <strong>list</strong> only <code>configmaps</code>. It must <strong>not</strong> create ConfigMaps or read Secrets. Use a namespace-scoped Role and RoleBinding.</p><ol><li>Set up only the challenge objects: <code>kubectl --context kind-cks-lab apply -f .\lab\rbac-starter.yaml</code>.</li><li>Create your answer in <code>lab/rbac-answer.yaml</code>. Use the official <a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">RBAC reference</a>—documentation use is expected.</li><li>Apply it to the isolated lab.</li><li>Run the four verification commands below. Do not stop after the first success.</li></ol><h2>Proof commands</h2><pre><code>kubectl --context kind-cks-lab auth can-i get configmaps \
|
||||||
|
-n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader
|
||||||
|
kubectl --context kind-cks-lab auth can-i list configmaps \
|
||||||
|
-n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader
|
||||||
|
kubectl --context kind-cks-lab auth can-i create configmaps \
|
||||||
|
-n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader
|
||||||
|
kubectl --context kind-cks-lab auth can-i get secrets \
|
||||||
|
-n cks-rbac --as=system:serviceaccount:cks-rbac:report-reader</code></pre><p>Expected result sequence: <strong>yes, yes, no, no</strong>.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>A requirement is confined to one namespace. Which binding usually best supports least privilege?</p><label><input type="radio" name="q" value="cluster"> ClusterRoleBinding</label><label><input type="radio" name="q" value="role"> RoleBinding in that namespace</label><label><input type="radio" name="q" value="admin"> Binding to cluster-admin</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Debrief</h2><p>Send me your four results and your YAML. I’ll review scope, subject, resource names, verbs, and whether a narrower permission is possible. That review is the feedback loop that builds confidence.</p><footer>Primary source: <a href="https://kubernetes.io/docs/concepts/security/rbac-good-practices/">Kubernetes RBAC good practices</a> · Review: <a href="../reference/cks-four-week-field-guide.html">CKS four-week field guide</a></footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='role'){f.textContent='Correct. A RoleBinding lets you scope authorization to the namespace where it is needed.';f.className='feedback good';}else{f.textContent='Not quite. Prefer namespace scope when the requirement does not need cluster-wide access.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0004-workload-hardening.html
Normal file
1
lessons/0004-workload-hardening.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 4: harden a workload</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0004 · 25 minutes</p><h1>Harden a workload without breaking it</h1><p class="lede">You already know how to write YAML. This drill trains the security-engineering question: which process-level privileges does this workload actually need?</p><div class="callout"><strong>Rule of thumb:</strong> every privilege you do not explicitly need is attack surface. Start restrictive, then add only what the workload demonstrably requires.</div><h2>The hardening controls</h2><ul><li><code>runAsNonRoot: true</code> and <code>runAsUser: 1000</code>: prevents the process from running as UID 0.</li><li><code>allowPrivilegeEscalation: false</code>: blocks gaining more privilege through mechanisms such as setuid binaries.</li><li><code>capabilities.drop: ["ALL"]</code>: removes Linux capabilities unless one is explicitly justified.</li><li><code>readOnlyRootFilesystem: true</code>: prevents writes to the image filesystem.</li><li><code>seccompProfile.type: RuntimeDefault</code>: applies the container runtime’s default syscall filter.</li></ul><p>Use the official <a href="https://kubernetes.io/docs/concepts/security/linux-kernel-security-constraints/">Linux kernel security constraints</a> and <a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/">SecurityContext task guide</a> if you need syntax or a deeper explanation.</p><h2>Timed task: 15 minutes</h2><p>The existing <code>inventory</code> pod in namespace <code>cks-workload</code> is intentionally insecure. Create <code>lab/workload-hardening-answer.yaml</code> that preserves the pod name, namespace, image, command, and label while adding every control from the list above.</p><ol><li>Inspect the current manifest: <code>kubectl --context kind-cks-lab -n cks-workload get pod inventory -o yaml</code>.</li><li>Write your replacement manifest from memory. The fast way to replace a Pod is <code>kubectl --context kind-cks-lab replace --force -f .\lab\workload-hardening-answer.yaml</code>.</li><li>Wait for it: <code>kubectl --context kind-cks-lab -n cks-workload wait --for=condition=Ready pod/inventory --timeout=60s</code>.</li><li>Verify identity: <code>kubectl --context kind-cks-lab -n cks-workload exec inventory -- id</code>. It must show a non-zero UID.</li></ol><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>A container needs no special kernel privilege. What is the secure default for Linux capabilities?</p><label><input type="radio" name="q" value="add"> Add NET_ADMIN for troubleshooting</label><label><input type="radio" name="q" value="drop"> Drop ALL capabilities</label><label><input type="radio" name="q" value="default"> Leave image defaults unchanged</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>What to send me</h2><p>Paste your YAML plus the output of <code>id</code>. I’ll verify the security context and show you one exam-speed inspection command.</p><footer>Previous: <a href="0003-rbac-least-privilege.html">RBAC least privilege</a> · Reference: <a href="../reference/cks-four-week-field-guide.html">CKS four-week field guide</a></footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='drop'){f.textContent='Correct. Drop ALL, then add back only a specifically required capability.';f.className='feedback good';}else{f.textContent='Not quite. Least privilege starts by dropping capabilities that are not explicitly required.';f.className='feedback bad';}}</script></body></html>
|
||||||
4
lessons/0005-pod-security-standards.html
Normal file
4
lessons/0005-pod-security-standards.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 5: Pod Security Standards</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0005 · 25 minutes · Minimize Microservice Vulnerabilities</p><h1>Enforce a restricted workload boundary</h1><p class="lede">A hardened Pod is useful; preventing the next insecure Pod is better. This CKS-style task combines namespace labels, admission behavior, and a compliant workload.</p><div class="callout"><strong>Exam reflex:</strong> first identify the enforcement scope. Pod Security Admission is configured with labels on the Namespace, not annotations on an individual Pod.</div><h2>Timed task: 15 minutes</h2><p>In namespace <code>cks-pss</code>, enforce the <code>restricted</code> Pod Security Standard for the Kubernetes version running in the cluster. Then make the existing <code>orders</code> Pod compliant and running. Create your answer as <code>lab/pod-security-standards-answer.yaml</code>.</p><p>Start with <code>kubectl --context kind-cks-lab apply -f .\lab\pod-security-standards-starter.yaml</code>. Your answer must add the required Namespace labels and preserve the Pod name, namespace, image, command, and label.</p><h2>Proof, not assumption</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\pod-security-standards-answer.yaml
|
||||||
|
kubectl --context kind-cks-lab -n cks-pss wait --for=condition=Ready pod/orders --timeout=60s
|
||||||
|
kubectl --context kind-cks-lab -n cks-pss get pod orders -o jsonpath='{.spec.securityContext.seccompProfile.type}{"\n"}{.spec.containers[0].securityContext}'
|
||||||
|
kubectl --context kind-cks-lab -n cks-pss run root-test --image=busybox:1.37 --restart=Never -- sh -c 'sleep 60'</code></pre><p>The final command should be rejected by admission. Remove the failed test Pod if one was created.</p><h2>Tricky retrieval check</h2><div class="quiz"><h3>Question</h3><p>A Pod has <code>spec.securityContext.runAsNonRoot: true</code>, but its only container sets <code>securityContext.runAsUser: 0</code>. In a namespace enforcing <code>restricted</code>, what should you expect?</p><label><input type="radio" name="q" value="inherit">It runs as a non-root user because the Pod-level value always wins.</label><label><input type="radio" name="q" value="reject">It is rejected because the container-level setting overrides the Pod-level setting and violates the policy.</label><label><input type="radio" name="q" value="warn">It is admitted with a warning because UID 0 is allowed under restricted.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/">Pod Security Standards</a> reference. Look up the exact Namespace label keys and restricted requirements rather than relying on memory.</p><h2>What to send me</h2><p>Paste your answer YAML plus the rejection message from <code>root-test</code>. I’ll check both the policy scope and whether your workload satisfies it.</p><footer>Previous: <a href="0004-workload-hardening.html">workload hardening</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='reject'){f.textContent='Correct. A container securityContext takes precedence where it is set. UID 0 fails the restricted runAsNonRoot requirement.';f.className='feedback good';}else{f.textContent='Not quite. Container-level securityContext values override the Pod-level values for that container; UID 0 violates restricted.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0006-read-only-root-filesystem.html
Normal file
1
lessons/0006-read-only-root-filesystem.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 6: Read-only filesystems</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0006 · 25 minutes · Minimize Microservice Vulnerabilities</p><h1>Harden a web workload without breaking its writable paths</h1><p class="lede">A read-only root filesystem limits post-compromise persistence, but applications still need deliberate places for transient state. This drill builds that boundary.</p><div class="callout"><strong>Exam reflex:</strong> when you set <code>readOnlyRootFilesystem: true</code>, identify every path the process must write to and mount only those paths as temporary writable volumes.</div><h2>Timed task: 15 minutes</h2><p>In namespace <code>payments-prod</code>, harden the <code>payments</code> Pod while keeping it available on port <code>8080</code>. Begin with <code>kubectl --context kind-cks-lab apply -f .\lab\payments-hardening-starter.yaml</code> and write the completed manifest to <code>lab/payments-hardening-answer.yaml</code>.</p><p>Preserve the Pod name, namespace, image, label, and container port. Add the following controls:</p><ul><li>Run as non-root UID <code>101</code>.</li><li>Set <code>allowPrivilegeEscalation: false</code>.</li><li>Drop all Linux capabilities.</li><li>Use the <code>RuntimeDefault</code> seccomp profile.</li><li>Set <code>readOnlyRootFilesystem: true</code>.</li><li>Mount one writable <code>emptyDir</code> volume at <code>/tmp</code>.</li></ul><p>This uses the unprivileged NGINX image, which listens on <code>8080</code>. A non-root process that has dropped all capabilities cannot generally bind port 80; changing the port is the correct secure design rather than adding <code>NET_BIND_SERVICE</code>.</p><h2>Proof, not assumption</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\payments-hardening-answer.yaml 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'</code></pre><p>The identity must have a non-zero UID, the <code>/tmp</code> check must succeed, and the final command must fail with a read-only filesystem error.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is an <code>emptyDir</code> volume usually preferable to leaving the image filesystem writable for runtime scratch data?</p><label><input type="radio" name="q" value="isolated">It limits writes to an explicit, ephemeral path while keeping the image filesystem immutable.</label><label><input type="radio" name="q" value="faster">It makes all application writes faster.</label><label><input type="radio" name="q" value="permanent">It preserves the data across Pod replacement.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/">Configure a Security Context for a Pod or Container</a> guide to confirm security-context placement and volume-mount syntax.</p><h2>What to send me</h2><p>Paste your answer YAML and the three proof outputs. I’ll check the writable boundary and show you a fast inspection command.</p><footer>Previous: <a href="0005-pod-security-standards.html">Pod Security Standards</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='isolated'){f.textContent='Correct. The writable area is explicit and ephemeral, while the rest of the image filesystem stays protected.';f.className='feedback good';}else{f.textContent='Not quite. emptyDir is useful because it narrows writable state to a defined, temporary location.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0007-serviceaccount-token-exposure.html
Normal file
1
lessons/0007-serviceaccount-token-exposure.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 7: ServiceAccount token exposure</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0007 · 20 minutes · Minimize Microservice Vulnerabilities</p><h1>Remove API credentials a workload does not need</h1><p class="lede">Every automatically mounted ServiceAccount token is a potential credential exposed through a container compromise. Disable it when the workload does not call the Kubernetes API.</p><div class="callout"><strong>Exam reflex:</strong> distinguish identity from credential mounting. A Pod can name a ServiceAccount for policy clarity while still opting out of its token volume.</div><h2>Timed task: 12 minutes</h2><p>The <code>checkout</code> Pod runs in the <code>checkout</code> namespace and does not need to call the Kubernetes API. Apply <code>lab/serviceaccount-token-starter.yaml</code>, then create <code>lab/serviceaccount-token-answer.yaml</code> that preserves its name, namespace, label, image, command, and <code>serviceAccountName</code> while preventing the token from being mounted into this Pod.</p><p>Use the smallest-scoped native control: make the change at the Pod specification, rather than changing the ServiceAccount globally.</p><h2>Proof, not assumption</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\serviceaccount-token-answer.yaml 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"}'</code></pre><p>The token-path check must return success and the final command must print <code>false</code>.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Where should you set the control when only one Pod must not receive a ServiceAccount token, while other Pods using the same ServiceAccount might still need one?</p><label><input type="radio" name="q" value="pod">On that Pod’s spec.</label><label><input type="radio" name="q" value="namespace">On the Namespace.</label><label><input type="radio" name="q" value="cluster">On the API server.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/">Configure Service Accounts for Pods</a> guide to confirm the precedence of Pod and ServiceAccount token-mount settings.</p><h2>What to send me</h2><p>Paste your answer YAML and the output of both proof commands. I’ll check the scope and the runtime credential boundary.</p><footer>Previous: <a href="0006-read-only-root-filesystem.html">Read-only root filesystem</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='pod'){f.textContent='Correct. The Pod-level field limits the change to this workload.';f.className='feedback good';}else{f.textContent='Not quite. Set the Pod-level control when the exception is specific to one workload.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0008-networkpolicy-default-deny.html
Normal file
1
lessons/0008-networkpolicy-default-deny.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 8: NetworkPolicy default deny</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0008 · 35 minutes · Minimize Microservice Vulnerabilities</p><h1>Build a default-deny network boundary from scratch</h1><p class="lede">This is intentionally closer to an exam task: the workload fixture is supplied, but no NetworkPolicy skeleton or selector hints are provided.</p><div class="callout"><strong>Exam reflex:</strong> NetworkPolicies are additive. Start by naming which traffic must survive, then express the smallest set of ingress and egress exceptions after default denial.</div><h2>Timed task: 25 minutes</h2><p>Apply <code>lab/lesson-8-fixture.yaml</code>. Then create <code>lab/lesson-8-answer.yaml</code> containing the NetworkPolicies needed to meet every requirement below. Do not change the fixture.</p><ul><li>In namespace <code>cks-net-hard</code>, default-deny both ingress and egress for every Pod.</li><li>Only the Pod labelled <code>app=checkout</code> may call the <code>ledger</code> Service on TCP port <code>8080</code>.</li><li><code>checkout</code> must be able to resolve Kubernetes Service names through cluster DNS.</li><li><code>ledger</code> must not have any egress permission.</li><li><code>intruder</code> must not be able to reach <code>ledger</code>.</li></ul><p>Your answer must use label selectors. Do not use IP blocks, and do not allow all traffic to or from the namespace.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-8-fixture.yaml 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</code></pre><p>Expected results: <code>checkout</code> prints <code>ledger</code>; <code>intruder</code> times out; <code>ledger</code> times out. The two denied commands should have a non-zero exit code.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>After a default-deny egress policy is applied to <code>checkout</code>, why can its request to <code>http://ledger:8080</code> fail before a TCP connection is attempted?</p><label><input type="radio" name="q" value="dns">The Pod also needs an egress allowance for DNS resolution.</label><label><input type="radio" name="q" value="service">Services cannot work with NetworkPolicies.</label><label><input type="radio" name="q" value="ingress">The ledger Pod needs unrestricted egress.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">Network Policies</a> reference. Inspect the CoreDNS labels in your own cluster before writing its selector.</p><h2>What to send me</h2><p>Paste only your policy YAML and the three command results. I’ll assess selector scope, DNS handling, and whether your default deny is genuinely complete.</p><footer>Previous: <a href="0007-serviceaccount-token-exposure.html">ServiceAccount token exposure</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='dns'){f.textContent='Correct. Service names require DNS first, so default-deny egress needs a specific DNS exception.';f.className='feedback good';}else{f.textContent='Not quite. The service name must first be resolved through DNS, which egress policy can block.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0009-rbac-subresources-and-resourcenames.html
Normal file
1
lessons/0009-rbac-subresources-and-resourcenames.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 9: Precise RBAC</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0009 · 35 minutes · Minimize Microservice Vulnerabilities</p><h1>Grant incident access without granting escalation paths</h1><p class="lede">A useful incident identity often needs narrow read access across a namespace. The trap is solving that requirement with a wildcard or by forgetting that logs are a separate subresource.</p><div class="callout"><strong>Exam reflex:</strong> translate every requested action into an API resource, verb, and scope. Treat <code>pods/log</code> as distinct from <code>pods</code>, and use <code>resourceNames</code> when the task names a single object.</div><h2>Timed task: 25 minutes</h2><p>Apply <code>lab/lesson-9-fixture.yaml</code>. Then create <code>lab/lesson-9-answer.yaml</code> from scratch.</p><p>In the <code>incident-response</code> namespace, grant the ServiceAccount <code>audit-bot</code> from namespace <code>security-ops</code> exactly these permissions:</p><ul><li><code>get</code>, <code>list</code>, and <code>watch</code> Pods.</li><li><code>get</code> Pod logs.</li><li><code>get</code> only the ConfigMap named <code>incident-playbook</code>.</li></ul><p>It must not be able to read Secrets, create Pods, execute into Pods, or read any other ConfigMap. Use a namespace-scoped role and binding; do not use a ClusterRole or wildcard resources/verbs.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-9-fixture.yaml 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</code></pre><p>Expected answers, in order: <code>yes yes yes yes no no no no</code>.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why does granting <code>get</code> on <code>pods</code> not automatically permit <code>kubectl logs</code>?</p><label><input type="radio" name="q" value="subresource">Logs use the distinct <code>pods/log</code> subresource.</label><label><input type="radio" name="q" value="namespace">Pod logs are always cluster-scoped.</label><label><input type="radio" name="q" value="secret">Logs require Secret access.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">RBAC reference</a>. Confirm the subject format for a cross-namespace ServiceAccount and the behavior of <code>resourceNames</code>.</p><h2>What to send me</h2><p>Paste only your Role and RoleBinding YAML, then the eight <code>can-i</code> results. I’ll review least privilege—not merely whether the allowed commands return yes.</p><footer>Previous: <a href="0008-networkpolicy-default-deny.html">NetworkPolicy default deny</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='subresource'){f.textContent='Correct. RBAC authorizes the pods/log subresource independently.';f.className='feedback good';}else{f.textContent='Not quite. Pod logs use their own API subresource, pods/log.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0010-minimize-secret-exposure.html
Normal file
1
lessons/0010-minimize-secret-exposure.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 10: Minimize Secret exposure</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0010 · 35 minutes · Minimize Microservice Vulnerabilities</p><h1>Expose only the Secret data an application needs</h1><p class="lede">Mounting a Secret is not automatically least privilege: every key becomes readable to the process. This drill combines a single-key Secret projection with the workload controls you should now retrieve from memory.</p><div class="callout"><strong>Exam reflex:</strong> secure the data path, not just the Secret object. Limit which keys are projected, where they appear, who can read them, and whether the Pod receives unrelated API credentials.</div><h2>Timed task: 25 minutes</h2><p>Apply <code>lab/lesson-10-fixture.yaml</code>. In namespace <code>billing</code>, create <code>lab/lesson-10-answer.yaml</code> from scratch containing a Pod named <code>reconciler</code>.</p><p>The Pod must use image <code>busybox:1.37</code> with command <code>sh -c 'sleep 3600'</code>, run as UID <code>1000</code>, and satisfy all requirements below:</p><ul><li>Mount only the <code>api-key</code> key from Secret <code>billing-credentials</code>.</li><li>Expose it as the file <code>/var/run/credentials/api-key</code>; the Secret’s <code>legacy-password</code> must not be present in the container.</li><li>Make the projected Secret volume read-only for the container.</li><li>Do not mount a ServiceAccount token.</li><li>Use <code>RuntimeDefault</code> seccomp, prevent privilege escalation, and drop all Linux capabilities.</li><li>Use a read-only root filesystem while still allowing the process to use <code>/tmp</code>.</li></ul><p>Do not use environment variables for the Secret and do not use wildcard-like broad mounts of the Secret volume.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-10-fixture.yaml 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'</code></pre><p>Expected: a non-zero UID, <code>test-key</code>, and success from each final two commands.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Which Secret-volume mechanism prevents an unneeded key from being mounted at all?</p><label><input type="radio" name="q" value="items">Specify the allowed key under the volume’s <code>items</code> list.</label><label><input type="radio" name="q" value="readonly">Set only <code>readOnly: true</code> on the volume mount.</label><label><input type="radio" name="q" value="mode">Set a restrictive file mode alone.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the Kubernetes <a href="https://kubernetes.io/docs/concepts/configuration/secret/">Secret</a> reference for selective key projection and the <a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/">Security Context</a> guide for the runtime controls.</p><h2>What to send me</h2><p>Paste your Pod YAML and the four proof results. I’ll inspect both the visible behavior and whether the manifest prevents the broader Secret exposure.</p><footer>Previous: <a href="0009-rbac-subresources-and-resourcenames.html">Precise RBAC</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='items'){f.textContent='Correct. items controls which Secret keys are projected into the volume.';f.className='feedback good';}else{f.textContent='Not quite. Read-only and file mode help protect mounted data, but items limits which keys appear at all.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0011-apparmor-runtime-confinement.html
Normal file
1
lessons/0011-apparmor-runtime-confinement.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 11: AppArmor confinement</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0011 · 35 minutes · System Hardening</p><h1>Apply runtime confinement to a multi-container Pod</h1><p class="lede">Linux capabilities and seccomp reduce a container’s power. AppArmor adds a path- and operation-oriented confinement layer. The difficult part is ensuring every container receives the intended profile without relying on deprecated annotations.</p><div class="callout"><strong>Exam reflex:</strong> prefer structured <code>securityContext</code> fields over legacy annotations. A container-level setting overrides the Pod-level setting, so inspect both levels when validating a multi-container workload.</div><h2>Timed task: 25 minutes</h2><p>Apply <code>lab/lesson-11-fixture.yaml</code>. Create <code>lab/lesson-11-answer.yaml</code> from scratch containing the Pod described below.</p><p>Create Pod <code>event-forwarder</code> in namespace <code>runtime-hardening</code> with two containers, both using <code>busybox:1.37</code> and command <code>sh -c 'sleep 3600'</code>:</p><ul><li>Container names: <code>forwarder</code> and <code>metrics</code>.</li><li>Both containers must receive the runtime’s default AppArmor profile. Use the current structured API field—not an AppArmor annotation.</li><li>Both containers must prevent privilege escalation and drop all capabilities.</li><li>Run the Pod as a non-root UID <code>1000</code>, with RuntimeDefault seccomp.</li><li>Do not mount a ServiceAccount token.</li><li>Use a read-only root filesystem for both containers, while allowing each to write under <code>/tmp</code>.</li><li>Do not enable host networking, host PID, host IPC, privileged mode, or hostPath volumes.</li></ul><p>You decide whether a Pod-level or container-level field is the most concise correct way to satisfy the AppArmor requirement. Your manifest must not contain a conflicting override.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-11-fixture.yaml 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"}'</code></pre><p>Both containers must be Ready, run as a non-zero UID, and write successfully to <code>/tmp</code>. If you set AppArmor at container level, inspect the equivalent container field instead of expecting the final command’s first value.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>A Pod-level AppArmor profile is <code>RuntimeDefault</code>, but one container sets its own profile to <code>Unconfined</code>. Which profile applies to that container?</p><label><input type="radio" name="q" value="container">The container-level <code>Unconfined</code> profile, because it overrides the Pod-level setting.</label><label><input type="radio" name="q" value="pod">The Pod-level RuntimeDefault profile.</label><label><input type="radio" name="q" value="merge">Both profiles merge into a stricter profile.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://kubernetes.io/docs/tutorials/security/apparmor/">AppArmor tutorial</a> and inspect the API schema with <code>kubectl explain pod.spec.securityContext.appArmorProfile</code>.</p><h2>What to send me</h2><p>Paste your Pod YAML and the proof outputs. I’ll inspect effective profile scope as well as the usual workload-hardening controls.</p><footer>Previous: <a href="0010-minimize-secret-exposure.html">Minimize Secret exposure</a> · CKS domain: System Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='container'){f.textContent='Correct. A container-level AppArmor profile overrides the Pod-level profile for that container.';f.className='feedback good';}else{f.textContent='Not quite. The explicit container-level profile takes precedence for that container.';f.className='feedback bad';}}</script></body></html>
|
||||||
4
lessons/0012-audit-log-investigation.html
Normal file
4
lessons/0012-audit-log-investigation.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 12: Audit investigation</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0012 · 30 minutes · Monitoring, Logging, and Runtime Security</p><h1>Reconstruct an RBAC escalation from audit evidence</h1><p class="lede">Audit logs record API activity, not intent. Your job is to filter noise, correlate a principal and source address, and identify the attack sequence with evidence.</p><div class="callout"><strong>Incident reflex:</strong> begin with high-impact API resources and verbs—RBAC writes, Secret reads, and exec activity—then pivot by username, source IP, and timestamps.</div><h2>Timed task: 20 minutes</h2><p>Analyze <code>lab/lesson-12-audit.jsonl</code>. Do not edit the file. Send me a concise incident note answering all of these:</p><ol><li>Which principal is suspicious?</li><li>What source IP is associated with the activity?</li><li>What is the earliest suspicious timestamp?</li><li>Which ClusterRoleBinding was created?</li><li>Which Secret was accessed?</li><li>What runtime action followed the Secret access?</li><li>Give two immediate containment actions, in order.</li></ol><p>Use PowerShell, <code>jq</code>, or another structured JSON tool. Do not rely on free-text visual scanning alone.</p><h2>Useful investigation shapes</h2><pre><code>Get-Content .\lab\lesson-12-audit.jsonl | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object { $_.objectRef.resource -in 'clusterrolebindings','secrets' } | Select-Object requestReceivedTimestamp, verb, user, sourceIPs, objectRef
|
||||||
|
|
||||||
|
# Linux exam-style equivalent
|
||||||
|
jq -c 'select(.objectRef.resource == "clusterrolebindings" or .objectRef.resource == "secrets")' lesson-12-audit.jsonl</code></pre><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Which audit event is the strongest initial indicator of privilege escalation?</p><label><input type="radio" name="q" value="binding">Creating a ClusterRoleBinding.</label><label><input type="radio" name="q" value="service">Reading a Service.</label><label><input type="radio" name="q" value="pod">Creating an ordinary application Pod.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/">auditing</a> documentation to review audit stages, levels, and policy behavior.</p><h2>What to send me</h2><p>Send your seven answers and the exact filter command you used. I’ll check the timeline and the containment priorities.</p><footer>Previous: <a href="0011-apparmor-runtime-confinement.html">AppArmor runtime confinement</a> · CKS domain: Monitoring, Logging, and Runtime Security</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='binding'){f.textContent='Correct. Creating a ClusterRoleBinding can grant broad authority and warrants immediate investigation.';f.className='feedback good';}else{f.textContent='Not quite. An RBAC binding write is a high-impact control-plane action and a strong escalation signal.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0013-image-digest-admission-policy.html
Normal file
1
lessons/0013-image-digest-admission-policy.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 13: Image digest admission</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0013 · 45 minutes · Supply Chain Security</p><h1>Enforce immutable container images at admission</h1><p class="lede">A tag can be moved after review; a digest identifies immutable content. This drill combines workload remediation with a native admission control that blocks the next mutable image.</p><div class="callout"><strong>Exam reflex:</strong> distinguish detecting an unsafe image from preventing its reintroduction. First remediate the workload; then enforce the intended rule at the narrowest practical scope.</div><h2>Timed task: 35 minutes</h2><p>Apply <code>lab/lesson-13-fixture.yaml</code>. Create <code>lab/lesson-13-answer.yaml</code> from scratch. Do not alter the fixture.</p><p>Your answer must contain all resources needed to meet these requirements:</p><ul><li>Update Deployment <code>frontend</code> so its container image is an immutable digest reference from the official Docker Hub library. Preserve its name, namespace, replica count, labels, container name, and command.</li><li>Create a native <code>ValidatingAdmissionPolicy</code> and <code>ValidatingAdmissionPolicyBinding</code>.</li><li>Within only the <code>supply-chain</code> namespace, reject Pod creates and updates unless every regular container image starts with <code>docker.io/library/</code> and contains an image digest (<code>@sha256:</code>).</li><li>The policy must deny invalid workloads with a helpful message. Do not use a webhook, Kyverno, or a cluster-wide binding.</li></ul><p>Retrieve the digest from the running fixture Pod rather than guessing it: inspect <code>.status.containerStatuses[0].imageID</code>. The admission expression must validate all containers, not merely the first one.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-13-fixture.yaml 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</code></pre><p>Expected: the rollout succeeds; <code>mutable-test</code> is rejected by admission; <code>outside-scope</code> is admitted because the binding is limited to <code>supply-chain</code>. Remove <code>outside-scope</code> after testing.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is <code>busybox:1.37</code> weaker supply-chain evidence than <code>docker.io/library/busybox@sha256:…</code>?</p><label><input type="radio" name="q" value="digest">A tag can be repointed, while a digest identifies immutable image content.</label><label><input type="radio" name="q" value="size">A digest always makes an image smaller.</label><label><input type="radio" name="q" value="network">A digest removes the need for registry access.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the Kubernetes <a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/">Validating Admission Policy</a> reference and inspect the schema locally with <code>kubectl explain validatingadmissionpolicy.spec.validations</code>.</p><h2>What to send me</h2><p>Paste your policy, binding, and Deployment update, followed by the two admission outcomes. I’ll evaluate the CEL logic, namespace scope, and image-reference enforcement.</p><footer>Previous: <a href="0012-audit-log-investigation.html">Audit log investigation</a> · CKS domain: Supply Chain Security</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='digest'){f.textContent='Correct. Tags are mutable references; digests identify a specific immutable manifest.';f.className='feedback good';}else{f.textContent='Not quite. The key difference is immutability and traceability, not image size or network access.';f.className='feedback bad';}}</script></body></html>
|
||||||
4
lessons/0014-kubernetes-audit-policy.html
Normal file
4
lessons/0014-kubernetes-audit-policy.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 14: Audit policy</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0014 · 35 minutes · Monitoring, Logging, and Runtime Security</p><h1>Write an audit policy that captures escalation evidence</h1><p class="lede">Audit logs are only useful when the policy records enough detail for high-risk actions without turning every request into expensive noise. This is a from-scratch configuration task—there is no starter YAML.</p><div class="callout"><strong>Exam reflex:</strong> audit-policy rules are first-match wins. Put the most specific, high-value rules first and end with a deliberate catch-all.</div><h2>Timed task: 25 minutes</h2><p>Create <code>lab/lesson-14-audit-policy.yaml</code>. It must be a valid Kubernetes audit policy that meets every requirement:</p><ul><li>Use <code>audit.k8s.io/v1</code> and kind <code>Policy</code>.</li><li>Never record the <code>RequestReceived</code> stage.</li><li>Record at <code>RequestResponse</code> level for all Secret requests in namespace <code>payments</code>.</li><li>Record at <code>Metadata</code> level for <code>create</code>, <code>update</code>, <code>patch</code>, <code>delete</code>, and <code>deletecollection</code> operations on Roles, RoleBindings, ClusterRoles, and ClusterRoleBindings, regardless of namespace.</li><li>Record every other request at <code>Metadata</code> level.</li></ul><p>Do not use <code>None</code> to hide any requests beyond the omitted stage. Do not place the catch-all rule above the specific rules.</p><h2>Validation</h2><p>An audit policy is an API-server configuration file, not a Kubernetes object. <code>kubectl apply</code> cannot validate or install it. Check its YAML syntax with your editor, then inspect the rule ordering manually. In a real cluster, the API server must be started with <code>--audit-policy-file</code> pointing to this file.</p><pre><code>Get-Content .\lab\lesson-14-audit-policy.yaml
|
||||||
|
|
||||||
|
# On a Linux control-plane node, examine the API-server manifest/configuration:
|
||||||
|
grep -R -- '--audit-policy-file' /etc/kubernetes/manifests /etc/kubernetes 2>/dev/null</code></pre><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>A catch-all <code>Metadata</code> rule is first in the rule list, followed by a Secret <code>RequestResponse</code> rule. What level is recorded for a Secret request?</p><label><input type="radio" name="q" value="metadata">Metadata, because the first matching audit-policy rule wins.</label><label><input type="radio" name="q" value="response">RequestResponse, because it is more specific.</label><label><input type="radio" name="q" value="both">Both levels are recorded.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/">auditing</a> documentation. Confirm the resource-group names for RBAC objects.</p><h2>What to send me</h2><p>Paste the policy YAML. I’ll evaluate ordering, resource matching, verb coverage, and whether it records the necessary response bodies.</p><footer>Previous: <a href="0013-image-digest-admission-policy.html">Image digest admission</a> · CKS domain: Monitoring, Logging, and Runtime Security</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='metadata'){f.textContent='Correct. Audit-policy rules are evaluated in order; the first match determines the result.';f.className='feedback good';}else{f.textContent='Not quite. The first matching rule wins, so ordering is part of the security design.';f.className='feedback bad';}}</script></body></html>
|
||||||
2
lessons/0015-kubelet-endpoint-hardening.html
Normal file
2
lessons/0015-kubelet-endpoint-hardening.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 15: Kubelet endpoint hardening</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0015 · 35 minutes · Cluster Setup and System Hardening</p><h1>Remove anonymous access to the kubelet</h1><p class="lede">The kubelet is a node-level control point. An exposed read-only or anonymously accessible endpoint can reveal workload and node information, so secure its authentication and authorization before investigating higher-level policy.</p><div class="callout"><strong>Exam reflex:</strong> distinguish kubelet authentication from authorization. Disabling anonymous access does not make authorization safe by itself; both controls must be configured.</div><h2>Timed task: 25 minutes</h2><p>Review <code>lab/lesson-15-kubelet-config-insecure.yaml</code>. Create a complete replacement configuration as <code>lab/lesson-15-kubelet-config-answer.yaml</code>.</p><p>Preserve the API version and kind. Your completed configuration must:</p><ul><li>Reject unauthenticated kubelet requests.</li><li>Use webhook authentication and webhook authorization for authenticated API requests.</li><li>Disable the legacy read-only kubelet port.</li><li>Protect kernel defaults.</li><li>Enable kubelet client-certificate rotation.</li></ul><p>Do not add unrelated fields or change API versions. This is a node configuration artifact; do not run <code>kubectl apply</code> against it.</p><h2>Verification</h2><pre><code>Get-Content .\lab\lesson-15-kubelet-config-answer.yaml
|
||||||
|
</code></pre><p>The completed configuration should leave nothing listening on TCP 10255. In a real cluster, deploy it through the node’s kubelet configuration-management path and restart the kubelet only under the approved maintenance procedure.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is setting <code>authorization.mode: Webhook</code> insufficient if <code>authentication.anonymous.enabled</code> remains true?</p><label><input type="radio" name="q" value="anonymous">Anonymous requests can still reach the kubelet and rely on whatever authorization is granted to the anonymous identity.</label><label><input type="radio" name="q" value="port">Webhook authorization automatically closes port 10255.</label><label><input type="radio" name="q" value="cert">Authorization automatically rotates client certificates.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/">KubeletConfiguration API reference</a> and the kubelet authentication/authorization documentation.</p><h2>What to send me</h2><p>Paste the replacement configuration. I’ll check every required field, then explain how the settings combine to secure the endpoint.</p><footer>Previous: <a href="0014-kubernetes-audit-policy.html">Kubernetes audit policy</a> · CKS domains: Cluster Setup and System Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='anonymous'){f.textContent='Correct. Authentication decides whether and as whom the request enters; authorization then evaluates that identity.';f.className='feedback good';}else{f.textContent='Not quite. Authentication and authorization are separate controls; configure both.';f.className='feedback bad';}}</script></body></html>
|
||||||
3
lessons/0016-tls-ingress.html
Normal file
3
lessons/0016-tls-ingress.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 16: TLS Ingress</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0016 · 30 minutes · Cluster Setup</p><h1>Route a service through a TLS-enabled Ingress</h1><p class="lede">Ingress TLS is a three-part relationship: the hostname in the rule, the hostname in the TLS section, and the Secret that holds the certificate. A correct backend alone does not secure the public route.</p><div class="callout"><strong>Exam reflex:</strong> use <code>networking.k8s.io/v1</code>. In every HTTP path, specify <code>pathType</code> and the Service backend name and port explicitly.</div><h2>Timed task: 20 minutes</h2><p>Apply <code>lab/lesson-16-fixture.yaml</code>. Create <code>lab/lesson-16-answer.yaml</code> from scratch with a single Ingress in namespace <code>ingress-drill</code>.</p><p>The Ingress must meet all requirements:</p><ul><li>Name: <code>orders</code>; API version: <code>networking.k8s.io/v1</code>.</li><li>Use ingress class <code>nginx</code>.</li><li>Serve only host <code>orders.training.local</code>.</li><li>Configure TLS for that same host using Secret <code>orders-tls</code>.</li><li>Route <code>/api</code> with <code>Prefix</code> path type to Service <code>orders</code> on port <code>8080</code>.</li><li>Route <code>/healthz</code> with <code>Exact</code> path type to the same Service and port.</li></ul><p>Do not add a wildcard host, default backend, or controller-specific annotations. The supplied cluster has no Ingress controller, so this drill validates the API manifest and routing intent rather than an external HTTPS request.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-16-fixture.yaml
|
||||||
|
kubectl --context kind-cks-lab apply --dry-run=server -f .\lab\lesson-16-answer.yaml
|
||||||
|
kubectl --context kind-cks-lab -n ingress-drill get ingress orders -o yaml</code></pre><p>After server-side dry run succeeds, apply the answer manifest for inspection. Do not expect an address or TLS handshake without an Ingress controller.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>What must match for the TLS host in an Ingress to cover the public route?</p><label><input type="radio" name="q" value="host">The hostname in <code>spec.tls.hosts</code> must match the hostname in the Ingress rule.</label><label><input type="radio" name="q" value="path">The TLS Secret name must match the backend Service name.</label><label><input type="radio" name="q" value="port">The TLS host must include the backend Service port.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/">Ingress</a> reference to verify the v1 backend and TLS schema.</p><h2>What to send me</h2><p>Paste the Ingress YAML and the server-side dry-run result. I’ll check host/TLS alignment, path semantics, and backend syntax.</p><footer>Previous: <a href="0015-kubelet-endpoint-hardening.html">Kubelet endpoint hardening</a> · CKS domain: Cluster Setup</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='host'){f.textContent='Correct. The TLS section and rule host must align for the certificate to cover the requested hostname.';f.className='feedback good';}else{f.textContent='Not quite. TLS applies to the requested hostname, so the TLS and rule hosts must match.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0017-tenant-isolation-admission.html
Normal file
1
lessons/0017-tenant-isolation-admission.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 17: Tenant isolation admission</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0017 · 45 minutes · Minimize Microservice Vulnerabilities</p><h1>Block host-escape settings before scheduling</h1><p class="lede">Namespace isolation is weak if a tenant can request host namespaces, host networking, privileged containers, or hostPath volumes. This drill uses native CEL admission controls to make those settings unrepresentable in one tenant namespace.</p><div class="callout"><strong>Exam reflex:</strong> protect optional fields with CEL <code>has(...)</code> checks. A policy must handle an omitted securityContext as safely as an explicit <code>false</code>.</div><h2>Timed task: 35 minutes</h2><p>Apply <code>lab/lesson-17-fixture.yaml</code>. Create <code>lab/lesson-17-answer.yaml</code> from scratch containing a <code>ValidatingAdmissionPolicy</code> and <code>ValidatingAdmissionPolicyBinding</code>.</p><p>Only in namespace <code>tenant-a</code>, deny Pod creates and updates when any of the following is true:</p><ul><li><code>hostNetwork</code>, <code>hostPID</code>, or <code>hostIPC</code> is true.</li><li>Any volume is a <code>hostPath</code> volume.</li><li>Any regular container is privileged.</li></ul><p>The policy must allow a normal unprivileged Pod that omits all of these fields. Use <code>failurePolicy: Fail</code>, a useful denial message, and <code>Deny</code> in the binding. Scope the binding with the supplied namespace label; do not apply it cluster-wide or use a webhook.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-17-fixture.yaml 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'</code></pre><p>Expected: <code>normal</code> is admitted, <code>host-net</code> is rejected, and <code>host-net-outside</code> is admitted. Clean up the two admitted test Pods afterward. Also inspect your CEL carefully: the commands prove host networking, while the policy must cover all four listed controls.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is a plain expression such as <code>c.securityContext.privileged == false</code> unsafe for this task?</p><label><input type="radio" name="q" value="optional">A container can omit <code>securityContext</code>; the expression must handle optional fields without an evaluation error.</label><label><input type="radio" name="q" value="scope">Privileged is a namespace-level field.</label><label><input type="radio" name="q" value="port">Privileged containers always require a host port.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the Kubernetes <a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/">Validating Admission Policy</a> reference and inspect CEL field availability with <code>kubectl explain pod.spec</code>.</p><h2>What to send me</h2><p>Paste the policy and binding, plus the three admission outcomes. I’ll inspect scope, optional-field handling, and every escape path.</p><footer>Previous: <a href="0016-tls-ingress.html">TLS Ingress</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='optional'){f.textContent='Correct. CEL expressions must explicitly handle fields that are optional in the Kubernetes API.';f.className='feedback good';}else{f.textContent='Not quite. securityContext can be absent, so a robust policy must account for it.';f.className='feedback bad';}}</script></body></html>
|
||||||
9
lessons/0018-secrets-encryption-at-rest.html
Normal file
9
lessons/0018-secrets-encryption-at-rest.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 18: Encrypt Secrets at rest</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0018 · 40 minutes · Cluster Hardening</p><h1>Configure encryption before data reaches etcd</h1><p class="lede">Kubernetes Secrets are base64-encoded in API responses, not encrypted by default in etcd. Encryption at rest is controlled by an API-server configuration file and provider order matters.</p><div class="callout"><strong>Exam reflex:</strong> this is an API-server configuration, not a Kubernetes object. Do not use <code>kubectl apply</code> for the encryption configuration. First providers encrypt new writes; <code>identity</code> is a fallback and belongs last.</div><h2>Timed task: 30 minutes</h2><p>Apply <code>lab/lesson-18-fixture.yaml</code> only to create the sample Secret. Then write <code>lab/lesson-18-encryption-config.yaml</code> from scratch.</p><p>Your configuration must:</p><ul><li>Use <code>apiserver.config.k8s.io/v1</code> and kind <code>EncryptionConfiguration</code>.</li><li>Encrypt only the core <code>secrets</code> resource.</li><li>Use <code>aescbc</code> as the first provider, with one key named <code>key1</code>.</li><li>Use a random, valid 32-byte key encoded in base64.</li><li>Include <code>identity</code> only as the final provider.</li></ul><p>Do not place <code>identity</code> first, do not use a plaintext key, and do not add an etcd object or Secret to store this configuration.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-18-fixture.yaml
|
||||||
|
|
||||||
|
# Generate a valid 32-byte base64 key in PowerShell:
|
||||||
|
$bytes = [byte[]]::new(32)
|
||||||
|
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
|
||||||
|
[Convert]::ToBase64String($bytes)
|
||||||
|
|
||||||
|
# Check your generated key decodes to 32 bytes:
|
||||||
|
[Convert]::FromBase64String('<your-key>').Length</code></pre><p>In a production control plane, place the completed file securely on every API-server node, configure <code>--encryption-provider-config</code>, restart one API server at a time, then rewrite existing Secrets so they are stored with the new provider.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why must <code>identity</code> be last in the provider list?</p><label><input type="radio" name="q" value="order">The first provider encrypts new writes; placing identity first would keep new data unencrypted.</label><label><input type="radio" name="q" value="speed">Identity is slower than AES-CBC.</label><label><input type="radio" name="q" value="api">Identity is only valid for non-core API resources.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/">Encrypting Confidential Data at Rest</a> guide. Review the operational steps for rotating keys and rewriting already-stored objects.</p><h2>What to send me</h2><p>Paste the configuration only—not the actual production key. I’ll validate provider order, resource scope, and base64 key length in this local training file.</p><footer>Previous: <a href="0017-tenant-isolation-admission.html">Tenant isolation admission</a> · CKS domain: Cluster Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='order'){f.textContent='Correct. Provider order controls how new data is written; identity is a compatibility fallback.';f.className='feedback good';}else{f.textContent='Not quite. Encryption providers are evaluated in order, and the first one writes new data.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0019-platform-binary-verification.html
Normal file
1
lessons/0019-platform-binary-verification.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 19: Binary verification</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0019 · 25 minutes · Cluster Setup</p><h1>Verify a platform binary before deployment</h1><p class="lede">A checksum does not prove who produced an artifact, but it detects whether the bytes match a checksum obtained through a trusted channel. Treat a mismatch as a release blocker, not a warning to work around.</p><div class="callout"><strong>Exam reflex:</strong> verify the artifact before copying it into a privileged path, making it executable, or restarting a control-plane component. A failed checksum means stop and investigate.</div><h2>Timed task: 15 minutes</h2><p>In <code>lab/lesson-19-artifacts</code>, the trusted-release team supplied <code>SHA256SUMS</code> separately from the two received artifacts. Verify both artifacts using a Linux checksum command. Then create <code>lab/lesson-19-answer.md</code> containing:</p><ol><li>The exact verification command you ran.</li><li>Which artifact is verified and may proceed to the next deployment-review stage.</li><li>Which artifact must not be deployed.</li><li>Two appropriate next actions for the mismatched artifact.</li></ol><p>Do not edit either artifact or the checksum file. The artifacts are small training stand-ins, not real Kubernetes executables.</p><h2>Verification</h2><pre><code>cd lab/lesson-19-artifacts sha256sum -c SHA256SUMS # PowerShell equivalent for a single artifact: Get-FileHash .\kubelet-v1.35.0-linux-amd64 -Algorithm SHA256</code></pre><p>One result should be <code>OK</code>; one should be <code>FAILED</code>. A command can return non-zero when any verification fails—that non-zero result is expected evidence, not a reason to skip the check.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>What is the correct immediate response to a checksum mismatch for a proposed kubelet binary?</p><label><input type="radio" name="q" value="stop">Do not deploy it; quarantine the artifact and obtain or verify a trusted replacement.</label><label><input type="radio" name="q" value="retry">Install it, then re-download the checksum file later.</label><label><input type="radio" name="q" value="ignore">Ignore the mismatch if the version tag is correct.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Review the official Kubernetes release process and the security guidance for obtaining release artifacts. In production, pair checksums with signed provenance and trusted release channels.</p><h2>What to send me</h2><p>Send the four requested items from your Markdown answer. I’ll check your operational decision, not just the command syntax.</p><footer>Previous: <a href="0018-secrets-encryption-at-rest.html">Secrets encryption at rest</a> · CKS domain: Cluster Setup</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='stop'){f.textContent='Correct. A mismatch is a supply-chain incident signal; fail closed and re-establish artifact trust.';f.className='feedback good';}else{f.textContent='Not quite. Integrity verification must fail closed; do not deploy an artifact whose bytes do not match trusted evidence.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0020-runtime-shell-detection.html
Normal file
1
lessons/0020-runtime-shell-detection.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 20: Runtime shell detection</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0020 · 35 minutes · Monitoring, Logging, and Runtime Security</p><h1>Detect an interactive shell in a production container</h1><p class="lede">Runtime detection turns suspicious behavior into an actionable alert. A shell inside a workload is not automatically malicious, but in a defined production scope it deserves timely investigation—especially when it touches mounted credentials.</p><div class="callout"><strong>Incident reflex:</strong> make detection rules precise enough to reduce noise, but include enough event context in the alert that responders can identify the workload without re-running the search.</div><h2>Timed task: 25 minutes</h2><p>Review <code>lab/lesson-20-runtime-event.json</code>. Create <code>lab/lesson-20-falco-rule.yaml</code> from scratch containing one custom Falco rule.</p><p>The rule must:</p><ul><li>Use source <code>syscall</code>.</li><li>Match a process named <code>sh</code> or <code>bash</code> only when it runs in a container in Kubernetes namespace <code>payments</code>.</li><li>Use priority <code>WARNING</code>.</li><li>Include the process command line, Kubernetes namespace, Pod name, and container name in its output.</li><li>Include tags <code>container</code> and <code>mitre_execution</code>.</li></ul><p>Write the condition so it does not match a host-level shell or a shell in another namespace. Do not suppress the alert because the process is running as root; identity is evidence for triage, not a reason to skip detection.</p><h2>Evidence and response</h2><p>In <code>lab/lesson-20-answer.md</code>, give:</p><ol><li>The exact suspicious command from the supplied event.</li><li>The affected namespace, Pod, and container.</li><li>Two immediate containment actions in priority order.</li></ol><p>This environment does not run Falco, so validate the YAML structure in your editor. On a Falco-equipped Linux node, use <code>falco --validate /path/to/rule.yaml</code> before reloading the rules.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Which condition component prevents a rule from alerting on every shell launched directly on a Kubernetes node?</p><label><input type="radio" name="q" value="container">The <code>container</code> condition.</label><label><input type="radio" name="q" value="process">The <code>proc.name</code> condition.</label><label><input type="radio" name="q" value="priority">The <code>priority</code> field.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official <a href="https://falco.org/docs/reference/rules/">Falco rules reference</a> to confirm condition syntax and available output fields.</p><h2>What to send me</h2><p>Paste the rule and your three incident answers. I’ll assess false-positive scope, alert context, and containment quality.</p><footer>Previous: <a href="0019-platform-binary-verification.html">Platform binary verification</a> · CKS domain: Monitoring, Logging, and Runtime Security</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='container'){f.textContent='Correct. container limits the rule to containerized processes instead of host-level activity.';f.className='feedback good';}else{f.textContent='Not quite. proc.name matches host processes too; container scopes the event to a container.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0021-pod-security-admission-modes.html
Normal file
1
lessons/0021-pod-security-admission-modes.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 21: Pod Security Admission modes</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0021 · 40 minutes · Minimize Microservice Vulnerabilities</p><h1>Introduce restricted policy without losing visibility</h1><p class="lede">Pod Security Admission has three independent modes: enforce blocks a workload, audit records violations, and warn informs the requesting user. Mature rollouts often use all three during a migration.</p><div class="callout"><strong>Exam reflex:</strong> these are Namespace labels. Each mode needs its own label key and can use a different target level or version. Enforcement alone is not the whole rollout story.</div><h2>Timed task: 30 minutes</h2><p>Apply <code>lab/lesson-21-fixture.yaml</code>. Create <code>lab/lesson-21-answer.yaml</code> from scratch. Do not change the fixture.</p><p>Deliver a safe migration for namespace <code>partner-workloads</code>:</p><ul><li>Existing Pods must remain running.</li><li>New or updated Pods must be rejected unless they meet the <code>restricted</code> Pod Security Standard.</li><li>Violations must also be visible through audit and user-facing warnings at <code>restricted</code>.</li><li>Pin every mode to the Kubernetes minor version running in this cluster.</li><li>Create a compliant replacement Pod named <code>exporter-v2</code> in the same namespace, using image <code>busybox:1.37</code>, command <code>sh -c 'sleep 3600'</code>, and label <code>app=exporter</code>.</li></ul><p>The replacement must run as non-root UID <code>1000</code>, use RuntimeDefault seccomp, prevent privilege escalation, and drop all capabilities. Do not mount a ServiceAccount token. No starter Pod or namespace-label YAML is provided.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab version --output=yaml 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</code></pre><p>Expected: <code>legacy-exporter</code> remains running, <code>exporter-v2</code> is Ready, and <code>rejected-test</code> is denied. The final command must show six PSA labels: enforce, audit, and warn levels plus their three version labels.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Which Pod Security Admission mode blocks a non-compliant new Pod?</p><label><input type="radio" name="q" value="enforce">Enforce.</label><label><input type="radio" name="q" value="audit">Audit.</label><label><input type="radio" name="q" value="warn">Warn.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/concepts/security/pod-security-admission/">Pod Security Admission</a> reference. Confirm the exact label names and version formatting instead of relying on memory.</p><h2>What to send me</h2><p>Paste your namespace and replacement-Pod YAML, plus the rejected admission message. I’ll check all three modes, version pinning, and workload compliance.</p><footer>Previous: <a href="0020-runtime-shell-detection.html">Runtime shell detection</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='enforce'){f.textContent='Correct. Enforce rejects non-compliant workloads; audit records and warn reports them.';f.className='feedback good';}else{f.textContent='Not quite. Only enforce prevents admission; audit and warn create visibility.';f.className='feedback bad';}}</script></body></html>
|
||||||
4
lessons/0022-kube-apiserver-hardening.html
Normal file
4
lessons/0022-kube-apiserver-hardening.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 22: API server hardening</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0022 · 45 minutes · Cluster Hardening</p><h1>Remove weak API-server access paths</h1><p class="lede">The API server is the cluster’s security boundary. A permissive flag can bypass all the careful RBAC and workload policy built above it, so secure the control-plane configuration first.</p><div class="callout"><strong>Exam reflex:</strong> inspect every API-server flag before changing it. Preserve unrelated required flags, remove or replace weak settings, and make audit logging explicit.</div><h2>Timed task: 35 minutes</h2><p>Review <code>lab/lesson-22-kube-apiserver-insecure.yaml</code>. Create <code>lab/lesson-22-kube-apiserver-answer.yaml</code> as a complete replacement static-Pod manifest.</p><p>Preserve the API version, kind, Pod name, namespace, host networking, container name, image, <code>--etcd-servers</code>, and <code>--service-cluster-ip-range</code>. Harden the API server so it:</p><ul><li>Rejects anonymous API requests.</li><li>Uses <code>Node,RBAC</code> authorization rather than <code>AlwaysAllow</code>.</li><li>Validates ServiceAccount tokens against the API server.</li><li>Disables profiling endpoints.</li><li>Writes audit events to <code>/var/log/kubernetes/audit/audit.log</code> instead of standard output.</li></ul><p>Do not add a new API server, change the image, or use an API object such as a Deployment. This is a static control-plane Pod manifest. It must not be applied to the Kind lab.</p><h2>Verification</h2><pre><code>Get-Content .\lab\lesson-22-kube-apiserver-answer.yaml
|
||||||
|
|
||||||
|
# On a real kubeadm control-plane node, inspect the active static Pod:
|
||||||
|
sudo grep -E -- '--anonymous-auth|--authorization-mode|--service-account-lookup|--profiling|--audit-log-path' /etc/kubernetes/manifests/kube-apiserver.yaml</code></pre><p>Production changes to the API-server manifest require the approved control-plane maintenance process. A kubelet automatically restarts its static Pod when the manifest changes, so do not make experimental edits on a live production control plane.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is <code>--authorization-mode=AlwaysAllow</code> dangerous even if Roles and RoleBindings are carefully designed?</p><label><input type="radio" name="q" value="always">It allows requests without applying RBAC authorization decisions.</label><label><input type="radio" name="q" value="logs">It disables audit logs.</label><label><input type="radio" name="q" value="pods">It makes every Pod privileged.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the Kubernetes <a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/">kube-apiserver command reference</a> to confirm the security-flag behavior.</p><h2>What to send me</h2><p>Paste your complete command list. I’ll review one flag at a time and preserve your request not to reveal the next correction early.</p><footer>Previous: <a href="0021-pod-security-admission-modes.html">Pod Security Admission modes</a> · CKS domain: Cluster Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='always'){f.textContent='Correct. AlwaysAllow bypasses the authorization controls that RBAC is meant to enforce.';f.className='feedback good';}else{f.textContent='Not quite. The authorization mode decides whether RBAC is evaluated for a request.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0023-cilium-wireguard-encryption.html
Normal file
1
lessons/0023-cilium-wireguard-encryption.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 23: Cilium WireGuard encryption</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0023 · 35 minutes · Minimize Microservice Vulnerabilities</p><h1>Encrypt Pod-to-Pod traffic with Cilium WireGuard</h1><p class="lede">NetworkPolicy controls who may communicate. Transparent encryption controls whether traffic can be read while it travels between nodes. They solve different security problems and commonly work together.</p><div class="callout"><strong>Exam reflex:</strong> identify the encryption scope before enabling options. Default WireGuard encryption protects traffic between Cilium-managed Pods on different nodes; node-to-node encryption is a separate option and is not required here.</div><h2>Timed task: 25 minutes</h2><p>Read <code>lab/lesson-23-cilium-scenario.md</code>. Create <code>lab/lesson-23-answer.md</code> with these four items:</p><ol><li>One exact <code>helm upgrade</code> command that retains the existing release values while enabling transparent WireGuard encryption for Cilium 1.19.6.</li><li>The required firewall change, including protocol and port.</li><li>Two preflight checks you would run before changing the cluster.</li><li>Two post-change validation commands, and what each confirms.</li></ol><p>Do not enable node-to-node encryption. Do not propose IPsec, a CNI change, or CNI-chaining MTU settings; the scenario does not require them.</p><h2>Expected reasoning</h2><p>For this scenario, WireGuard’s required Helm options enable transparent encryption and select the WireGuard implementation. The nodes must reach one another on WireGuard’s UDP port <code>51871</code>. WireGuard keys are generated per node and distributed through Cilium’s Kubernetes resources; do not create a shared Kubernetes Secret for them.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Does Cilium WireGuard encrypt traffic between two Pods that are scheduled on the same node?</p><label><input type="radio" name="q" value="no">No. Same-node traffic is not encrypted by the inter-node WireGuard tunnel.</label><label><input type="radio" name="q" value="yes">Yes. Every Pod-to-Pod packet always enters a WireGuard tunnel.</label><label><input type="radio" name="q" value="policy">Only if a NetworkPolicy allows it.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use Cilium’s official <a href="https://docs.cilium.io/en/stable/security/network/encryption-wireguard/">WireGuard transparent encryption</a> guide. Confirm the Helm settings, kernel prerequisite, port, scope, and validation procedure.</p><h2>What to send me</h2><p>Paste your four-item rollout note. I’ll review it one issue at a time, without giving the next answer early.</p><footer>Previous: <a href="0022-kube-apiserver-hardening.html">API server hardening</a> · CKS domain: Minimize Microservice Vulnerabilities</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='no'){f.textContent='Correct. Inter-node encryption does not add protection for traffic that never leaves the node.';f.className='feedback good';}else{f.textContent='Not quite. WireGuard encrypts traffic travelling between nodes; same-node traffic does not use that tunnel.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0024-rbac-remediate-overprivileged-role.html
Normal file
1
lessons/0024-rbac-remediate-overprivileged-role.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CKS lesson 24: RBAC remediation</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Lesson 0024 · 20 minutes · Cluster Hardening</p><h1>Remove an unnecessary Secret-read permission</h1><p class="lede">This is an exam-style remediation: an existing workload identity has more access than it needs. Change only the permission rule, preserve the established identity and binding, and prove the boundary.</p><div class="callout"><strong>Exam reflex:</strong> before creating new RBAC objects, inspect the existing Role and RoleBinding. Often the smallest safe fix is simply to narrow one Role rule.</div><h2>Timed task: 12 minutes</h2><p>Apply <code>lab/lesson-24-fixture.yaml</code>. The <code>release-bot</code> ServiceAccount in namespace <code>release</code> is overprivileged.</p><p>Create <code>lab/lesson-24-answer.yaml</code> containing a replacement for only the existing Role <code>release-bot-reader</code>. Preserve its name and namespace. After your change, the ServiceAccount must:</p><ul><li>Be able to <code>get</code>, <code>list</code>, and <code>watch</code> ConfigMaps in <code>release</code>.</li><li>Have no permission to read Secrets.</li><li>Have no permission to create ConfigMaps.</li></ul><p>Do not create another Role, RoleBinding, ClusterRole, or ServiceAccount. The RoleBinding already connects the correct identity to the correct Role.</p><h2>Verification</h2><pre><code>kubectl --context kind-cks-lab apply -f .\lab\lesson-24-fixture.yaml 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</code></pre><p>Expected results: <code>yes yes yes no no</code>.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is it better to remove the Secret permission from the existing Role than to create a second, narrower Role and binding?</p><label><input type="radio" name="q" value="additive">RBAC permissions are additive; the original Role would still grant Secret access.</label><label><input type="radio" name="q" value="names">A ServiceAccount can only have one RoleBinding.</label><label><input type="radio" name="q" value="namespace">Roles cannot grant Secret access.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use the official Kubernetes <a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">RBAC reference</a> for rules, bindings, and ServiceAccount subject names.</p><h2>What to send me</h2><p>Paste only the replacement Role and the five <code>can-i</code> results. I’ll review it one issue at a time and will not reveal the next correction early.</p><footer>Previous: <a href="0023-cilium-wireguard-encryption.html">Cilium WireGuard encryption</a> · CKS domain: Cluster Hardening</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='additive'){f.textContent='Correct. RBAC allows the union of permissions granted by all applicable bindings.';f.className='feedback good';}else{f.textContent='Not quite. Adding a narrower Role does not subtract the broader permission that already exists.';f.className='feedback bad';}}</script></body></html>
|
||||||
4
lessons/0025-istio-strict-mtls.html
Normal file
4
lessons/0025-istio-strict-mtls.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Supplemental lesson 25: Istio strict mTLS</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Supplemental Lesson 0025 · 20 minutes · Istio service-mesh security</p><h1>Require mTLS for one application namespace</h1><p class="lede">Istio sidecars can automatically use mTLS, but permissive acceptance may still allow plaintext requests. <code>PeerAuthentication</code> with <code>STRICT</code> tells receiving workloads to accept only mTLS traffic.</p><div class="callout"><strong>Security reflex:</strong> PeerAuthentication controls what the destination workload accepts. For namespace-wide policy, use no workload selector; a selector narrows the policy to only matching Pods.</div><h2>Timed task: 12 minutes</h2><p>Read <code>lab/lesson-25-fixture.yaml</code>. Create <code>lab/lesson-25-answer.yaml</code> from scratch with one Istio <code>PeerAuthentication</code> resource.</p><p>The policy must:</p><ul><li>Use API version <code>security.istio.io/v1</code>.</li><li>Be named <code>default</code> in namespace <code>payments-mesh</code>.</li><li>Require <code>STRICT</code> mTLS for every workload in that namespace.</li><li>Contain no workload selector and no port exceptions.</li></ul><p>The local Kind cluster does not have Istio installed, so do not apply this manifest there. This is API and policy-scope practice; on an Istio cluster, first apply the supplied fixture so the Namespace receives sidecar injection.</p><h2>Verification on an Istio cluster</h2><pre><code>kubectl apply -f .\lab\lesson-25-fixture.yaml
|
||||||
|
kubectl apply -f .\lab\lesson-25-answer.yaml
|
||||||
|
kubectl -n payments-mesh get peerauthentication default -o yaml
|
||||||
|
istioctl x describe pod -n payments-mesh -l app=payments-api</code></pre><p>The first command creates the injection-enabled namespace and workload. The final command confirms the workload is in the mesh and helps diagnose mTLS policy state.</p><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why should this namespace-wide policy omit <code>spec.selector</code>?</p><label><input type="radio" name="q" value="scope">Without a selector, it applies to every workload in its own namespace.</label><label><input type="radio" name="q" value="tls">Selectors disable automatic certificates.</label><label><input type="radio" name="q" value="cluster">A selector would make the policy cluster-wide.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use Istio’s official <a href="https://istio.io/latest/docs/reference/config/security/peer_authentication/">PeerAuthentication reference</a> and <a href="https://istio.io/latest/docs/tasks/security/authentication/authn-policy/">mTLS authentication task</a>.</p><h2>What to send me</h2><p>Paste the single policy YAML. I’ll check it one issue at a time and won’t reveal the next correction early.</p><footer>Supplemental path: Istio mTLS · Next: workload-scoped mTLS and authorization</footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='scope'){f.textContent='Correct. A PeerAuthentication without a selector applies to all workloads in that policy namespace.';f.className='feedback good';}else{f.textContent='Not quite. Omitting the selector is what makes this a namespace-wide destination policy.';f.className='feedback bad';}}</script></body></html>
|
||||||
1
lessons/0026-istio-workload-authorization.html
Normal file
1
lessons/0026-istio-workload-authorization.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Supplemental lesson 26: Istio workload authorization</title><link rel="stylesheet" href="../assets/course.css"></head><body><main><p class="eyebrow">Supplemental Lesson 0026 · 25 minutes · Istio service-mesh security</p><h1>Allow one workload identity to call the payments API</h1><p class="lede">Strict mTLS proves the client has a mesh identity. An Istio AuthorizationPolicy decides which authenticated identity may actually call a workload. Together, they turn encrypted traffic into least-privilege service access.</p><div class="callout"><strong>Security reflex:</strong> use a SPIFFE-style principal when access must be tied to one ServiceAccount. A Namespace name alone is broader: every workload in that Namespace could match it.</div><h2>Timed task: 15 minutes</h2><p>Read <code>lab/lesson-26-fixture.yaml</code>. Assume Lesson 25 strict mTLS is already active in <code>payments-mesh</code>. Create <code>lab/lesson-26-answer.yaml</code> from scratch with one Istio <code>AuthorizationPolicy</code>.</p><p>The policy must:</p><ul><li>Be named <code>payments-api-only-frontend</code> in namespace <code>payments-mesh</code>.</li><li>Select only workloads labelled <code>app=payments-api</code>.</li><li>Use action <code>ALLOW</code>.</li><li>Allow only the ServiceAccount <code>frontend</code> in namespace <code>web-mesh</code>.</li><li>Allow only TCP port <code>8080</code>.</li></ul><p>Do not allow an entire Namespace, use a wildcard principal, add an HTTP method/path rule, or create a PeerAuthentication policy. The local Kind cluster does not have Istio, so this is a manifest-reading exercise.</p><h2>Identity shape</h2><pre><code>cluster.local/ns/<namespace>/sa/<serviceaccount></code></pre><p>Istio obtains that identity from mTLS. This is why strict mTLS is the foundation for this type of workload authorization.</p><h2>Verification on an Istio cluster</h2><pre><code>kubectl apply -f .\lab\lesson-26-fixture.yaml 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</code></pre><h2>Retrieval check</h2><div class="quiz"><h3>Question</h3><p>Why is the source principal safer here than allowing all clients from <code>web-mesh</code>?</p><label><input type="radio" name="q" value="identity">It limits access to the exact frontend ServiceAccount instead of every workload in that namespace.</label><label><input type="radio" name="q" value="tls">It disables mTLS for unrelated clients.</label><label><input type="radio" name="q" value="port">It automatically chooses port 8080.</label><button onclick="check()">Check answer</button><p id="feedback" class="feedback" aria-live="polite"></p></div><h2>Documentation target</h2><p>Use Istio’s official <a href="https://istio.io/latest/docs/reference/config/security/authorization-policy/">AuthorizationPolicy reference</a> and <a href="https://istio.io/latest/docs/concepts/security/">security concepts</a>.</p><h2>What to send me</h2><p>Paste the single policy YAML. I’ll review it one issue at a time and will not reveal the next correction early.</p><footer>Supplemental path: Istio authorization · Previous: <a href="0025-istio-strict-mtls.html">Strict mTLS</a></footer></main><script>function check(){const c=document.querySelector('input[name=q]:checked'),f=document.getElementById('feedback');if(!c){f.textContent='Choose an answer.';f.className='feedback bad';return;}if(c.value==='identity'){f.textContent='Correct. The ServiceAccount principal is the narrowest workload identity in this scenario.';f.className='feedback good';}else{f.textContent='Not quite. A Namespace is a group of workloads; the principal names one exact authenticated workload identity.';f.className='feedback bad';}}</script></body></html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user