feat(root): add Kubernetes security curriculum

Four new modules, moving ROOT 09 off PLANNED and building the
22-scenario CKS-style bank into a coherent course instead of
disconnected exam tasks — see docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md
for the CORE/GUIDED/INDEPENDENT/ADVANCED/ASSESSMENT-BANK classification
behind every placement.

root-09-m01-security-model-and-securitycontext — READY, full
gold-standard pattern: defense in depth, least privilege, hardening a
Pod with SecurityContext. Lesson, demonstration (a Pod before/after
hardening), knowledge check, guided exercise (a hardening checklist),
real Labs (CORE: non-root/drop-capabilities/read-only-fs/
pod-security-context; GUIDED: seccomp; ADVANCED: AppArmor; INDEPENDENT:
the generic pod-security-context duplicate), and a reflection.

root-09-m02-serviceaccounts-and-rbac — PARTIAL: workload identity and
least-privilege permissions. Lesson + knowledge check + real Labs.

root-09-m03-network-and-supplychain-security — PARTIAL: NetworkPolicy
as a security control (default-deny), image/registry trust, TLS
ingress. Lesson + knowledge check + real Labs.

root-09-m04-cluster-hardening-and-runtime — PARTIAL, deliberately
survey-depth: Pod Security Admission and a lighter treatment of
audit-policy/runtime-sandboxing topics that don't belong in the core
daily-driver path.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-26 09:07:07 -05:00
co-authored by Claude Sonnet 5
parent 73cea1ba1d
commit 960a638ec9
10 changed files with 633 additions and 0 deletions
@@ -0,0 +1,87 @@
# Defense in Depth & the Kubernetes Security Model
## Security is a posture, not a single setting
There is no single "make it secure" switch in Kubernetes. Real security comes from **defense in
depth** — multiple independent layers, each reducing what an attacker (or a bug) can do if an
earlier layer fails. If a container has a vulnerability that lets an attacker execute arbitrary
code, the question that actually matters is: what can that code DO once it's running? A
container running as root, with every Linux capability, on a writable filesystem, with no
restrictions at all, can do far more damage than one deliberately hardened to need almost
nothing.
## Least privilege: grant only what's actually needed
The organizing principle behind everything in this course is **least privilege**: a workload
should have exactly the permissions and capabilities it needs to do its job, and nothing more.
This sounds obvious stated plainly, but the DEFAULT behavior of a plain container is closer to
"maximum privilege unless told otherwise" — root user, all standard Linux capabilities, a
writable filesystem. Hardening a Pod means deliberately narrowing that down.
## SecurityContext: where you actually configure this
`securityContext` (settable at the Pod level and per-container) is where you declare these
restrictions:
```yaml
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: app
securityContext:
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
```
- **`runAsNonRoot` / `runAsUser`** — the container process runs as a specific non-root user
instead of root. If a container image was built to require root and you set this, it will
fail to start — which is a good, loud failure, not a silent security gap. Hardening
sometimes surfaces an image's OWN bad assumptions.
- **`capabilities: drop: ["ALL"]`** — Linux capabilities are fine-grained permissions
(`NET_BIND_SERVICE`, `SYS_ADMIN`, dozens more) a process can hold even without full root.
Dropping all of them, then adding back only the specific ones actually needed, is the
least-privilege pattern applied to capabilities specifically.
- **`readOnlyRootFilesystem: true`** — the container's own filesystem cannot be written to at
all (use an explicit `emptyDir` volume, ROOT 08, for anything that genuinely needs to write
temporary files). This closes off a whole category of attack that relies on writing a
malicious file to disk.
## Why this matters even when "nothing is currently wrong"
None of these settings fix a bug or a vulnerability — they limit the BLAST RADIUS if one exists
that you don't yet know about. A container hardened with non-root, dropped capabilities, and a
read-only filesystem gives a successful exploit dramatically less to work with than an
unhardened one, even against the exact same vulnerability. This is the entire point of defense
in depth: you don't rely on any single layer being perfect.
## Instructor Notes
**Approximate timing:** 15 minutes read; 10-12 minutes live with the demonstration.
**Demo plan:** Have the demonstration activity ready to run right after this lesson — seeing a
concrete before/after Pod definition lands the abstract "least privilege" principle far better
than the lesson text alone.
**Likely misconceptions to watch for:**
- Students sometimes think hardening settings are only relevant "if you expect to be attacked."
Reframe: defense in depth assumes SOMETHING will eventually go wrong somewhere (a
vulnerability, a bug, a misconfiguration) and prepares for that, rather than betting
everything on nothing ever going wrong.
- `runAsNonRoot: true` failing a Pod's startup is sometimes read as "the security setting is
broken." It's the opposite — it's correctly refusing to let a root-requiring image start,
which is valuable, actionable information about the image itself.
**Discussion question:** "If dropping all capabilities breaks a container that turns out to
genuinely need one specific capability, what's the right fix — removing the drop, or something
more targeted?" (Answer: add back only that ONE specific capability rather than removing the
blanket drop — least privilege means "exactly what's needed," not "give up and grant
everything again.")
**Optional extension:** ask students to identify one setting from this lesson that would have
made zero difference against a vulnerability that lets an attacker read environment variables
(a good check that they understand these are layered, not universal, protections — the answer
is that none of SecurityContext's settings here directly protect secrets stored in env vars;
that's a different concern, covered later in this course).
@@ -0,0 +1,87 @@
# Demonstration: A Pod Before and After Hardening
## Before: the default, unhardened Pod
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-unhardened
spec:
containers:
- name: app
image: myapp:latest
```
Checking what this actually runs as:
```
$ kubectl exec app-unhardened -- id
uid=0(root) gid=0(root) groups=0(root)
$ kubectl exec app-unhardened -- touch /etc/anything-i-want
$ kubectl exec app-unhardened -- ls -la /etc/anything-i-want
-rw-r--r-- 1 root root 0 Jan 1 00:00 /etc/anything-i-want
```
Running as root, with a fully writable filesystem, including system directories. This is not a
misconfiguration — it's simply what happens when nothing restricts it.
## After: the same application, hardened
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-hardened
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: app
image: myapp:latest
securityContext:
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
```
```
$ kubectl exec app-hardened -- id
uid=1000 gid=0 groups=0
$ kubectl exec app-hardened -- touch /etc/anything-i-want
touch: /etc/anything-i-want: Read-only file system
```
Same application image, same functionality (assuming the app doesn't itself require root or
filesystem writes it wasn't told about) — but a compromise of this container now can't write
to the filesystem, can't use the elevated capabilities that were dropped, and never had root to
begin with.
## What this actually buys you
Nothing here fixed a bug in `myapp:latest` — the image is identical in both cases. What changed
is what an attacker (or a bug behaving unexpectedly) could DO if something inside that container
went wrong. The unhardened version had almost no limits; the hardened version has several
independent ones, any of which alone would have blocked the `touch` command above.
## Instructor Notes
**Demo script:** Run both Pods live if time allows — watching `touch` succeed on one and fail
on the other in real time is far more convincing than reading it.
**Approximate timing:** 8-10 minutes live; 5-7 minutes self-paced.
**Common misconception to preempt:** students sometimes read the hardened Pod's failed `touch`
as "hardening broke something." Reinforce: the FUNCTIONALITY didn't change (the application
itself never needed to write to `/etc`) — only an unnecessary, unused capability was removed.
If hardening breaks something the application genuinely needs, that's real information about
the application, not a reason to skip hardening.
**Optional extension:** ask what would happen if `myapp:latest` were built assuming it could
write a cache file to `/tmp` inside the container — a good bridge to the fact that
`readOnlyRootFilesystem` often needs an explicit `emptyDir` volume mounted at the specific path
an application legitimately needs to write to (ROOT 08), rather than being an all-or-nothing
choice.
@@ -0,0 +1,47 @@
# Knowledge Check: Security Model & SecurityContext
---
**1. A container is hardened with `capabilities: drop: ["ALL"]` and then fails because it
genuinely needs to bind to a low-numbered network port, which requires `NET_BIND_SERVICE`.
What's the least-privilege fix?**
*Answer:* Add back only `NET_BIND_SERVICE` specifically (`capabilities: add:
["NET_BIND_SERVICE"]` alongside the drop-all), not remove the blanket drop entirely.
Least privilege means granting exactly what's needed, not reverting to granting everything.
---
**2. True or false: setting `runAsNonRoot: true` fixes a vulnerability inside the application
itself.**
*Answer:* False. It doesn't fix anything about the application's code — it limits what a
compromise of that code could DO, by ensuring the process never has root privileges to begin
with. This is the defense-in-depth distinction: reducing blast radius, not eliminating bugs.
---
**3. Why might `runAsNonRoot: true` cause a Pod to fail to start entirely, and is that a
problem with the setting?**
*Answer:* If the container image was built assuming it runs as root (e.g. it tries to bind a
privileged port, or write to a root-owned directory), the Pod will fail to start under
`runAsNonRoot`. This is NOT a problem with the setting — it's valuable, actionable information
that the image itself makes an assumption that should probably be fixed.
---
**4. A container has `readOnlyRootFilesystem: true` and needs to write temporary cache files.
What's the correct way to allow that without abandoning the read-only filesystem entirely?**
*Answer:* Mount an explicit `emptyDir` volume (ROOT 08) at the specific path the application
needs to write to. This keeps the rest of the filesystem read-only while allowing the one
legitimately-needed writable location.
## Instructor Notes
**Facilitation:** Question 1 and question 4 both reinforce the same "narrow the exception, don't
remove the protection" pattern — worth calling out that they're the same principle applied
twice.
**Approximate timing:** 5-7 minutes.
@@ -0,0 +1,51 @@
# Guided Exercise: Planning a Pod Hardening Checklist
Paper-and-thinking exercise before the Labs.
## The scenario
You're handed a Pod spec for a small internal API with NO `securityContext` set at all — the
fully-default, unhardened case from the demonstration. Your team lead asks you to harden it
before it ships.
## Your task
Without writing the actual YAML yet, answer these:
1. List the settings you'd add, in the order you'd verify them (hint: there are at least three
distinct concerns from the lesson — user/root, filesystem, capabilities).
2. For each setting, name ONE way you'd notice if you'd broken the application by adding it
(what would the failure actually look like?).
3. If the application genuinely needs to write a cache file to disk, does that mean you should
skip `readOnlyRootFilesystem` entirely? What's the more precise fix?
4. Suppose after hardening, the container still runs fine but a security scan flags that it
still has the `NET_RAW` capability. What's the most likely reason, and what's the fix?
## A worked answer to check yourself against
1. In a reasonable order: (a) `runAsNonRoot`/`runAsUser` — a fundamental identity change,
surfaces image assumptions fastest; (b) `capabilities: drop: ["ALL"]` — narrow, usually safe
for a typical web API; (c) `readOnlyRootFilesystem: true` — most likely to need an
accompanying volume, so verify last once the others are confirmed working.
2. (a) Pod fails to start entirely (`CrashLoopBackOff` or an immediate error) if the image
assumed root. (b) The application errors on a specific operation that needed a capability
you dropped (e.g. failing to bind a privileged port). (c) A write operation fails at runtime
(a log line, an exception) the first time the app tries to write to disk.
3. No — the fix is mounting an explicit `emptyDir` volume at the specific path the cache needs,
keeping the rest of the filesystem read-only rather than abandoning the protection entirely.
4. Most likely: `capabilities: drop: ["ALL"]` was never actually applied (a YAML indentation
mistake, or it was added to the Pod's securityContext instead of the container's — capability
drops are container-level, not Pod-level). The fix is verifying the drop is actually present
on the CONTAINER's securityContext specifically.
## Instructor Notes
**Facilitation:** Question 4 is deliberately tricky — Pod-level vs. container-level
`securityContext` fields is a real, common source of "I set this and it didn't work" confusion
worth surfacing here before students hit it in a Lab.
**Approximate timing:** 15 minutes live with discussion; 12-15 minutes self-paced.
**Optional extension:** ask students to identify which of the settings in this exercise would
be MOST valuable to apply first if they could only add one to a large, existing fleet of
unhardened Pods under time pressure, and why.
@@ -0,0 +1,42 @@
# Reflection: Least Privilege Is a Default, Not an Afterthought
You just hardened real Pods across several Labs — non-root, dropped capabilities, a read-only
filesystem, and a full SecurityContext review. Take a few minutes to connect this to real
practice.
## Reflection questions
1. Before this module, would you have described "runs as root with a fully writable
filesystem" as insecure, or just as "the default, unremarkable configuration"? Has that
changed?
2. None of the settings you configured fixed a bug in any application. What did they actually
change, precisely?
3. If your team had to choose between hardening every new Pod by default (as a policy) versus
hardening Pods only after an incident reveals a gap, which produces less total risk over
time, and why?
## Real Ops takeaway
**Unhardened is not neutral — it's the maximum-privilege default.** A container with no
SecurityContext configured isn't "unopinionated"; it's root, every capability, fully writable —
the MOST privileged configuration possible, not a safe middle ground. Teams that treat hardening
as optional are choosing maximum blast radius by default, usually without realizing they're
choosing anything at all.
**Defense in depth means no single mistake should be catastrophic.** The value of stacking
non-root + dropped capabilities + read-only filesystem isn't that any ONE of them is perfect —
it's that a real compromise would have to defeat several independent restrictions simultaneously
instead of exploiting one gap that grants everything. This is exactly why security-minded teams
apply all of these by default rather than picking "the one that matters most."
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 3 is worth pushing on directly — it's
the actual business case for security-by-default, not just a technical preference.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Optional extension:** ask students to draft a one-paragraph "default Pod hardening policy"
their hypothetical team would adopt, based on what they've learned in this module.
@@ -0,0 +1,90 @@
# Identity for Workloads: ServiceAccounts & RBAC
## Workloads need an identity too, separate from yours
When YOU run `kubectl`, you're authenticated as yourself. But a Pod running INSIDE the cluster
that needs to call the Kubernetes API (to list other Pods, read a ConfigMap, whatever) needs its
own identity — it can't and shouldn't use your personal credentials. That identity is a
**ServiceAccount**.
Every Pod runs as SOME ServiceAccount — if you don't specify one, it uses the `default`
ServiceAccount in its Namespace, which by default has essentially no permissions. A Pod's
ServiceAccount token is automatically mounted into the container unless you explicitly disable
that (`automountServiceAccountToken: false`) — worth doing for any Pod that never actually needs
to call the Kubernetes API at all, since an unused, mounted credential is still something an
attacker could steal and use.
## RBAC: what that identity is actually allowed to do
Having an identity (a ServiceAccount) doesn't grant any permissions by itself — **RBAC
(Role-Based Access Control)** is the separate system that defines what an identity can DO.
- A **Role** (namespace-scoped) or **ClusterRole** (cluster-wide) defines a set of permissions:
which API resources, which verbs (get/list/watch/create/update/delete).
- A **RoleBinding** (or **ClusterRoleBinding**) connects a Role/ClusterRole to a specific
identity (a ServiceAccount, a user, or a group) — the Role alone grants nothing to anyone
until it's bound.
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: ServiceAccount
name: my-app
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
```
This grants the `my-app` ServiceAccount permission to `get`/`list` Pods in the `default`
Namespace — and nothing else. It cannot delete Pods, cannot touch Secrets, cannot do anything
not explicitly listed.
## Role vs. ClusterRole: scope matters
A Role only grants permissions within the ONE Namespace it's defined in. A ClusterRole can grant
permissions across the entire cluster (or, bound via a RoleBinding instead of a
ClusterRoleBinding, can also be reused namespace-by-namespace) — reach for a ClusterRole only
when the workload genuinely needs cluster-wide visibility, not by default.
## Least privilege, applied to permissions specifically
The same principle from the previous module applies directly here: grant the narrowest set of
verbs on the narrowest set of resources that the workload actually needs. A Role granting
`get`/`list` on Pods is very different from one granting `*` on `*` — the latter is
"root-equivalent" API access, and should be treated with exactly that level of caution.
## Instructor Notes
**Approximate timing:** 15 minutes read; 10 minutes live.
**Likely misconceptions to watch for:**
- Students often conflate "ServiceAccount" with "user account for logging in." A ServiceAccount
is specifically an identity for WORKLOADS, not a human login mechanism.
- The Role/RoleBinding two-step (define permissions, then separately grant them to someone) is
sometimes seen as redundant. Emphasize the reuse benefit: the same Role can be bound to
multiple identities without redefining the permission set each time.
**Discussion question:** "Why might you explicitly disable automounting a ServiceAccount token
for a Pod that never calls the Kubernetes API?" (Answer: an unused but present credential is
still something an attacker who compromises the container could steal and use elsewhere —
removing it entirely removes that risk for a Pod that never needed it.)
**Optional extension:** ask what the practical difference is between granting a ClusterRole via
a RoleBinding (scoped to one Namespace) versus a ClusterRoleBinding (cluster-wide) — a good
check that students understand the Role/ClusterRole and Binding-type distinctions are
independent axes, not the same choice.
@@ -0,0 +1,44 @@
# Knowledge Check: ServiceAccounts & RBAC
---
**1. A Pod is created with no ServiceAccount specified. What identity does it actually run as,
and what can that identity do by default?**
*Answer:* The `default` ServiceAccount in its Namespace, which by default has essentially no
permissions — it can authenticate to the API but is granted nothing until an RBAC binding says
otherwise.
---
**2. You create a Role granting `get`/`list` on Pods, but a workload still can't read Pods.
What's the most likely missing piece?**
*Answer:* A RoleBinding (or ClusterRoleBinding) connecting that Role to the workload's actual
ServiceAccount. A Role by itself grants nothing to anyone — it has to be bound to an identity.
---
**3. True or false: a ClusterRole can only ever be applied cluster-wide.**
*Answer:* False. A ClusterRole can be bound via a RoleBinding to apply within just ONE
Namespace (reusing a cluster-wide-DEFINED permission set in a namespace-SCOPED way), or via a
ClusterRoleBinding to apply cluster-wide. The Role-type and Binding-type are independent
choices.
---
**4. Why would a security review flag a Role granting `verbs: ["*"]` on `resources: ["*"]`
even if the workload "seems to work fine"?**
*Answer:* That grant is root-equivalent API access — it violates least privilege regardless of
whether the workload happens to only use a small subset of it today. "Works fine" doesn't mean
"correctly scoped" — an over-broad grant is a real risk the moment that workload (or its
credentials) is compromised, whether or not it's ever exploited in practice.
## Instructor Notes
**Facilitation:** Question 4 is worth discussing even if everyone answers correctly — the "it
works, so what's the problem" instinct is exactly what least-privilege review exists to counter.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,76 @@
# NetworkPolicy as a Security Control & Supply Chain Trust
## NetworkPolicy, revisited as a security posture
ROOT 07 introduced NetworkPolicy architecturally: Kubernetes doesn't isolate Pod-to-Pod traffic
by default, and NetworkPolicy is how you declare intended reachability. Here, the same
mechanism is the point of a deliberate SECURITY practice: **default deny**.
A default-deny policy blocks all traffic to selected Pods, and you then explicitly allow only
what's actually needed:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {} # selects EVERY Pod in the namespace
policyTypes: ["Ingress"]
```
An empty `podSelector: {}` with no ingress rules means "select every Pod, allow nothing in" —
the network equivalent of the SecurityContext principle from earlier in this course: start
from nothing, add back only what's proven necessary. This is exactly the opposite of
Kubernetes' un-configured default (everything open), which is precisely why it has to be
declared explicitly rather than relying on any built-in setting.
**Egress restriction** works the same way in reverse — restricting what a Pod can call OUT to,
not just what can call IN. A compromised Pod that can freely make outbound connections to
anything is far more dangerous (data exfiltration, calling out to an attacker's server) than one
restricted to only the specific destinations it legitimately needs.
## Supply chain: trusting what you run before it ever starts
Everything covered so far in this course assumes the container image itself is trustworthy.
Supply-chain security is about that assumption specifically:
- **Image-pull secrets** — when pulling from a private registry, the cluster needs credentials
to authenticate. Storing and using these correctly (rather than, say, a publicly-readable
registry with no access control) is a basic supply-chain control: only images from a trusted,
access-controlled source get pulled at all.
- **Minimizing image footprint** (referenced here, not a hands-on Lab in this module) — a
smaller image means fewer packages, fewer known CVEs, and a smaller attack surface if the
image is ever compromised.
## TLS at the Ingress layer: protecting data in transit
Recall ROOT 07's Ingress lesson — routing external HTTP(S) traffic by hostname/path. Configuring
TLS termination at that same Ingress layer means traffic between users and your cluster is
encrypted, protecting against interception on the network path. This is a security concern
layered on top of the same architectural mechanism, the same relationship NetworkPolicy has to
its ROOT 07 introduction.
## Instructor Notes
**Approximate timing:** 15 minutes read; 10 minutes live.
**Likely misconceptions to watch for:**
- Students sometimes think "default deny" means blocking ALL traffic including what the
application needs to function. Clarify: default deny is the STARTING point; you then add
specific allow rules for exactly what's needed — the goal state is fully functional AND fully
intentional, not non-functional.
- Egress restriction is sometimes seen as less important than ingress. Emphasize the
exfiltration angle explicitly — a compromised Pod's outbound reach is just as much a real
risk as inbound exposure.
**Discussion question:** "Why is 'default deny, then explicitly allow' a better starting
posture than 'default allow, then explicitly block known-bad traffic'?" (Answer: default-deny
means a NEW, unanticipated attempt to reach or leave a Pod is blocked by default; default-allow
means anything not YET identified as bad gets through — you're always one step behind an
attacker under the second model.)
**Optional extension:** ask what a real team would want to verify before trusting a
publicly-available container image from an unfamiliar source, beyond just "does it work" — a
good bridge into image scanning/provenance concepts (not built as a hands-on Lab in this course,
but worth naming as a real practice).
@@ -0,0 +1,45 @@
# Knowledge Check: Network & Supply Chain Security
---
**1. A NetworkPolicy has `podSelector: {}` with `policyTypes: ["Ingress"]` and no ingress rules
defined. What's the practical effect on every Pod in that namespace?**
*Answer:* Every Pod is selected, and since no ingress rule allows anything, all incoming traffic
to every Pod is denied — this is the "default deny" starting posture, applied namespace-wide.
---
**2. Why is restricting EGRESS (outbound) traffic from a Pod a real security control, not just
a mirror-image nicety of ingress restriction?**
*Answer:* A compromised Pod with unrestricted egress can exfiltrate data to an attacker-controlled
destination or call out to download additional malicious payloads. Restricting egress to only
what's legitimately needed limits what a compromise can accomplish, independent of whether
inbound access was ever an issue.
---
**3. True or false: "default deny, then explicitly allow" and "default allow, then explicitly
block known-bad traffic" provide equivalent security given the same set of rules.**
*Answer:* False. Default-deny blocks anything NOT yet explicitly allowed, including new/
unanticipated attempts. Default-allow only blocks what's already been identified as bad — an
attacker doing something not yet on the block list gets through under the second model but not
the first.
---
**4. Why does minimizing a container image's footprint (fewer packages) improve supply-chain
security specifically, rather than just making the image smaller/faster to pull?**
*Answer:* Fewer installed packages means fewer potential known vulnerabilities (CVEs) present in
the image at all, and a smaller attack surface if the image is ever compromised — it's a
security property, not merely a performance optimization.
## Instructor Notes
**Facilitation:** Question 3 is the core conceptual point of this module — worth confirming it's
genuinely understood, not just correctly guessed.
**Approximate timing:** 5-7 minutes.
@@ -0,0 +1,64 @@
# Pod Security Admission & a Survey of Runtime Hardening
This module is deliberately a survey, not a deep dive — the topics here are real and important,
but lean toward cluster-operator/platform-team concerns rather than the daily-driver skills most
ROOT_ learners will use constantly. Treat the labs in this module as good-to-know, not required
mastery, before moving on.
## Pod Security Admission: enforcing a baseline across a Namespace
Everything you configured by hand in earlier modules (non-root, dropped capabilities, read-only
filesystem) can also be ENFORCED automatically at the Namespace level via **Pod Security
Admission (PSA)** — a label on the Namespace itself that rejects any Pod violating a chosen
security standard (`privileged`, `baseline`, or `restricted`, from least to most strict):
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
```
This replaces the older PodSecurityPolicy (PSP) mechanism, which was removed from Kubernetes —
if you encounter PSP in older documentation or an older cluster, know that PSA is its current
replacement.
## Immutable Secrets: preventing a class of accidental change
A Secret can be marked `immutable: true`, which prevents its data from being changed after
creation (a new Secret with a new name has to be created instead, and workloads updated to
reference it). This trades a small amount of operational flexibility for a real safety
property: an accidental or malicious edit to a live Secret can't silently happen.
## A survey of further topics (labs available, treat as optional depth)
- **Audit policy** — configuring what the Kubernetes API server logs about requests made against
it (who did what, when) — genuinely important for incident investigation, but a cluster-admin
concern, not something most workload teams configure themselves.
- **RuntimeClass** — lets specific Pods run under a different container runtime configuration
(e.g. an extra-sandboxed runtime like gVisor or Kata Containers) for workloads that need
stronger isolation than the default runtime provides — relevant mainly for genuinely
untrusted or high-risk workloads, not typical applications.
## Instructor Notes
**Approximate timing:** 15 minutes read (shorter than other lessons — deliberately survey-depth).
**Framing for instructors:** Be explicit with students that this module is intentionally
lighter than the earlier ones in this course — the goal is awareness ("this exists, here's
roughly what it's for") rather than hands-on fluency, consistent with not every CKS-style topic
belonging in the core learning path.
**Discussion question:** "Why would a platform team prefer enforcing security standards via
Pod Security Admission at the Namespace level, instead of trusting every team to configure
SecurityContext correctly on every Pod by hand?" (Answer: PSA provides a hard backstop —
correctly-configured SecurityContext relies on every individual Pod author doing it right every
time; PSA rejects violations automatically regardless of whether any individual author
remembered to.)
**Optional extension:** ask which of `privileged`/`baseline`/`restricted` a team should start a
brand-new, security-conscious project with, and why not jump straight to the strictest option
on a cluster already running older, unaudited workloads (a good practical judgment question
about migration risk, not just definitions).