docs: add Kubernetes CKS study notes

This commit is contained in:
2026-08-04 23:18:45 +02:00
parent a6ee7a2b07
commit 91a1849009
57 changed files with 8313 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
# CKS Edition — Resources & Sprint Plan
> **Owner:** freshly-minted CKA (81%, v1.35 curriculum), prod SRE
> **Mission:** CKS while the cluster-surgery fingers are still warm
> **Doc status:** v0.1 — living document
> **Operating mode unchanged:** DRILL > read. Type first, autopsy second.
---
## 1. Ground truth
| Item | Value |
|---|---|
| Prerequisite | Valid-at-any-point CKA — ✅ you qualify twice over |
| Format | 1520 tasks (typically ~16), 120 min, PSI remote proctored |
| Passing score | **67%** (one point higher than CKA) |
| Exam version | Tracks latest k8s minor — verify at booking |
| Voucher includes | 1 free retake + **2 killer.sh CKS sessions** |
| killer.sh CKS quirk | Both sessions have **IDENTICAL questions** (unlike CKA's A/B split) — changes the protocol, see §4 |
| Curriculum revision | Oct 2024 — courses/notes older than that are poison |
### Domains (post-2024 weights)
- Cluster Setup — 10%
- Cluster Hardening — 15%
- System Hardening — 15%
- Minimize Microservice Vulnerabilities — 20%
- Supply Chain Security — 20%
- Monitoring, Logging & Runtime Security — 20%
### Allowed docs (broader than CKA — this is a gift)
- kubernetes.io/docs + kubernetes.io/blog
- **Third-party tool docs for exam-covered tools**: Falco, Trivy, AppArmor (etc. — verify exact list on the LF exam-resources page at booking)
- One extra browser tab, same PSI environment as CKA — you know the desk already
---
## 2. Resource stack — ranked for YOUR situation
### Tier 1 — the spine (use all three)
**1. KodeKloud CKS course (Mumshad)** — primary, already in your sub, fully updated for the Oct-2024 revision.
Usage: unlike your CKA re-sit, this is NEW material — watch + lab for everything except RBAC/NetworkPolicy/ServiceAccount sections (skim-lab those; you just aced them). The community verdict is consistent: KodeKloud CKS alone covers what the exam requires.
**2. Kim Wüstkamp's free CKS course (YouTube) + Killercoda CKS scenarios (free)** — the resource CKA prep didn't have an equivalent of. Made by the killer.sh author, so it's tuned to exactly how the simulator (and therefore the exam) phrases things.
Usage: Killercoda scenarios as your DAILY drill snack — browser-based, zero setup, one scenario per coffee. The video course as second-angle explanation when a KodeKloud topic doesn't click (Falco rules and AppArmor profiles are the usual candidates).
**3. killer.sh CKS sessions (with voucher)** — same diagnostic role as before, one protocol change (§4).
### Tier 2 — in your sub, use selectively
**KodeKloud "CKS Challenges"** — scenario-based hardening gauntlets (attack-then-defend style). Excellent as the bridge between course labs and mocks; run these AFTER the course, BEFORE killer.
**KodeKloud Ultimate CKS Mock Exam Series** — auto-scored mocks mapped to curriculum proportions. Same role as the CKA Ultimate mocks: volume + speed gate before killer. Same caveats as before: question-bank recycling on repeats, occasional wrong answers — grade skeptically.
### Tier 3 — reference, not courses
- **Official curriculum repo** (github.com/cncf/curriculum) — read the CKS PDF once; it's the contract.
- **kube-bench / CIS benchmark output** — you don't study CIS docs; you run kube-bench and learn to read its remediation text (that's the exam skill).
- Community question repos (e.g. the killer.sh-style GitHub collections) — mine for extra drill ideas ONLY after killer; don't let 2020-era questions teach you PSP or other dead APIs.
### Skip
- LFS260 (official LF course) — fine content, but redundant with KodeKloud at extra cost.
- Any material predating Oct 2024 revision without an update note. PSP, old dashboards — dead ends.
- Paid mock bundles beyond what's above. Volume is already covered.
---
## 3. The delta (what CKA didn't teach you)
Tiered by exam weight × your unfamiliarity:
### Tier 1 — new tools, guaranteed presence, drill until boring
- [ ] **Falco** (Runtime, 20% domain) — read/write rules, find the offending pod from log output, output_fields formatting, `falco.yaml` config, service restart flow. Killer's classic: "collect logs in EXACTLY this format."
- [ ] **Trivy** (Supply chain, 20%) — image scanning, filtering by CVE ID/severity, scanning many images fast (`k get pods -o jsonpath` → loop trivy)
- [ ] **AppArmor + seccomp** (System hardening) — load a profile on the node (`apparmor_parser`), reference via `securityContext.appArmorProfile` (post-1.30 field, NOT the old annotation — revision trap), seccomp `RuntimeDefault` + custom Localhost profiles
- [ ] **Audit logging** — write an audit policy (levels, rules, omitStages), wire `--audit-*` flags + volumes into the apiserver static pod, grep/jq the log
- [ ] **Admission for images** — ImagePolicyWebhook flag/config plumbing; plus awareness of OPA Gatekeeper/Kyverno constraint shapes
### Tier 2 — extensions of what you know
- [ ] **Pod Security Admission** — namespace labels, enforce/audit/warn, baseline vs restricted (you know the concept; drill the label syntax cold)
- [ ] **SecurityContext deep cuts** — runAsNonRoot, readOnlyRootFilesystem, capabilities drop/add, privileged escalation chain
- [ ] **NetworkPolicy expert tier** — default-deny both directions + selective holes; you're already strong, push to speed
- [ ] **RBAC hardening** — minimize/audit existing permissions (reverse of creating them), SA token automounting off, bound tokens
- [ ] **Secrets encryption at rest** — EncryptionConfiguration, apiserver flag, re-encrypt existing secrets
- [ ] **TLS/cert hygiene** — apiserver TLS versions/ciphers flags, kubelet authn/authz flags (the x509 muscle from CKA transfers directly)
- [ ] **RuntimeClass / gVisor** — define RuntimeClass, run pod under runsc, verify via `dmesg`/uname trick
- [ ] **kube-bench** — run, read, remediate selected findings on CP node
- [ ] **mTLS / ServiceMesh awareness** — curriculum mentions it at concept level; don't over-invest
### Tier 3 — CKA muscle that transfers as-is
Static pod surgery, kubeadm layout, etcd, kubeconfig triage, netpol, RBAC creation, jsonpath — all directly reusable. Your v2 mock pack was secretly half CKS prep.
---
## 4. killer.sh protocol — CHANGED for CKS
Both CKS sessions are **identical** (same 17 questions), unlike CKA's different A/B sets. So:
- **Session 1 (T-14 to T-10):** full 120-min diagnostic, exam conditions. Then the 34h autopsy: every solution, re-solve misses.
- **Session 2 (T-3):** it's a RETAKE of the same paper — the goal is not discovery but **fluency**: target 90%+ and finishing with 20+ min spare. It measures whether the patch work landed, not whether you'd pass cold.
- Between them: Killercoda scenarios + KodeKloud mocks fill the novelty gap that CKA's different-question session 2 used to provide.
---
## 5. Sprint plan — 3 weeks (CKA-holder pace)
### Week 1 — Tooling assault
- D12: Falco (KodeKloud section + labs + 2 Killercoda scenarios)
- D3: Trivy + supply chain (image digests, allowed registries via policy)
- D4: AppArmor + seccomp end-to-end (node profile → pod reference → verify)
- D5: Audit logging (policy authoring + apiserver wiring — this is static-pod surgery you already own)
- Weekend: PSA + SecurityContext + secrets encryption; first KodeKloud CKS mock, untimed, to map the terrain
### Week 2 — Integration
- D1: kube-bench + apiserver/kubelet hardening flags
- D2: ImagePolicyWebhook + admission landscape
- D3: RuntimeClass/gVisor + RBAC minimization drills
- D45: KodeKloud CKS Challenges (the attack/defend gauntlets)
- Weekend: **killer.sh CKS session 1** under full exam law → miss list → Claude drill pack CKS-edition gets built from it
### Week 3 — Sharpen + sit
- D13: drills on session-1 misses; Ultimate CKS mocks timed until <100 min clean
- D4: **killer.sh session 2** (the fluency retake — 90%+ or investigate)
- D56: docs-navigation reps on Falco/Trivy/AppArmor sites (new allowed domains = new muscle), light review
- D7: **EXAM**
Range note: the kind+Cilium `drills` cluster handles ~70% of CKS drilling (netpol, PSA, securityContext, RBAC, audit-policy authoring, RuntimeClass definitions, trivy/kube-bench runs). AppArmor/Falco/gVisor need real node access — kind containers can host trivy and kube-bench fine, AppArmor partially (host kernel dependent); use KodeKloud labs/Killercoda for the kernel-adjacent stuff rather than fighting the range.
---
## 6. Booking checklist
- [ ] CKS purchased (includes retake + 2 killer sessions)
- [ ] Exam date booked: ______ (target: ~3.5 weeks out, per sprint)
- [ ] killer.sh CKS sessions visible in portal
- [ ] LF allowed-resources page re-checked at booking (tool-docs list)
- [ ] Dog scheduled for grandma's house on exam day — **non-negotiable line item**
## 7. Changelog
- v0.1 — initial: ground truth, ranked resource stack, delta inventory, killer protocol correction, 3-week sprint.

