feat(curriculum): Phase 6C module upgrades, security assessment bank, ROOT 11 orientation
Upgrades six PARTIAL modules to the gold-module standard (lesson/demonstration/ knowledge-check/guided-exercise/lab(s)/reflection), preserving every existing lab activity_id and scenario_id: root-06-m03 (jobs/cronjobs/daemonsets), root-07-m04 (ingress/networkpolicy), root-07-m02 (configuration/secrets, converted in place from a bare 5-lab bank), root-09-m02 (serviceaccounts/rbac), root-09-m03 (network/supply-chain security), root-10-m03 (troubleshooting framework). root-05-m01 (20-task kubectl-fluency bank) gets a minimal bookending lesson + reflection rather than a full redesign, since each task already carries its own explanation. root-12-m01, root-capstone-m01, and root-09-m04 are promoted to READY via documented exceptions (no new activities) where adding one would misrepresent their intentional design. Wires a new rootnode-security-assessment (exam, 60min, hint_policy disabled) using the existing Phase 3B assessment engine: 3 required hands-on CORE scenarios + 4 random-draw pools covering SecurityContext, RBAC, network policy, and the 7 previously-unwired CKS MCQs. Wires the previously-unwired readiness-probe-task scenario as an OPTIONAL practice lab in root-10-m01, using the existing is_required model. Adds root-11-m01-devops-platform-thinking — an honest, lesson-only ROOT 11 orientation module naming DevOps/platform engineering practices already exercised in ROOT 05-10/12, explicit that GitOps/CI-CD/IaC/observability need lab infrastructure this platform doesn't yet provide. No fake labs. Adds infra/scripts/validate-course-content.js: cross-checks the DB against scenario/content files for duplicate ids, orphans, invalid references, cycles, sequence collisions, instructor-notes leakage, and (new) self-paced content deferring explanation to a live instructor. 0 errors against the resulting 22 courses/modules, 163 activities, 98 scenario files. No application code changed — every gap was satisfiable with existing tables (course_modules, module_activities, assessment_definitions) and the existing content-delivery pipeline. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
+62
@@ -0,0 +1,62 @@
|
||||
# Demonstration: Watching RBAC Actually Deny Something
|
||||
|
||||
## The setup
|
||||
|
||||
A ServiceAccount `readonly-bot` has a Role granting only `get`/`list` on `pods` — nothing else.
|
||||
|
||||
## What it CAN do
|
||||
|
||||
```
|
||||
$ kubectl auth can-i list pods --as=system:serviceaccount:default:readonly-bot
|
||||
yes
|
||||
|
||||
$ kubectl get pods --as=system:serviceaccount:default:readonly-bot
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
web-abc123 1/1 Running 0 2h
|
||||
```
|
||||
|
||||
## What it CANNOT do
|
||||
|
||||
```
|
||||
$ kubectl auth can-i delete pods --as=system:serviceaccount:default:readonly-bot
|
||||
no
|
||||
|
||||
$ kubectl delete pod web-abc123 --as=system:serviceaccount:default:readonly-bot
|
||||
Error from server (Forbidden): pods "web-abc123" is forbidden:
|
||||
User "system:serviceaccount:default:readonly-bot" cannot delete resource "pods"
|
||||
in API group "" in the namespace "default"
|
||||
```
|
||||
|
||||
This is what least privilege actually looks like in practice — not a policy document, a real,
|
||||
enforced denial with a specific, readable error naming exactly which permission was missing.
|
||||
`kubectl auth can-i` is your fastest way to CHECK a permission before attempting an action that
|
||||
might fail — useful both for debugging "why can't my workload do X" and for verifying a Role is
|
||||
actually as narrow as intended.
|
||||
|
||||
## What happens when the ServiceAccount tries something it was never even considered for
|
||||
|
||||
```
|
||||
$ kubectl auth can-i get secrets --as=system:serviceaccount:default:readonly-bot
|
||||
no
|
||||
```
|
||||
|
||||
Notice this returns `no` the same way `delete pods` did — RBAC has no concept of "not yet
|
||||
decided," only "explicitly allowed" or "everything else, denied." This is the practical meaning
|
||||
of default-deny applied to permissions: nothing is grey area.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo script:** Run all three `kubectl auth can-i`/action pairs live — the FORBIDDEN error
|
||||
message's exact wording (naming the verb, resource, and namespace) is worth reading aloud, since
|
||||
that's exactly the evidence a student would use to debug a real permissions issue later.
|
||||
|
||||
**Approximate timing:** 8 minutes live; 5 minutes self-paced.
|
||||
|
||||
**Common misconception to preempt:** students sometimes think `kubectl auth can-i` REQUIRES the
|
||||
action to actually be attempted to know the answer. It doesn't — it's a dry-run permission
|
||||
check against the same RBAC rules, safe to run repeatedly without side effects.
|
||||
|
||||
**Optional extension:** ask what the error message would look like if `readonly-bot` tried to
|
||||
act on a DIFFERENT namespace than the one its Role/RoleBinding applies to — a good check that
|
||||
students understand Role scope is namespace-specific unless a ClusterRole+ClusterRoleBinding is
|
||||
used instead.
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# Guided Exercise: Writing a Least-Privilege Role
|
||||
|
||||
Paper-and-thinking exercise before the Labs.
|
||||
|
||||
## The scenario
|
||||
|
||||
A monitoring agent needs to run inside your cluster. Its ONLY job is to periodically list Pods
|
||||
and read their status, across the whole cluster, to report health metrics elsewhere. It never
|
||||
needs to create, modify, or delete anything, and it never needs to touch Secrets, ConfigMaps, or
|
||||
any object type other than Pods.
|
||||
|
||||
## Your task
|
||||
|
||||
1. Should this use a Role or a ClusterRole? Justify your answer based on scope.
|
||||
2. Which verbs does it actually need? Which common verbs (create, update, delete, patch) should
|
||||
be deliberately left OUT?
|
||||
3. Does it need access to Secrets? Why is it tempting to grant broader access "just in case,"
|
||||
and why should you resist that?
|
||||
4. What would you check, using `kubectl auth can-i`, to CONFIRM your Role is exactly as narrow
|
||||
as intended before deploying it?
|
||||
|
||||
## A worked answer to check yourself against
|
||||
|
||||
1. **ClusterRole** (bound via a ClusterRoleBinding) — it needs to see Pods across the WHOLE
|
||||
cluster, not just one namespace, so a namespace-scoped Role can't satisfy the requirement.
|
||||
2. **`get`, `list`, `watch`** on `pods` — read-only verbs only. Deliberately exclude `create`,
|
||||
`update`, `patch`, `delete` — this workload never legitimately needs to change anything.
|
||||
3. No — it never touches Secrets, so it should get zero permission on them. The temptation to
|
||||
"just in case" grant broader access is exactly what least privilege exists to resist: every
|
||||
unused permission is pure risk with no corresponding benefit if this workload is ever
|
||||
compromised.
|
||||
4. Run `kubectl auth can-i delete pods --as=<the ServiceAccount>` and confirm it returns `no` —
|
||||
and similarly check a Secrets verb returns `no` — proving the Role is bounded, not just
|
||||
assuming it from reading the YAML.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 3 is the core judgment call this exercise is testing — push on WHY
|
||||
"just in case" access is a real anti-pattern, not just point out that it is one.
|
||||
|
||||
**Approximate timing:** 10-12 minutes live with discussion; 10 minutes self-paced.
|
||||
|
||||
**Optional extension:** ask what would change about the answer if this monitoring agent later
|
||||
needed to also restart crash-looping Pods automatically — a good check that students can reason
|
||||
about incrementally expanding a Role's scope only as far as a genuinely new requirement demands.
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# Reflection: Permissions Should Reflect What a Workload Actually Needs
|
||||
|
||||
You just configured real ServiceAccounts and RBAC rules. Take a few minutes to connect this to
|
||||
production security practice.
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Before this module, would you have thought twice about a workload's ServiceAccount having
|
||||
broad, unused permissions "just in case"? Has that changed?
|
||||
2. If a workload with cluster-admin-equivalent RBAC access is ever compromised (a vulnerable
|
||||
dependency, a supply-chain issue), what's the realistic blast radius compared to one scoped
|
||||
to exactly what it needs?
|
||||
3. How would you discover, on a real cluster you didn't build, whether any ServiceAccount has
|
||||
more access than it actually uses?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**RBAC over-permissioning is a silent, cumulative risk.** Unlike a misconfigured probe or a
|
||||
crashing Pod, an over-broad Role doesn't cause a visible symptom on a healthy day — it just sits
|
||||
there as unused risk until the one day a workload using it is compromised. This makes RBAC
|
||||
review a discipline that has to be practiced deliberately, not one that gets forced on you by
|
||||
an obvious failure.
|
||||
|
||||
**Least privilege is a default you set once, not a fix you apply after an incident.** Every
|
||||
example in this module started from "grant nothing, add exactly what's needed" rather than
|
||||
"grant broadly, restrict later" — the second approach almost never actually happens once
|
||||
something works, because there's no pressure forcing the cleanup.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 3 is a good one to leave partially open
|
||||
— acknowledge that real answers involve tooling beyond plain `kubectl` (RBAC audit tools) that
|
||||
this course doesn't build hands-on, while still reinforcing that the QUESTION itself is the
|
||||
right one to ask.
|
||||
|
||||
**Approximate timing:** 8 minutes live discussion; 5 minutes self-paced writing.
|
||||
|
||||
**Optional extension:** ask students to identify one ServiceAccount permission from earlier in
|
||||
ROOT_ (any Lab so far) that, in a real production system, they'd want to double-check was
|
||||
actually necessary.
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# Demonstration: Default-Deny, Before and After
|
||||
|
||||
## Before: everything can reach everything
|
||||
|
||||
```
|
||||
$ kubectl run test-pod --image=busybox --rm -it -- wget -qO- --timeout=2 http://backend-svc
|
||||
<real response from backend>
|
||||
```
|
||||
|
||||
Any Pod in the namespace can reach `backend-svc`, regardless of what it is or who deployed it —
|
||||
this is the un-configured default the ROOT 07 lesson described, now made concrete from a
|
||||
security angle.
|
||||
|
||||
## Applying a default-deny policy
|
||||
|
||||
```
|
||||
$ kubectl apply -f default-deny.yaml
|
||||
networkpolicy.networking.k8s.io/default-deny created
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: default-deny
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes: ["Ingress"]
|
||||
```
|
||||
|
||||
## After: the same request, now blocked
|
||||
|
||||
```
|
||||
$ kubectl run test-pod --image=busybox --rm -it -- wget -qO- --timeout=2 http://backend-svc
|
||||
wget: download timed out
|
||||
```
|
||||
|
||||
No error naming a permission (that's RBAC's job, a different layer) — just a timeout, because
|
||||
the packets are being dropped at the network layer before they ever reach the Pod. This is a
|
||||
DIFFERENT failure signature than an RBAC denial, worth being able to tell apart: RBAC denials
|
||||
are explicit and immediate; NetworkPolicy denials look like the destination is simply
|
||||
unreachable.
|
||||
|
||||
## Explicitly allowing only what's needed
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-frontend-to-backend
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels: { app: backend }
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels: { app: frontend }
|
||||
```
|
||||
|
||||
```
|
||||
$ kubectl run frontend-test --labels="app=frontend" --image=busybox --rm -it -- wget -qO- --timeout=2 http://backend-svc
|
||||
<real response from backend>
|
||||
|
||||
$ kubectl run other-test --labels="app=other" --image=busybox --rm -it -- wget -qO- --timeout=2 http://backend-svc
|
||||
wget: download timed out
|
||||
```
|
||||
|
||||
Same destination, same request — the ONLY difference is the caller's label. This is
|
||||
least-privilege network access made concrete: reachability now reflects an explicit decision,
|
||||
not an accident of both Pods existing in the same cluster.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo script:** Run all three states live if a real cluster is available — the timeout (not an
|
||||
error) for the denied case is worth calling out explicitly, since it looks identical to a
|
||||
genuinely broken/unreachable service from the caller's perspective.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live; 5-7 minutes self-paced.
|
||||
|
||||
**Common misconception to preempt:** students sometimes expect a NetworkPolicy denial to
|
||||
produce a clear "forbidden" error like RBAC does. It doesn't — reinforce that a NetworkPolicy-
|
||||
caused timeout is genuinely hard to distinguish from an actual network/DNS problem without
|
||||
already suspecting NetworkPolicy, which is exactly why checking existing policies is a real
|
||||
diagnostic step worth knowing.
|
||||
|
||||
**Optional extension:** ask how a student would distinguish "blocked by NetworkPolicy" from
|
||||
"the backend Pod is actually down" purely from the caller's side (answer: check the backend
|
||||
Pod's own status/logs directly — if it's healthy and Ready, but still unreachable from one
|
||||
caller and reachable from another with a different label, NetworkPolicy is the likely cause).
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Guided Exercise: Planning Egress Restriction
|
||||
|
||||
Paper-and-thinking exercise before the Labs.
|
||||
|
||||
## The scenario
|
||||
|
||||
A `payment-processor` Pod handles sensitive transactions. It legitimately needs to reach:
|
||||
- an internal `payments-db` Service, and
|
||||
- one external payment gateway API.
|
||||
|
||||
It should NOT be able to reach anything else — including the public internet in general, other
|
||||
internal services, or any other namespace.
|
||||
|
||||
## Your task
|
||||
|
||||
1. Why is restricting this Pod's OUTBOUND (egress) traffic just as important as restricting who
|
||||
can reach IT (ingress), given what this Pod handles?
|
||||
2. If this Pod were compromised via a vulnerable dependency, what would UNRESTRICTED egress let
|
||||
an attacker do that restricted egress would prevent?
|
||||
3. Sketch (in words) what an egress policy for this Pod needs to allow, and — just as
|
||||
important — what it must NOT allow by omission.
|
||||
4. Why might a security review flag a Pod with `policyTypes: ["Egress"]` but an EMPTY `egress`
|
||||
list as suspicious, rather than assume it's simply "not configured yet"?
|
||||
|
||||
## A worked answer to check yourself against
|
||||
|
||||
1. A Pod handling sensitive data that's compromised is far more dangerous if it can freely
|
||||
exfiltrate that data anywhere (unrestricted egress) than if its only reachable destinations
|
||||
are pre-approved and narrow.
|
||||
2. Unrestricted egress would let an attacker exfiltrate transaction data to an external server,
|
||||
or download additional malicious tooling. Restricted egress limits it to only the
|
||||
pre-approved destinations, even after compromise.
|
||||
3. Allow: egress to `payments-db` (internal, by label/namespace selector) and the specific
|
||||
external payment gateway (by IP range or, if supported, a domain-based egress rule). Must NOT
|
||||
allow: general internet access, other internal Services, or any other namespace by default.
|
||||
4. An empty `egress` list under `policyTypes: ["Egress"]` means DENY ALL outbound — exactly like
|
||||
the ingress case from the lesson. A reviewer seeing this should confirm it's intentional
|
||||
(a Pod that genuinely needs zero outbound network access) rather than assume it's an
|
||||
oversight, since the two look identical in the YAML.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 4 reinforces the same "empty rules list means deny-all, not
|
||||
unconfigured" point from earlier lessons — a good moment to confirm it generalizes correctly to
|
||||
egress, not just ingress.
|
||||
|
||||
**Approximate timing:** 10-12 minutes live with discussion; 10 minutes self-paced.
|
||||
|
||||
**Optional extension:** ask what additional control (beyond NetworkPolicy) a team might want for
|
||||
truly sensitive egress, like restricting DNS resolution itself for this Pod — a forward pointer
|
||||
to defense-in-depth without requiring a hands-on Lab here.
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Reflection: Reachability and Trust Are Decisions You Make on Purpose
|
||||
|
||||
You just applied default-deny NetworkPolicy, egress restriction, TLS ingress, and image-pull
|
||||
authentication for real. Take a few minutes to connect these to a single underlying principle.
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Four different mechanisms in this module (NetworkPolicy ingress, NetworkPolicy egress, TLS,
|
||||
image-pull secrets) all express the same underlying idea in a different layer. What is that
|
||||
idea, stated in one sentence?
|
||||
2. If you had to prioritize implementing ONLY ONE of this module's controls on a brand-new,
|
||||
unsecured cluster, which would you pick first, and why?
|
||||
3. How would you explain, to someone who thinks "our cluster is internal, so it doesn't need
|
||||
this," why internal-only still benefits from all four controls?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**Trust should be explicit, narrow, and verifiable — never assumed from proximity.** A Pod on
|
||||
the same cluster, a request from inside the corporate network, an image from "a registry we
|
||||
usually use" — none of these are actually trustworthy by default. Every control in this module
|
||||
replaces an assumption ("it's internal, it's probably fine") with an explicit, checkable
|
||||
decision (this specific Pod can reach this specific destination; this specific registry is
|
||||
authenticated; this specific connection is encrypted).
|
||||
|
||||
**Supply-chain and network security compound.** A compromised image with unrestricted egress on
|
||||
an open network is a far worse incident than the same compromised image on a hardened one — the
|
||||
controls in this module don't individually prevent every incident, but together they
|
||||
dramatically shrink what any single failure can turn into.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 1 is the module's thesis — make sure
|
||||
students can actually articulate it in their own words, not just recognize it when stated.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
|
||||
|
||||
**Optional extension:** ask students to rank this module's four controls by how much
|
||||
IMPLEMENTATION EFFORT each typically requires on a real team, separately from security value —
|
||||
a good exercise in real-world prioritization tradeoffs.
|
||||
Reference in New Issue
Block a user