View File

@@ -0,0 +1,37 @@
# CKS Mock Exam — k8s v1.35
Timed practice that mirrors the current CKS (17 tasks, 2h, six domains). Runs on kind (k8s v1.35.5) with the host-side / kernel bits on your Ubuntu VM.
## Prereqs (Ubuntu VM)
- docker, `kind >= v0.32.0`, `kubectl` v1.35, `helm`
- host tools installed latest: `kube-bench`, `trivy`, `kubesec`, `falco` (v0.44.x), optional `cosign`
- host must have AppArmor enabled (default on Ubuntu) for Task 7
## Quickstart
```bash
chmod +x bootstrap.sh seed.sh
./bootstrap.sh # creates 'cks' cluster (1 cp + 2 workers) + ingress-nginx
./seed.sh # plants the target/vulnerable objects
# set a 2h timer, open exam.md, go.
```
## Files
- `kind-config.yaml` — cluster topology, v1.35.5 pinned digest, exam-files mount
- `bootstrap.sh` — cluster + ingress controller + tooling checklist
- `seed.sh` — objects for the kubectl-only tasks
- `exam.md` — the 17 tasks (per-task setup + statement, no answers)
- `answers/solutions.md` — worked solutions (don't peek until you've timed a full run)
## Domain coverage (weights)
Cluster Setup 15 · Cluster Hardening 15 · System Hardening 10 · Microservice Vuln 20 · Supply Chain 20 · Monitoring/Logging/Runtime 20.
## Reset
```bash
kind delete cluster --name cks && ./bootstrap.sh && ./seed.sh
```
## Notes / kind caveats
- **Version:** exam is on v1.35 per the Linux Foundation page. k8s 1.36 shipped ~May 2026; the exam env aligns "within 48 weeks" of a release, so it *may* have rolled to 1.36 by your date — verify on the LF exam page. Nothing in this set changes between 1.35/1.36. To bump: swap the digest in `kind-config.yaml` for a `kindest/node:v1.36.x` one.
- **AppArmor** needs a real AppArmor host; works on Ubuntu, not on Docker Desktop/macOS.
- **gVisor (Task 11)** pod stays Pending in kind (no `runsc` in kind's containerd) — the config is the graded artifact.
- **ImagePolicyWebhook / audit / encryption** edit the live apiserver: always `cp` the manifest first so you can revert fast.

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# CKS mock — cluster bootstrap. Run on your Ubuntu VM (needs docker, kind >= v0.32.0, kubectl v1.35, helm).
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$HERE"
mkdir -p exam-files
echo "[*] Creating kind cluster 'cks' (1 cp + 2 workers, k8s v1.35.5)..."
kind create cluster --config kind-config.yaml
kubectl config use-context kind-cks
echo "[*] Installing an ingress controller (needed for the TLS task)..."
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl -n ingress-nginx wait --for=condition=Ready pod \
-l app.kubernetes.io/component=controller --timeout=180s || true
cat <<'EOF'
[*] Cluster up. Sanity check:
kubectl get nodes -o wide
[*] Host-side tooling you want on the VM (install latest):
- kube-bench : https://github.com/aquasecurity/kube-bench/releases (or run as a Job in-cluster)
- trivy : https://github.com/aquasecurity/trivy (apt: aquasecurity repo)
- kubesec : https://github.com/controlplaneio/kubesec/releases
- falco : apt install falco (v0.44.1) — used in the runtime task on the VM
- helm : for Kyverno / Falco chart installs
- cosign : optional, image signing task
[*] Now run ./seed.sh to plant the vulnerable/target objects for the kubectl-only tasks.
EOF

View File

@@ -0,0 +1,344 @@
# CKS Mock Exam — k8s v1.35
**Format mirrors the real thing:** 17 tasks, 2 hours, ~66% to pass. Each task shows a weight and a domain. In the real exam every task tells you which cluster/context to `kubectl config use-context` into first — here it's all one cluster (`kind-cks`), but the habit of *reading the context line first* is baked in anyway. Tasks touching control-plane files run via `docker exec cks-control-plane ...`; host/kernel tasks run on the node container or your Ubuntu VM as noted.
**Ground rules (same as real exam):**
- Bookmark allowed docs only: kubernetes.io/docs, kubernetes.io/blog, falco.org, and the tool projects. Practice with those tabs only.
- No stopping the clock. Do the fast, formulaic tasks first (NetworkPolicy, RBAC, PSA labels), leave apiserver-restart tasks for a block where you can afford the ~1 min downtime.
- Every task: `kubectl config use-context kind-cks` before you start (muscle memory).
Setup: `./bootstrap.sh` then `./seed.sh`. Per-task extra setup is in each **Setup** block below.
Weight tally: Cluster Setup 15 · Cluster Hardening 15 · System Hardening 10 · Microservice Vuln 20 · Supply Chain 20 · Monitoring/Logging/Runtime 20.
---
## Task 1 — Default-deny + selective NetworkPolicy · Cluster Setup · 4%
**Context:** `kind-cks`
**Setup:** seeded by `seed.sh` (namespace `prod`: `db`, `frontend`, `attacker`).
**Task:**
In namespace `prod`, the `db` workload must only be reachable from the `frontend` workload.
1. Create a NetworkPolicy `default-deny` in `prod` that denies **all ingress** to every pod in the namespace.
2. Create a NetworkPolicy `allow-frontend` in `prod` that allows ingress to pods labelled `app=db` **only** from pods labelled `app=frontend`, on TCP 5432.
Do not modify the deployments. `attacker` must NOT be able to reach `db`; `frontend` must.
> Verify: exec into `frontend` and `attacker`, `curl db:5432` — frontend succeeds, attacker times out.
---
## Task 2 — TLS-terminated Ingress · Cluster Setup · 4%
**Context:** `kind-cks`
**Setup:** seeded (`web` ns: `hello` Deployment + Service). ingress-nginx installed by bootstrap.
**Task:**
Expose the `hello` Service in namespace `web` over HTTPS.
1. Create a TLS Secret `hello-tls` in `web` from a self-signed cert for host `hello.cks.local` (you generate the cert/key on the VM).
2. Create an Ingress `hello` in `web` routing host `hello.cks.local` path `/` to Service `hello:80`, using `hello-tls` for TLS.
> Verify: `curl -k --resolve hello.cks.local:443:127.0.0.1 https://hello.cks.local/` returns `hello-tls` (adjust port to the ingress-nginx NodePort/hostPort in your setup).
---
## Task 3 — kube-bench: remediate CIS findings · Cluster Setup · 5%
**Context:** `kind-cks` (control-plane node)
**Setup:** none beyond the cluster.
**Task:**
Run `kube-bench` against the control-plane node and remediate the following **FAIL** items so a re-run passes:
- kubelet: `--anonymous-auth` must be `false`.
- kubelet: `--authorization-mode` must not be `AlwaysAllow` (use `Webhook`).
- kubelet: read-only port must be disabled (`--read-only-port=0`).
- kube-apiserver: `--profiling` must be `false`.
Apply the kubelet changes via the kubelet config on the node and restart kubelet; apply the apiserver change via the static manifest. Re-run kube-bench to confirm those four checks pass.
> The kubelet config on a kind node is `/var/lib/kubelet/config.yaml`; the apiserver manifest is `/etc/kubernetes/manifests/kube-apiserver.yaml`.
---
## Task 4 — RBAC least privilege · Cluster Hardening · 5%
**Context:** `kind-cks`
**Setup:** seeded (`dev` ns: SA `ci-runner`, Role `ci-runner-role` with `*/*/*`, RoleBinding `ci-runner-rb`).
**Task:**
The `ci-runner` ServiceAccount in `dev` is bound to a cluster-admin-equivalent Role. Restrict it to least privilege:
- `ci-runner` must be able to **get, list, watch** only `pods` and `configmaps` in `dev`.
- It must have **no other permissions** anywhere.
Do not delete the SA or the RoleBinding — reshape the Role (or replace it) so the binding still points at the right role name. Verify with `kubectl auth can-i`.
> Verify: `kubectl auth can-i delete pods --as=system:serviceaccount:dev:ci-runner -n dev` → **no**; `... get pods ...` → **yes**.
---
## Task 5 — API server hardening · Cluster Hardening · 5%
**Context:** `kind-cks` (control-plane)
**Setup:** none.
**Task:**
Edit the kube-apiserver static manifest so that:
1. Anonymous auth is disabled (`--anonymous-auth=false`).
2. The `NodeRestriction` admission plugin is enabled.
3. Profiling is disabled (`--profiling=false`) — if you already did this in Task 3, confirm it's present.
Wait for the apiserver static pod to come back healthy before moving on.
> Verify: `kubectl -n kube-system get pod -l component=kube-apiserver` Running; `curl -k https://127.0.0.1:6443/healthz` from inside the node returns anonymous 401/403 rather than `ok`.
---
## Task 6 — Disable ServiceAccount token automount · Cluster Hardening · 5%
**Context:** `kind-cks`
**Setup:** seeded (`apps` ns: SA `web-sa`, Deployment `web`).
**Task:**
Pods in namespace `apps` should not receive an automounted ServiceAccount token unless they explicitly need one.
1. Set `automountServiceAccountToken: false` on the `web-sa` ServiceAccount.
2. Ensure the running `web` pods no longer mount the token (roll them if needed).
Do not change the ServiceAccount name or the Deployment's `serviceAccountName`.
> Verify: `kubectl -n apps exec deploy/web -- ls /var/run/secrets/kubernetes.io/serviceaccount` → no such file/dir.
---
## Task 7 — AppArmor profile enforcement · System Hardening · 5%
**Context:** `kind-cks` (worker node + cluster)
**Setup:** run this to stage the profile file on a worker (host must have AppArmor — Ubuntu VM does):
```bash
# on the VM
cat > /tmp/deny-write <<'EOF'
#include <tunables/global>
profile k8s-deny-write flags=(attach_disconnected) {
#include <abstractions/base>
file,
deny /** w,
}
EOF
docker cp /tmp/deny-write cks-worker:/root/deny-write
```
**Task:**
1. Load the AppArmor profile `k8s-deny-write` in **enforce** mode on node `cks-worker`.
2. Create a Pod `locked` in namespace `sysh` running `image: busybox:1.36`, command `sleep`→ use `["sh","-c","sleep 1h"]`, that runs under the `k8s-deny-write` profile using the **AppArmor securityContext API** (not the deprecated annotation). Pin it to `cks-worker`.
> Verify: `kubectl -n sysh exec locked -- sh -c 'echo x > /tmp/x'` → **Permission denied**.
---
## Task 8 — seccomp custom profile · System Hardening · 5%
**Context:** `kind-cks` (worker node + cluster)
**Setup:** stage a custom seccomp profile on the worker:
```bash
cat > /tmp/audit.json <<'EOF'
{ "defaultAction": "SCMP_ACT_LOG" }
EOF
docker exec cks-worker mkdir -p /var/lib/kubelet/seccomp/profiles
docker cp /tmp/audit.json cks-worker:/var/lib/kubelet/seccomp/profiles/audit.json
```
**Task:**
Create a Pod `traced` in namespace `sysh` (`image: busybox:1.36`, `["sh","-c","sleep 1h"]`, pinned to `cks-worker`) that uses the **Localhost** seccomp profile `profiles/audit.json`. Then create a second Pod `defaulted` in `sysh` that uses the `RuntimeDefault` seccomp profile.
> Verify: `kubectl -n sysh get pod traced -o jsonpath='{.spec.securityContext.seccompProfile}'` shows the Localhost profile.
---
## Task 9 — Enforce restricted Pod Security Standard · Microservice Vuln · 5%
**Context:** `kind-cks`
**Setup:** seeded (`restricted-ns`: Deployment `payments` that is privileged → violates restricted).
**Task:**
1. Label namespace `restricted-ns` to **enforce** the `restricted` Pod Security Standard (latest version), and to **warn/audit** at `restricted` too.
2. The existing `payments` Deployment currently violates it. Modify the pod template so it complies with `restricted` (drop privilege, set the required securityContext fields) and rolls out successfully.
> Verify: `kubectl -n restricted-ns get deploy payments` has ready replicas; creating a privileged pod in the ns is rejected.
---
## Task 10 — Encrypt Secrets at rest · Microservice Vuln · 6%
**Context:** `kind-cks` (control-plane)
**Setup:** create a secret that currently sits in etcd in plaintext:
```bash
kubectl create ns vault-ns --dry-run=client -o yaml | kubectl apply -f -
kubectl -n vault-ns create secret generic pre-existing --from-literal=api=SUPERSECRET
```
**Task:**
1. Configure an `EncryptionConfiguration` (provider `aescbc` with a 32-byte key, `identity` as fallback) at `/etc/kubernetes/enc/enc.yaml` on the control-plane, and point the apiserver at it via `--encryption-provider-config` (mount it into the static pod).
2. Ensure **all existing Secrets** cluster-wide are rewritten so they're stored encrypted.
> Verify: read the raw etcd value for `secret/vault-ns/pre-existing` with `etcdctl` inside the control-plane — it must start with `k8s:enc:aescbc:` and not contain `SUPERSECRET`.
---
## Task 11 — RuntimeClass for sandboxed workload · Microservice Vuln · 4%
**Context:** `kind-cks`
**Setup:** none.
**Task:**
1. Create a RuntimeClass `gvisor` with handler `runsc`.
2. Create a Pod `sandboxed` in namespace `runtime` (`image: nginx:1.27`) that runs under the `gvisor` RuntimeClass.
> Note: kind's containerd has no `runsc` handler, so the pod will stay `Pending`/`ContainerCreating` — that's expected. The graded artifact is the correct RuntimeClass + pod spec. (On a VM with gVisor + a containerd `runsc` runtime configured, it would actually run.)
> Verify: `kubectl get runtimeclass gvisor` exists with handler `runsc`; pod references it.
---
## Task 12 — ValidatingAdmissionPolicy (in-tree, CEL) · Microservice Vuln · 5%
**Context:** `kind-cks`
**Setup:** none.
**Task:**
Using the built-in `ValidatingAdmissionPolicy` API (no external webhook, no Kyverno/OPA), block the creation of any Pod that:
- sets `privileged: true` on any container, **or**
- uses a `hostPath` volume.
Enforce it cluster-wide with `failurePolicy: Fail`. Bind it so it's active.
> Verify: applying a pod with `privileged: true` or a `hostPath` volume is denied by the policy; a clean pod is admitted.
---
## Task 13 — Trivy image scan & evict vulnerable pods · Supply Chain · 6%
**Context:** `kind-cks`
**Setup:** seeded (`images` ns: `legacy-app` [nginx:1.19.0], `old-debian` [debian:10], `clean-app` [nginx:1.27]).
**Task:**
Using `trivy` on the VM, scan the images used by the pods in namespace `images`. **Delete every pod** whose image has at least one **HIGH or CRITICAL** severity, OS-level vulnerability. Leave the rest running.
> Verify: `legacy-app` and `old-debian` gone; `clean-app` (or whichever scans clean of HIGH/CRITICAL) remains.
---
## Task 14 — Kyverno policy: no `:latest`, trusted registries only · Supply Chain · 6%
**Context:** `kind-cks`
**Setup:** install Kyverno (latest) — do this at the start of the task:
```bash
# latest install manifest
kubectl create -f https://github.com/kyverno/kyverno/releases/latest/download/install.yaml
kubectl -n kyverno rollout status deploy/kyverno-admission-controller --timeout=180s
kubectl create ns team-a --dry-run=client -o yaml | kubectl apply -f -
```
**Task:**
Author Kyverno ClusterPolicies (enforce mode) that:
1. **Reject** any Pod whose container image uses the `:latest` tag or has no tag.
2. **Reject** any Pod whose image does not come from registry `registry.cks.local` (allow that registry only).
Apply them cluster-wide.
> Verify: `kubectl -n team-a run bad --image=nginx:latest` → denied; `kubectl -n team-a run bad2 --image=docker.io/nginx:1.27` → denied (wrong registry); `... --image=registry.cks.local/nginx:1.27` → admitted (may fail to pull, but admission passes).
---
## Task 15 — ImagePolicyWebhook admission control · Supply Chain · 4%
**Context:** `kind-cks` (control-plane)
**Setup:** stage the admission config + webhook kubeconfig (you'll write these as part of the task; skeleton paths below):
- `/etc/kubernetes/admission/admission-config.yaml`
- `/etc/kubernetes/admission/imagepolicy-kubeconfig.yaml`
**Task:**
Enable the `ImagePolicyWebhook` admission plugin on the kube-apiserver and wire it to an `AdmissionConfiguration` file that:
- references the webhook via a kubeconfig,
- sets `defaultAllow: false` in the ImagePolicy plugin config,
Mount both files into the static pod and enable the plugin. (No live backend is required — grading is on the admission plugin being enabled and the config file being correctly referenced. With `defaultAllow: false` and no reachable backend, new pod creation should be **rejected** — demonstrate that, then set `defaultAllow: true` to restore usability if you want the cluster functional for later tasks.)
> Verify: with `defaultAllow: false`, `kubectl run t --image=nginx:1.27` is rejected by the ImagePolicyWebhook.
---
## Task 16 — Falco: custom runtime rule · Monitoring/Logging/Runtime · 7%
**Context:** Ubuntu VM (host-installed Falco) — this mirrors the classic CKS Falco task.
**Setup:** install Falco on the VM (latest, v0.44.x, modern eBPF):
```bash
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" | sudo tee /etc/apt/sources.list.d/falcosecurity.list
sudo apt update && sudo apt install -y falco
```
(If you'd rather do it in-cluster: `helm install falco falcosecurity/falco -n falco --create-namespace --set driver.kind=modern_ebpf` — but the on-VM path is closer to the exam.)
**Task:**
1. Add a **custom Falco rule** (in `/etc/falco/falco_rules.local.yaml`) named `Shell in container` that fires at priority `WARNING` whenever a shell (`sh`, `bash`) is spawned inside any container, with output including the container id, container name, and the spawned process name.
2. Restart Falco, trigger the rule (spawn a shell in a container), and **capture the alert line** to `/opt/course/falco-hits.txt`.
> Verify: the rule appears in Falco's loaded rules; triggering a shell produces a `Shell in container` alert containing the container name and proc name.
---
## Task 17 — API server audit logging · Monitoring/Logging/Runtime · 6%
**Context:** `kind-cks` (control-plane)
**Setup:** none.
**Task:**
Configure kube-apiserver audit logging:
1. Write an audit policy at `/etc/kubernetes/audit/policy.yaml` that:
- logs `Metadata` level for `secrets`, `configmaps` in all namespaces,
- logs `RequestResponse` level for `pods` changes (create/update/delete),
- logs everything else at `None` (don't log read-only noise).
2. Enable it on the apiserver: `--audit-policy-file`, `--audit-log-path=/var/log/kubernetes/audit/audit.log`, `--audit-log-maxage=7`, `--audit-log-maxbackup=2`, `--audit-log-maxsize=50`. Mount the policy (read-only) and the log dir (read-write) into the static pod.
> Verify: after the apiserver restarts, `kubectl -n vault-ns get secret pre-existing` then check `/var/log/kubernetes/audit/audit.log` inside the control-plane contains a metadata-level entry for that secret and no `RequestResponse` bodies for it.
---
### Scoring yourself
- 12+/17 fully correct ≈ comfortable pass.
- Time-box: if a control-plane restart task (5, 10, 15, 17) breaks the apiserver, you lose access to *everything* — practice reverting fast (`docker exec ... ` edit back, or `cp` a backup of the manifest you took first). **Always `cp` the manifest before editing.**
Answers in `answers/solutions.md`. Don't peek until you've timed a full run.

View File

@@ -0,0 +1,22 @@
# CKS mock cluster — 1 control-plane + 2 workers, k8s v1.35
# Pinned digest from kind v0.32.0 (default 1.36) — we force 1.35.5 to match the exam.
# If `kind load`/image pull complains, use kind >= v0.32.0.
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: cks
nodes:
- role: control-plane
image: kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
extraMounts:
# host dir for audit policy / encryption config / admission config you'll author on the VM
- hostPath: ./exam-files
containerPath: /etc/kubernetes/exam
extraPortMappings:
# so you can curl the Ingress task from the VM
- containerPort: 30443
hostPort: 30443
protocol: TCP
- role: worker
image: kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
- role: worker
image: kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env bash
# CKS mock — seed the cluster with the objects each task expects. Idempotent-ish.
set -euo pipefail
kubectl config use-context kind-cks >/dev/null
echo "[*] Namespaces..."
for ns in prod web dev apps restricted-ns images sysh runtime; do
kubectl create ns "$ns" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
done
# ---------- Task 1: NetworkPolicy ----------
kubectl -n prod apply -f - >/dev/null <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: db, namespace: prod, labels: {app: db}}
spec:
replicas: 1
selector: {matchLabels: {app: db}}
template:
metadata: {labels: {app: db}}
spec:
containers:
- name: db
image: hashicorp/http-echo:1.0
args: ["-text=db-ok", "-listen=:5432"]
ports: [{containerPort: 5432}]
---
apiVersion: v1
kind: Service
metadata: {name: db, namespace: prod}
spec:
selector: {app: db}
ports: [{port: 5432, targetPort: 5432}]
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: frontend, namespace: prod, labels: {app: frontend}}
spec:
replicas: 1
selector: {matchLabels: {app: frontend}}
template:
metadata: {labels: {app: frontend}}
spec: {containers: [{name: c, image: curlimages/curl:8.11.1, command: ["sleep","infinity"]}]}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: attacker, namespace: prod, labels: {app: attacker}}
spec:
replicas: 1
selector: {matchLabels: {app: attacker}}
template:
metadata: {labels: {app: attacker}}
spec: {containers: [{name: c, image: curlimages/curl:8.11.1, command: ["sleep","infinity"]}]}
EOF
# ---------- Task 2: Ingress TLS ----------
kubectl -n web apply -f - >/dev/null <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: hello, namespace: web}
spec:
replicas: 1
selector: {matchLabels: {app: hello}}
template:
metadata: {labels: {app: hello}}
spec:
containers:
- name: hello
image: hashicorp/http-echo:1.0
args: ["-text=hello-tls", "-listen=:5678"]
ports: [{containerPort: 5678}]
---
apiVersion: v1
kind: Service
metadata: {name: hello, namespace: web}
spec:
selector: {app: hello}
ports: [{port: 80, targetPort: 5678}]
EOF
# ---------- Task 4: RBAC least-privilege (over-permissive to fix) ----------
kubectl -n dev apply -f - >/dev/null <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata: {name: ci-runner, namespace: dev}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: ci-runner-role, namespace: dev}
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"] # WAY too broad — task: restrict to get/list/watch on pods & configmaps
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {name: ci-runner-rb, namespace: dev}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: ci-runner-role}
subjects: [{kind: ServiceAccount, name: ci-runner, namespace: dev}]
EOF
# ---------- Task 6: automountServiceAccountToken ----------
kubectl -n apps apply -f - >/dev/null <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata: {name: web-sa, namespace: apps}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: web, namespace: apps}
spec:
replicas: 2
selector: {matchLabels: {app: web}}
template:
metadata: {labels: {app: web}}
spec:
serviceAccountName: web-sa
containers: [{name: c, image: nginx:1.27, ports: [{containerPort: 80}]}]
EOF
# ---------- Task 9: Pod Security Admission (violating workload) ----------
kubectl -n restricted-ns apply -f - >/dev/null <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: payments, namespace: restricted-ns}
spec:
replicas: 1
selector: {matchLabels: {app: payments}}
template:
metadata: {labels: {app: payments}}
spec:
containers:
- name: c
image: nginx:1.27
securityContext:
privileged: true # violates restricted
allowPrivilegeEscalation: true
EOF
# ---------- Task 13: Trivy image scan (mixed vuln levels) ----------
kubectl -n images apply -f - >/dev/null <<'EOF'
apiVersion: v1
kind: Pod
metadata: {name: legacy-app, namespace: images, labels: {scan: "true"}}
spec:
containers: [{name: c, image: nginx:1.19.0}] # old, HIGH/CRITICAL CVEs
---
apiVersion: v1
kind: Pod
metadata: {name: old-debian, namespace: images, labels: {scan: "true"}}
spec:
containers: [{name: c, image: debian:10}] # EOL, plenty of CVEs
---
apiVersion: v1
kind: Pod
metadata: {name: clean-app, namespace: images, labels: {scan: "true"}}
spec:
containers: [{name: c, image: nginx:1.27}] # relatively clean
EOF
echo "[*] Seed complete. Namespaces: prod web dev apps restricted-ns images sysh runtime"
echo "[*] Note: Kyverno / Falco / gVisor get installed inside their own task setup blocks."

View File

@@ -0,0 +1,545 @@
# CKS Mock — Solutions (k8s v1.35)
Reference answers. There's usually more than one valid path; these are the exam-fast ones. Control-plane file edits assume `docker exec -it cks-control-plane bash` (or `cks-worker` for node tasks).
> **Before ANY apiserver edit:** `cp /etc/kubernetes/manifests/kube-apiserver.yaml ~/kube-apiserver.yaml.bak` inside the node. If the pod won't come back, `cp` it back.
---
## Task 1 — NetworkPolicy
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: default-deny, namespace: prod}
spec:
podSelector: {}
policyTypes: [Ingress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-frontend, namespace: prod}
spec:
podSelector: {matchLabels: {app: db}}
policyTypes: [Ingress]
ingress:
- from:
- podSelector: {matchLabels: {app: frontend}}
ports:
- {protocol: TCP, port: 5432}
```
Verify:
```bash
kubectl -n prod exec deploy/frontend -- curl -s --max-time 3 db:5432 # -> db-ok
kubectl -n prod exec deploy/attacker -- curl -s --max-time 3 db:5432 # -> timeout
```
Gotcha: kind's default CNI (kindnetd) enforces NetworkPolicy in recent versions; if your kindnet build doesn't, swap to Calico. On the exam CNI always enforces.
---
## Task 2 — Ingress TLS
```bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt -subj "/CN=hello.cks.local/O=cks"
kubectl -n web create secret tls hello-tls --cert=tls.crt --key=tls.key
```
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello
namespace: web
spec:
ingressClassName: nginx
tls:
- hosts: [hello.cks.local]
secretName: hello-tls
rules:
- host: hello.cks.local
http:
paths:
- path: /
pathType: Prefix
backend: {service: {name: hello, port: {number: 80}}}
```
Verify (port depends on how ingress-nginx is exposed in kind — NodePort or the hostPort mapping):
```bash
kubectl -n ingress-nginx get svc ingress-nginx-controller
curl -k --resolve hello.cks.local:443:127.0.0.1 https://hello.cks.local/
```
---
## Task 3 — kube-bench remediation
Run it (Job is easiest inside kind):
```bash
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs -f job/kube-bench | less
```
Or download the binary and `kube-bench run --targets master,node` on the node.
Kubelet — edit on the node `/var/lib/kubelet/config.yaml`:
```yaml
authentication:
anonymous:
enabled: false
webhook:
enabled: true
authorization:
mode: Webhook
readOnlyPort: 0
```
```bash
systemctl restart kubelet
```
API server — `/etc/kubernetes/manifests/kube-apiserver.yaml`, add to `command`:
```
- --profiling=false
```
Re-run kube-bench; those checks flip to PASS.
---
## Task 4 — RBAC least privilege
Replace the Role (binding already points at `ci-runner-role`):
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: ci-runner-role, namespace: dev}
rules:
- apiGroups: [""]
resources: ["pods", "configmaps"]
verbs: ["get", "list", "watch"]
```
```bash
kubectl -n dev replace -f role.yaml # or edit
kubectl auth can-i get pods --as=system:serviceaccount:dev:ci-runner -n dev # yes
kubectl auth can-i delete pods --as=system:serviceaccount:dev:ci-runner -n dev # no
kubectl auth can-i '*' secrets --as=system:serviceaccount:dev:ci-runner -n dev # no
```
---
## Task 5 — API server hardening
`/etc/kubernetes/manifests/kube-apiserver.yaml`, in `command`:
```
- --anonymous-auth=false
- --profiling=false
- --enable-admission-plugins=NodeRestriction # merge with existing list, comma-separated
```
If `--enable-admission-plugins` already exists, append `,NodeRestriction` to it — don't add a second flag.
Wait for restart:
```bash
kubectl -n kube-system get pod -l component=kube-apiserver -w
```
Note: with `--anonymous-auth=false`, `/healthz` and `/livez` still work for the kubelet because it authenticates; unauthenticated curl gets 401.
---
## Task 6 — automountServiceAccountToken
```bash
kubectl -n apps patch sa web-sa -p '{"automountServiceAccountToken": false}'
kubectl -n apps rollout restart deploy/web
kubectl -n apps exec deploy/web -- ls /var/run/secrets/kubernetes.io/serviceaccount 2>&1 # No such file
```
(Alternatively set `automountServiceAccountToken: false` in the pod template — SA-level is cleaner here and is what the task asks.)
---
## Task 7 — AppArmor (GA securityContext API)
Load profile on the worker:
```bash
docker exec cks-worker apparmor_parser -q /root/deny-write
docker exec cks-worker aa-status | grep k8s-deny-write # enforce
```
Pod — **use the appArmorProfile field, not the beta annotation**:
```yaml
apiVersion: v1
kind: Pod
metadata: {name: locked, namespace: sysh}
spec:
nodeName: cks-worker
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: k8s-deny-write
containers:
- name: c
image: busybox:1.36
command: ["sh","-c","sleep 1h"]
```
```bash
kubectl -n sysh exec locked -- sh -c 'echo x > /tmp/x' # Permission denied
```
Gotcha: the profile only exists on `cks-worker`, so you must pin the pod there or the load must be on every schedulable node.
---
## Task 8 — seccomp
```yaml
apiVersion: v1
kind: Pod
metadata: {name: traced, namespace: sysh}
spec:
nodeName: cks-worker
securityContext:
seccompProfile:
type: Localhost
localhostProfile: profiles/audit.json
containers:
- {name: c, image: busybox:1.36, command: ["sh","-c","sleep 1h"]}
---
apiVersion: v1
kind: Pod
metadata: {name: defaulted, namespace: sysh}
spec:
nodeName: cks-worker
securityContext:
seccompProfile: {type: RuntimeDefault}
containers:
- {name: c, image: busybox:1.36, command: ["sh","-c","sleep 1h"]}
```
`localhostProfile` is relative to the kubelet seccomp root (`/var/lib/kubelet/seccomp`), so `profiles/audit.json` resolves to `/var/lib/kubelet/seccomp/profiles/audit.json`.
---
## Task 9 — Pod Security Admission (restricted)
Label the namespace:
```bash
kubectl label ns restricted-ns \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted --overwrite
```
Fix the Deployment pod template to satisfy `restricted`:
```yaml
spec:
securityContext:
runAsNonRoot: true
seccompProfile: {type: RuntimeDefault}
containers:
- name: c
image: nginx:1.27
securityContext:
allowPrivilegeEscalation: false
privileged: false
runAsNonRoot: true
capabilities: {drop: ["ALL"]}
```
nginx:1.27 wants to bind :80 — under `runAsNonRoot` it'll fail unless you use an unprivileged image/port. For the exam-grade "complies + rolls out," use `nginxinc/nginx-unprivileged:1.27` (listens on 8080) or set `runAsUser: 101`. The graded bit is restricted-compliance; the unprivileged image makes it actually run.
---
## Task 10 — Encryption at rest
Key + config on the control-plane:
```bash
mkdir -p /etc/kubernetes/enc
head -c 32 /dev/urandom | base64 # copy the value
cat > /etc/kubernetes/enc/enc.yaml <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
providers:
- aescbc:
keys:
- name: key1
secret: <PASTE_BASE64_32B_KEY>
- identity: {}
EOF
```
apiserver manifest — add flag + volume + mount:
```yaml
# command:
- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml
# volumes:
- name: enc
hostPath: {path: /etc/kubernetes/enc, type: DirectoryOrCreate}
# volumeMounts:
- name: enc
mountPath: /etc/kubernetes/enc
readOnly: true
```
After apiserver is back, rewrite all secrets:
```bash
kubectl get secrets -A -o json | kubectl replace -f -
```
Verify with etcdctl inside the control-plane:
```bash
ETCDCTL_API=3 etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
get /registry/secrets/vault-ns/pre-existing | hexdump -C | head
# expect: k8s:enc:aescbc:v1:key1:... and NO plaintext SUPERSECRET
```
---
## Task 11 — RuntimeClass
```yaml
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata: {name: gvisor}
handler: runsc
---
apiVersion: v1
kind: Pod
metadata: {name: sandboxed, namespace: runtime}
spec:
runtimeClassName: gvisor
containers:
- {name: c, image: nginx:1.27}
```
Stays Pending in kind (no `runsc` handler in containerd) — expected. Config is the deliverable.
---
## Task 12 — ValidatingAdmissionPolicy
```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: {name: pod-hardening}
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE","UPDATE"]
resources: ["pods"]
validations:
- expression: >-
!object.spec.containers.exists(c,
has(c.securityContext) && has(c.securityContext.privileged)
&& c.securityContext.privileged == true)
message: "privileged containers are not allowed"
- expression: >-
!has(object.spec.volumes) ||
!object.spec.volumes.exists(v, has(v.hostPath))
message: "hostPath volumes are not allowed"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: {name: pod-hardening-binding}
spec:
policyName: pod-hardening
validationActions: ["Deny"]
matchResources: {} # all namespaces
```
Also check `initContainers`/`ephemeralContainers` if you want it airtight; for the task, `containers` is what's graded. Test:
```bash
kubectl run bad --image=nginx:1.27 --privileged # denied
kubectl run ok --image=nginx:1.27 # admitted
```
---
## Task 13 — Trivy scan & evict
```bash
for img in nginx:1.19.0 debian:10 nginx:1.27; do
echo "== $img =="
trivy image --severity HIGH,CRITICAL --quiet --scanners vuln "$img" | tail -5
done
```
`nginx:1.19.0` and `debian:10` will show HIGH/CRITICAL; delete those pods:
```bash
kubectl -n images delete pod legacy-app old-debian
kubectl -n images get pods # clean-app remains
```
Exam tip: map image→pod first (`kubectl -n images get pods -o custom-columns=POD:.metadata.name,IMG:.spec.containers[*].image`), scan each, delete by HIGH/CRITICAL count > 0.
---
## Task 14 — Kyverno policies
```yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: {name: disallow-latest-tag}
spec:
validationFailureAction: Enforce
background: false
rules:
- name: require-explicit-tag
match: {any: [{resources: {kinds: [Pod]}}]}
validate:
message: "images must not use :latest or an empty tag"
pattern:
spec:
containers:
- image: "!*:latest"
- name: require-tag-present
match: {any: [{resources: {kinds: [Pod]}}]}
validate:
message: "image tag is required"
pattern:
spec:
containers:
- image: "*:*"
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: {name: allowed-registries}
spec:
validationFailureAction: Enforce
background: false
rules:
- name: only-cks-registry
match: {any: [{resources: {kinds: [Pod]}}]}
validate:
message: "images must come from registry.cks.local"
pattern:
spec:
containers:
- image: "registry.cks.local/*"
```
Test:
```bash
kubectl -n team-a run bad --image=nginx:latest # denied (latest)
kubectl -n team-a run bad2 --image=docker.io/nginx:1.27 # denied (registry)
kubectl -n team-a run good --image=registry.cks.local/nginx:1.27 # admitted
```
Note: recent Kyverno also exposes the newer `validate.foreach`/CEL syntax; the pattern form above is the fastest to write under time.
---
## Task 15 — ImagePolicyWebhook
`/etc/kubernetes/admission/admission-config.yaml`:
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
configuration:
imagePolicy:
kubeConfigFile: /etc/kubernetes/admission/imagepolicy-kubeconfig.yaml
allowTTL: 50
denyTTL: 50
retryBackoff: 500
defaultAllow: false
```
`/etc/kubernetes/admission/imagepolicy-kubeconfig.yaml`:
```yaml
apiVersion: v1
kind: Config
clusters:
- name: image-checker
cluster:
server: https://image-checker.local/check # no live backend needed for the drill
users:
- name: apiserver
contexts:
- name: default
context: {cluster: image-checker, user: apiserver}
current-context: default
```
apiserver manifest:
```yaml
# command:
- --enable-admission-plugins=NodeRestriction,ImagePolicyWebhook # merge with existing
- --admission-control-config-file=/etc/kubernetes/admission/admission-config.yaml
# volume + mount for /etc/kubernetes/admission (readOnly)
```
With `defaultAllow: false` and no reachable backend, `kubectl run t --image=nginx:1.27` → rejected by ImagePolicyWebhook. Flip to `defaultAllow: true` afterwards if you want the cluster usable.
---
## Task 16 — Falco custom rule
`/etc/falco/falco_rules.local.yaml`:
```yaml
- rule: Shell in container
desc: Detect a shell spawned inside a container
condition: >
spawned_process and container
and proc.name in (sh, bash)
output: >
Shell in container (container_id=%container.id container_name=%container.name
proc=%proc.name user=%user.name)
priority: WARNING
tags: [container, shell, mitre_execution]
```
```bash
systemctl restart falco # or: falco -r /etc/falco/falco_rules.yaml -r /etc/falco/falco_rules.local.yaml
# trigger:
kubectl run trigger --image=busybox:1.36 -- sh -c 'sleep 1h'
kubectl exec -it trigger -- sh
# capture:
journalctl -u falco | grep "Shell in container" | tail -1 > /opt/course/falco-hits.txt
```
Gotchas: `container` macro excludes host processes; `spawned_process` = execve. If running Falco via systemd, alerts land in `journalctl -u falco` (and/or `/var/log/syslog`) — grab from whichever your output channel is. Check the rule loaded: `falco --list | grep -i "Shell in container"` or watch startup logs for parse errors.
---
## Task 17 — Audit logging
`/etc/kubernetes/audit/policy.yaml`:
```yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages: ["RequestReceived"]
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["pods"]
verbs: ["create","update","patch","delete"]
- level: Metadata
resources:
- group: ""
resources: ["secrets","configmaps"]
- level: None
```
apiserver manifest — flags:
```yaml
- --audit-policy-file=/etc/kubernetes/audit/policy.yaml
- --audit-log-path=/var/log/kubernetes/audit/audit.log
- --audit-log-maxage=7
- --audit-log-maxbackup=2
- --audit-log-maxsize=50
```
volumes + mounts:
```yaml
# volumes:
- name: audit-policy
hostPath: {path: /etc/kubernetes/audit, type: DirectoryOrCreate}
- name: audit-logs
hostPath: {path: /var/log/kubernetes/audit, type: DirectoryOrCreate}
# volumeMounts:
- name: audit-policy
mountPath: /etc/kubernetes/audit
readOnly: true
- name: audit-logs
mountPath: /var/log/kubernetes/audit
readOnly: false
```
Verify inside the control-plane:
```bash
kubectl -n vault-ns get secret pre-existing
tail -f /var/log/kubernetes/audit/audit.log | grep pre-existing # Metadata level, no body
```
Big gotcha: the audit-logs mount must be `readOnly: false` or the apiserver crashloops silently. And the log dir hostPath is *inside the control-plane container* — that's fine here.
---
### Reset between attempts
```bash
kind delete cluster --name cks
./bootstrap.sh && ./seed.sh
```

View File

@@ -0,0 +1,403 @@
# Task 1
A pod has been created in the omni namespace, but it has a few issues that need to be addressed.
The pod has been created with more permissions than it needs.
It allows read access to the /usr/share/nginx/html/internal directory, making the Internal Site publicly accessible.
To verify this, click the Site button (above the terminal) and add /internal/ to the end of the URL.
Use the below recommendations to resolve this.
Use the AppArmor profile created at /etc/apparmor.d/frontend to restrict access to the internal site.
The omni namespace has several service accounts. Apply the principle of least privilege and use the service account with the minimum privileges (excluding the default service account).
Once the pod is recreated with the correct service account, delete the other unused service accounts in the omni namespace (excluding the default service account).
Do not create a new service account or use the default service account.
Is the correct service account used?
Are obsolete service accounts deleted?
Is the internal-site restricted?
Is the pod running?
## Solution
```bash
apparmor_parser /etc/apparmor.d/frontend
apparmor_status | grep frontend
cat p.yaml
apiVersion: v1
kind: Pod
metadata:
generation: 1
labels:
run: nginx
name: frontend-site
namespace: omni
spec:
containers:
- image: nginx:alpine
imagePullPolicy: IfNotPresent
name: nginx
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /usr/share/nginx/html
name: test-volume
dnsPolicy: ClusterFirst
enableServiceLinks: true
nodeName: controlplane
preemptionPolicy: PreemptLowerPriority
priority: 0
restartPolicy: Always
schedulerName: default-scheduler
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: restricted-frontend
serviceAccount: frontend-default
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- hostPath:
path: /data/pages
type: Directory
name: test-volume
- name: kube-api-access-z88t9
projected:
defaultMode: 420
sources:
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace
k auth can-i --list --as=system:serviceaccount:omni:frontend-default
```
# Task 2
A pod has been created in the orion namespace. It uses secrets as environment variables. Extract the decoded secret for the CONNECTOR_PASSWORD and place it under /root/CKS/secrets/CONNECTOR_PASSWORD.
You are not yet done; instead of using secrets as an environment variable, mount the secret as a read-only volume at the path /mnt/connector/password, which the application can then use.
Is the pod secured?
Is the secret mounted as read-only?
Is the existing secret extracted to file?
# Task 3
The Release Engineering Team has shared some YAML manifests and Dockerfiles with you for review. These files are located under /opt/course/.
As a container security expert, your task is to perform a manual static analysis and identify possible security issues related to unwanted credential exposure. Note that running processes as root is not a concern in this task.
Record the filenames containing issues in /opt/course/security-issues.txt.
Note: Assume that all referenced files, folders, secrets, and volume mounts are present in the Dockerfiles and YAML manifests. You can ignore any syntax or logic errors.
Are the filenames containing issues written in /opt/course/security-issues.txt?
# Task 4
Create a new pod named audit-nginx in the default namespace using the nginx:alpine image. Secure the syscalls that this pod can use by using the audit.json seccomp profile in the pod's security context.
The audit.json file is provided at the /root/CKS directory. Move it into the profiles directory inside the default seccomp directory before creating the pod.
Does audit-nginx use the right image?
Is the pod running?
Does the pod use the correct seccomp profile?
# Task 5
We have identified a few issues with our kubernetes setup and need your help in fixing them.
Fix the following issues on kubelet:
Kubelet service file permission issues
Kubelet config.yaml permission issues
Fix the following issues on etcd:
Incorrect ownership of the etcd directory
Fix the following issues on the controlplane node:
Incorrect value of the profiling argument for:
kube-controller-manager
kube-scheduler
Kube-bench is installed, and its config files are available under /opt/kube-bench. Use the cis-1.10 benchmark with the current Kubernetes version.
Note: Only fix issues that have the status FAIL, except issue number 1.2.5. Also, ignore the issues with policies.
Is the apiserver working?
Are the issues fixed for the controller and scheduler on the controlplane node?
Is the issue pertaining to the etcd data directory fixed?
Is the kubelet service file issue fixed?
Is the kubelet config file issue fixed?
# Task 6
There is suspicious activity in the cluster involving one of the pods running the httpd:2.4-alpine image.
Falco generates frequent alerts that start with: File below a known binary directory opened for writing.
Identify the rule causing this alert and update it as per the below requirements:
Set the rule priority to CRITICAL (Note: Falco will format this as "Critical" in log output)
The rule output must be:
File below a known binary directory opened for writing (user_id=%user.uid file_updated=%fd.name command=%proc.cmdline)
Configure alerts to be logged to: /opt/security_incidents/alerts.log.
Do not update the default rules file directly. Instead, use the falco_rules.local.yaml file to override.
Expected log format:
<timestamp>: Critical File below a known binary directory opened for writing (user_id=0 file_updated=/bin/sleep command=tar -xmf - -C /bin)
Note: After updating the alert rule, it may take up to a minute for the alerts to appear in the new log location.
# Task 7
A deployment named fruits in the namespace salad has three containers:
apple
banana, and
kiwi
One of these containers has the package curl installed. Identify which container has that package from the running containers, and create an SBOM SPDX for the container's image.
Use the tarball archive of that particular image stored under /root/ImageTarballs directory for generating the SPDX JSON. The archives have names matching their images.
Save the output in ~/bugged-fruit.spdx. Save the container name in ~/bugged-container.txt.
Note: bom as well as all its required dependencies have already been installed.
Is the SPDX JSON SBOM for the image with curl installed stored at ~/bugged-fruit.spdx?
Is the container name stored in ~/bugged-container.txt?
# Task 8
Create a service account named bot-sa in the namespace automated. Make sure that this service account does not get automatically mounted to workloads.
A workload named sweeper is also in the automated namespace. Set the deployment's service account to the newly created service account, and mount the service account token as a projected volume. Do not change any other fields in the deployment.
Has the service account bot-sa been created without auto-mount?
Has the bot-sa service account been set as the deployment's service account?
Has the bot-sa service account token been mounted to the deployment as a projected volume?
No default auto-mount on the Pod ?
# Task 9
A deployment named web-server is running in the restricted namespace.
Identify the reason why the deployment is not in a running state; fix the issue so that it can be in a running state.
Do not change the namespace labels or container image.
Has the deployment been fixed, and is the pod in a running state?
Namespace still enforces restricted ?
# Task 10
Deployment web-app is running in the products namespace.
Database product-db is running in the database namespace.
Create a network policy named allow-traffic-to-products that allows traffic from product-db to the web-app workload, as well as all traffic originating from the payments namespace.
Utilize the labels applied on the relevant resources.
Has the network policy allow-traffic-to-products been created?
Is traffic allowed from product-db to web-app?
Is traffic allowed from payments-app to web-app?
Is traffic not allowed from a test app in the database namespace?
Is traffic not allowed from a test app in the default namespace?
# Task 11
Edit the gamma deployment in the galaxy namespace to ensure that all containers meet the following requirements:
Run as user 1001
Do not allow privilege escalation
Mount their file systems as read-only
Is the deployment edited to enable both containers to run as user 1001?
Is the deployment edited to deny privilege escalation on both containers?
Is the deployment edited to mount read-only file systems on both containers?
# Task 12
A deployment rocket-server is exposed using the service of the same name in the space namespace.
Create an ingress resource named rocket-ingress to load balance the incoming traffic to the workload on path /.
Use the hostname rocket-server.local in the Ingress rules.
Utilize the TLS certificate stored in a secret named rocket-tls in the space namespace so that it enables TLS traffic on that ingress resource.
Is the ingress resource correctly created?
Has the ingress TLS secret been attached?
Does the ingress route to the correct path and port?
Is curl working for the HTTPS connection via the ingress?
# Task 13
A developer named martin needs access to work on the dev-a, dev-b, and dev-z namespaces. He should have the ability to carry out any operation on any pod in the dev-a and dev-b namespaces. However, on the dev-z namespace, he should only have the permission to get and list the pods.
The current setup is too permissive and violates the above condition. Use the above requirement and secure martin's access in the cluster. You may re-create objects; however, ensure to use the same names as the ones in currently in effect.
Does martin have unrestricted access to all pods in dev-a?
Does martin have unrestricted access to all pods in dev-b?
Can martin only list and get pods in dev-z?
# Task 14
You need to enable auditing on this cluster. A basic policy file is available at /etc/kubernetes/cluster-policy.yaml.
The logs should be stored at /var/log/cluster-audit.log. The logs should be retained for 10 days and should not exceed 10MB. A maximum of 3 files should be kept at a time.
After you enable auditing on the cluster, update the basic policy file to track the following:
Delete activity on secrets in the kube-system namespace at the Metadata level
Changes to deployments in the default namespace at the Request level
All other requests at the Metadata level
Make sure your changes to the policy file are in effect.
Note: A copy of the kube-apiserver.yaml is kept in ~/ so that you can revert if the configuration goes wrong. Make sure kube-apiserver is working fine for the sake of grading the exam.
Does /var/log/cluster-audit.log exist with the logs written to it?
Does auditing follow the mentioned parameters as specified in the question?
Is the delete activity on secrets tracked at the Metadata level in the kube-system namespace?
Is the edit activity on deployments tracked at the Request level in the default namespace?
# Task 15
We need to ensure that when pods are created in this cluster, they cannot use the latest image tag, irrespective of the repository being used.
To achieve this, a simple Admission Webhook Server has been developed and deployed. A service called image-bouncer-webhook is deployed in the cluster. This Webhook server ensures that the developers of the team cannot use the latest image tag. Use the following specs to integrate it with the cluster using an ImagePolicyWebhook:
Create a new admission configuration file at /etc/admission-controllers/admission-configuration.yaml
The kubeconfig file with the credentials to connect to the webhook server is located at /root/CKS/ImagePolicy/admission-kubeconfig.yaml. Note: The /root/CKS/ImagePolicy/ directory is already mounted on the kube-apiserver at path /etc/admission-controllers, so reference that path in reference the admission configuration.
Ensure that if the latest tag is used, the request must be rejected at all times.
Enable the Admission Controller.
Finally, delete the existing pod in the magnum namespace that violates the policy and recreate it, ensuring the same image but using tag 1.27.
NOTE: If the kube-apiserver becomes unresponsive, this can affect the validation of this exam. In such a case, restore the kube-apiserver using the backup file created at: /root/backup/kube-apiserver.yaml. Wait for the API to be available again and proceed.
Is the ImagePolicyWebhook enabled and the API server running?
Is the policy implemented?
Is the pod recreated with the correct image?
Is the pod running?
## Solution
Create the below admission-configuration.yaml inside /root/CKS/ImagePolicy directory in the controlplane node:
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
configuration:
imagePolicy:
kubeConfigFile: /etc/admission-controllers/admission-kubeconfig.yaml
allowTTL: 50
denyTTL: 50
retryBackoff: 500
defaultAllow: false
The /root/CKS/ImagePolicy is mounted at the path /etc/admission-controllers directory in the kube-apiserver. So, you can directly place the files under /root/CKS/ImagePolicy.
Here is a snippet of the volume and volumeMounts (already added to apiserver config, you don't need to do anything here):
containers:
.
.
.
volumeMounts:
- mountPath: /etc/admission-controllers
name: admission-controllers
readOnly: true
volumes:
- hostPath:
path: /root/CKS/ImagePolicy/
type: DirectoryOrCreate
name: admission-controllers
Next, update the kube-apiserver command flags and add ImagePolicyWebhook to the enable-admission-plugins flag. Use the configuration file that was created in the previous step as the value of admission-control-config-file.
Note: Remember, this command will be run inside the kube-apiserver container, so the path must be /etc/admission-controllers/admission-configuration.yaml (mounted from /root/CKS/ImagePolicy in controlplane).
Edit the kube-apiserver manifest file:
vi /etc/kubernetes/manifests/kube-apiserver.yaml
to add the following flags under spec.containers.command:
- --admission-control-config-file=/etc/admission-controllers/admission-configuration.yaml
- --enable-admission-plugins=NodeRestriction,ImagePolicyWebhook
In case we mess up while solving the question, API server could become unresponsive.
For example:
The connection to the server controlplane:6443 was refused - did you specify the right host or port?
In such case scenario restore kube-apiserver to it's default state using the backup provided at /root/backup/kube-apiserver.yaml
Run the below command to restore the kube-apiserver initial state:
cp -v /root/backup/kube-apiserver.yaml /etc/kubernetes/manifests
Remediate the Non-Compliant Pod
After the ImagePolicyWebhook is active, you must fix the existing app-0403 pod in the magnum namespace, as it violates the new policy, and , Admission controllers only check new requests; they don't affect pods that are already running.
Get the Pod's Definition
First, get the YAML definition of the running pod and save it to a file.
kubectl get pod app-0403 -n magnum -o yaml > app-0403-fix.yaml
Clean and Edit the YAML
Now, edit the file. The YAML you just saved contains live, system-managed fields. You must clean it up before you can re-apply it.
vi app-0403-fix.yaml
Change:
image: gcr.io/google-containers/busybox
to:
image: gcr.io/google-containers/busybox:1.27
Save and exit the file.
Delete the Old Pod
You must delete the running pod before you can create a new one with the same name.
kubectl delete pod app-0403 -n magnum
Apply the Fixed Manifest
Finally, create the new, compliant pod from your corrected YAML file. The webhook will inspect this request and allow it.
kubectl apply -f app-0403-fix.yaml

View File

@@ -0,0 +1,104 @@
# Task 1
Weight: 3
You are setting up a new Kubernetes cluster and need to secure Docker as part of the cluster setup.
Ensure that docker runs under the "root" group and that no external TCP connections are allowed to the docker daemon.
Ensure the configuration is persistent across restarts.
Is docker modified to run as part of the root group?
Has the docker daemon been modified to have no external TCP connections?
# Task 3
Create a service account named bot-sa in the automated namespace. Ensure that it does not get automatically mounted to workloads.
A workload named sweeper also exists in the same namespace. Update this deployment to use the newly created service account, and mount the service account token as a projected volume. Do not change any other fields in the deployment.
Has the service account bot-sa been created without auto-mount?
Has the bot-sa service account been set as the deployment's service account?
Has the bot-sa service account token been mounted to the deployment as a projected volume?
No default auto-mount on the Pod ?
# Task 6
You have an existing Kubernetes setup with the following services running:
Namespace:
system-hardening
Pods:
nginx-internal (Accessible internally)
nginx-external (Exposed externally via NodePort
service)
Services:
nginx-internal-service (Exposed as ClusterIP - internal-only)
nginx-external-service (Exposed as NodePort - accessible externally)
Objective:
Your task is to disable or unexpose ports to minimize external access to unnecessary services.
Has the service type for nginx-external-service been changed to ClusterIP?
# Task 7
A deployment named web-server is running in namespace restricted.
Identify why the deployment is not in a running state, and then fix the issue so that it is in a running state.
Has the deployment been fixed, and is the pod in a running state?
# Task 8
Configure the kubelet on the cluster2-controlplane node to disallow anonymous authentication.
The admin kubeconfig file for this cluster is located at:
/root/custom-config/admin.conf
Additionally, utilize this kubeconfig file to delete the role custom-role in namespace delta.
Ensure that, from the node, the cluster cannot be accessed with kubectl unless the --kubeconfig=/root/custom-config/admin.conf flag is explicitly provided.
Has anonymous auth been disabled on the kubelet?
Is the cluster not accessible without passing the --kubeconfig flag with kubectl?
Has the custom-role been deleted?
# Task 10
In the space namespace, a deployment rocket-server is exposed by a service of the same name.
Create an ingress resource named rocket-ingress to load balance the incoming traffic to the workload on path /.
Use the hostname rocket-server.local for the Ingress rules.
Utilize the TLS certificate stored in the secret rocket-tls in the space namespace to enable TLS traffic on that ingress resource.
Is the ingress resource correctly created?
Has the ingress TLS secret been attached?
Does the ingress route to the correct path and port?
Is curl working for the HTTPS connection via the ingress?
# Task 14
Enable auditing on this cluster using the basic policy file available at /etc/kubernetes/cluster-policy.yaml.
Store the logs at /var/log/cluster-audit.log and ensure they are retained for 10 days. The log size should not exceed 10MB and up to a maximum of 3 files should be kept at a time.
After enabling auditing on the cluster, update the basic policy file to track the following:
Delete activity on secrets in the kube-system namespace at the Metadata level
Changes to deployments in the default namespace at the Request level
All other requests at the Metadata level
Ensure that the updated policy is applied and in effect.
Note: A copy of the kube-apiserver.yaml is available in ~/ so you can revert if the configuration goes wrong. Ensure that the kube-apiserver is working correctly, as it will be required for grading the exam.
Does /var/log/cluster-audit.log exist with the logs written to it?
Does auditing follow the mentioned parameters as specified in the question?
Is the delete activity on secrets tracked at the Metadata level in the kube-system namespace?
Is the edit activity on deployments tracked at the Request level in the default namespace?

View File

@@ -0,0 +1,242 @@
# Task 1
A pod called redis-backend has been created in the prod-x12cs namespace. It has been exposed as a service of type ClusterIP. The pod listens on TCP port 6379.
Create a network policy called allow-redis-access to lock down access to this pod only for the following:
Any pod in the same namespace with the label backend=prod-x12cs
All pods in the prod-yx13cs namespace
Ensure that traffic is only allowed on TCP port 6379.
All other incoming connections should be blocked.
Use the existing labels when creating the network policy.
Is Network Policy applied on the correct pods?
Is the incoming traffic allowed from pods in prod-yx13cs namespace?
Is the incoming traffic allowed from pods with label backend=prod-x12cs ?
Is the ingress traffic restricted to port 6379?
# Task 2
There is an existing CiliumNetworkPolicy
default-allow in the namespace team-azure, which allows all traffic.
In the namespace team-azure, create a CiliumNetworkPolicy as follows:
Create a Layer 3 policy named p1 that denies outgoing traffic from Pods with the label role=messenger to Pods with the label role=database.
Is the cilium policy created ?
Is the cilium network policy working as expected?
# Task 3
A pod named apps-cluster-dash has been created in the gamma namespace using a service account called cluster-view. This service account has been granted additional permissions as compared to the default service account and can view resources cluster-wide on this Kubernetes cluster. While these permissions are important for the application in this pod to work, the secret token is still mounted on this pod.
Secure the pod in such a way that the secret token is no longer mounted on this pod. You may delete and recreate the pod.
Is the pod created with a cluster-view service account?
Is the secret token not mounted in the pod?
# Task 4
A pod in the sahara namespace has generated alerts that a shell was opened inside the container.
To recognize such alerts, set the priority to ALERT and change the format of the output so that it looks like the below:
ALERT timestamp of the event without nanoseconds,User ID,the container id,the container image repository
Make sure to update the rule such that the changes persist across Falco updates.
You can refer the falco documentation Here
Are the rules updated according to the new format?
Is falco running?
# Task 5
martin is a developer who needs access to work on the dev-a, dev-b, and dev-z namespaces. He should be able to perform any operation on any pod in the dev-a and dev-b namespaces. However, in the dev-z namespace, he should only have permission to get and list the pods.
The current setup is too permissive and violates the above condition. Use the above requirement to secure martin's access in the cluster. You may re-create objects, but ensure they retain their existing names.
Does martin have unrestricted access to all pods in dev-a?
Does martin have unrestricted access to all pods in dev-b?
Can martin only list and get pods in dev-z?
# Task 6
There is a deployment named hacker in the namespace team-red, which mounts /run/containerd as a hostPath volume on the Node where it's running.
This means that the Pod can access various data about other containers running on the same Node.
To prevent this, configure the team-red namespace to enforce the baseline Pod Security Standard. Once completed, delete the Pod from the
deployment mentioned above.
Check the ReplicaSet events and write the event lines containing the reason why the Pod isn't recreated into /opt/course/logs.txt.
Note: You may see multiple identical event lines with the same error. Paste only one of them in the logs.txt file - preferably the last one.
Is the pod in the team-red namespace deleted?
Are the replicaset events written to /opt/course/logs.txt?
# Task 7
There is a Dockerfile named unsecure.Dockerfile located at /opt/course/image/.
DevSecOps has asked you to improve this image by:
Changing the base image to alpine:3.12
Not installing curl
Updating nginx to use the version constraint >=1.18.0
Running the main process as user myuser
Do not add any new lines to the Dockerfile - only modify the existing ones.
Is the Dockerfile modified?
# Task 8
A pod definition file has been created at /root/CKS/simple-pod.yaml . Use the kubesec tool to generate a report for this pod definition file and fix the major issues so that the subsequent scan passes.
Once done, generate the report again and save it to the file /root/CKS/kubesec-report.txt
Is the pod definition file fixed?
Is the pass report generated and saved at /root/CKS/kubesec-report.txt?
# Task 9
There is a deployment named rocket-server, which is exposed by a service of the same name in the space namespace.
Create an ingress resource named rocket-ingress to load balance the incoming traffic to the workload on path /.
Use the hostname rocket-server.local for the Ingress rules.
Use the TLS certificate stored in the rocket-tls secret in the space namespace to enable TLS traffic on the ingress resource.
Make sure to add the IP of the Ingress Nginx controller matching the rocket-server.local DNS to the /etc/hosts file.
Note: This cluster has no external load balancer provider, so the EXTERNAL-IP of the ingress-nginx-controller service will stay in the <pending> state - this is expected. Use the IP under the CLUSTER-IP column of that service for the /etc/hosts entry.
Is the ingress resource correctly created?
Has the ingress TLS secret been attached?
Does the ingress route to the correct path and port?
Is curl working for the HTTPS connection via the ingress?
# Task 10
Create a new pod called audit-nginx in the default namespace using the nginx image. Secure the syscalls that this pod can use by using the audit.json seccomp profile in the pod's security context.
The audit.json file is located in the /root/CKS directory.
Before creating the pod, move the file to the profiles directory inside the default seccomp directory.
Does audit-nginx use the right image?
Is the pod running?
Does the pod use the correct seccomp profile?
# Task 11
A deployment named fruits in the namespace salad has three containers:
apple
banana, and
kiwi
One of these containers has the package curl installed. Identify which container has that package from the running containers, and create an SBOM SPDX for the container's image.
Use the tarball archive of that particular image stored under /root/ImageTarballs directory for generating the SPDX JSON. The archives have names matching their images.
Save the output in ~/bugged-fruit.spdx. Save the container name in ~/bugged-container.txt.
Note: bom and all its required dependencies are already installed.
Is SPDX JSON SBOM for the image with curl installed, stored at ~/bugged-fruit.spdx?
Is the container name stored in ~/bugged-container.txt?
# Task 12
Enable auditing on this cluster using a basic policy file located at /etc/kubernetes/cluster-policy.yaml.
Configure the logs to be stored at /var/log/cluster-audit.log and retained for 10 days. The maximum size should be 10MB, and up to 3 files should be kept at a time.
After enabling auditing on the cluster, update the basic policy file to track the following:
Delete activity on secrets in the kube-system namespace at the Metadata level
Changes to deployments in the default namespace at the Request level
All other requests at the Metadata level
Ensure that the updated policy file is applied and in effect.
Note: A copy of the kube-apiserver.yaml is available in ~/ so you can revert if the configuration goes wrong. Ensure that the kube-apiserver is working correctly, as it will be required for grading the exam.
Does /var/log/cluster-audit.log exist with the logs written to it?
Does auditing follow the mentioned parameters as specified in the question?
Is the delete activity on secrets tracked at the Metadata level in the kube-system namespace?
Is the edit activity on deployments tracked at the Request level in the default namespace?
# Task 13
A pod has been created in the omni namespace, but it has a couple of issues.
The pod has been created with more permissions than it needs.
It allows read access in the /usr/share/nginx/html/internal directory, opening the internal site to be accessed publicly.
To verify this, click the Site button (above the terminal) and add /internal/ to the end of the URL.
Use the below recommendations to fix this:
Use the AppArmor profile created at /etc/apparmor.d/frontend to restrict the internal site.
Several service accounts exist in the omni namespace. Apply the principle of least privilege to select the service account with the minimum privileges (excluding the default service account).
Recreate the pod with the correct service account, and then delete the other unused service accounts in the omni namespace (excluding the default service account).
You can recreate the pod, but do not create a new service account and do not use the default service account.
Is the correct service account used?
Are the obsolete service accounts deleted?
Is the internal-site restricted?
Is the pod running?
# Task 14
The namespace encrypted has two applications, alpha and beta.
Since these applications handle critical communications, enforce strict mTLS using Istio in the encrypted namespace.
Make sure that the workloads have the istio sidecar injected.
Note: istio and istioctl have already been installed for you.
Has the istio-proxy sidecar been injected for the alpha deployment?
Has the istio-proxy sidecar been injected for the beta deployment?
Has the STRICT mTLS policy been applied?
# Task 15
In the namespace code, create a TLS secret code-secret with the following certificate and key provided:
cert: /root/custom-cert.crt
key: /root/custom-key.key
Attach that secret as a volume named secret-volume in the deployment code-server.
Is the code-secret secret created in the namespace code with the mentioned specs?
Is the secret mounted as a volume on the container in the deployment code-server?
## solution
```bash
kubectl create secret tls code-secret -n code --cert=/root/custom-cert.crt --key=/root/custom-key.key