From 7f6e1857c0477b0ff709b55f306a72fc0e63d817 Mon Sep 17 00:00:00 2001 From: ced4568 Date: Wed, 26 Aug 2026 11:20:17 -0500 Subject: [PATCH] feat(curriculum): Phase 6C module upgrades, security assessment bank, ROOT 11 orientation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../00-lesson-kubectl-fluency.md | 49 ++++ .../21-reflection.md | 40 ++++ .../03-demonstration-job-lifecycle.md | 72 ++++++ .../04-guided-exercise-choosing-primitive.md | 50 ++++ .../05-reflection.md | 40 ++++ .../00-lesson-configuration.md | 84 +++++++ .../01-demonstration-secret-not-encrypted.md | 66 +++++ .../02-knowledge-check.md | 48 ++++ .../03-guided-exercise-choosing-mechanism.md | 50 ++++ .../06-reflection.md | 39 +++ .../03-demonstration-ingress-routing.md | 72 ++++++ ...-guided-exercise-network-isolation-plan.md | 47 ++++ .../05-reflection.md | 40 ++++ .../03-demonstration-rbac-denial.md | 62 +++++ ...04-guided-exercise-least-privilege-role.md | 45 ++++ .../07-reflection.md | 42 ++++ .../03-demonstration-default-deny.md | 89 +++++++ .../04-guided-exercise-egress-plan.md | 51 ++++ .../09-reflection.md | 41 ++++ .../04-guided-exercise-endpoint-mismatch.md | 52 ++++ .../06-reflection.md | 45 ++++ .../01-lesson-devops-platform-thinking.md | 88 +++++++ .../02-knowledge-check.md | 46 ++++ .../03-reflection.md | 45 ++++ infra/scripts/validate-course-content.js | 188 +++++++++++++++ .../024_phase6c_curriculum_completion.sql | 225 ++++++++++++++++++ 26 files changed, 1716 insertions(+) create mode 100644 content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/00-lesson-kubectl-fluency.md create mode 100644 content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/21-reflection.md create mode 100644 content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/03-demonstration-job-lifecycle.md create mode 100644 content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/04-guided-exercise-choosing-primitive.md create mode 100644 content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/05-reflection.md create mode 100644 content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/00-lesson-configuration.md create mode 100644 content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/01-demonstration-secret-not-encrypted.md create mode 100644 content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/02-knowledge-check.md create mode 100644 content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/03-guided-exercise-choosing-mechanism.md create mode 100644 content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/06-reflection.md create mode 100644 content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/03-demonstration-ingress-routing.md create mode 100644 content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/04-guided-exercise-network-isolation-plan.md create mode 100644 content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/05-reflection.md create mode 100644 content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/03-demonstration-rbac-denial.md create mode 100644 content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/04-guided-exercise-least-privilege-role.md create mode 100644 content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/07-reflection.md create mode 100644 content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/03-demonstration-default-deny.md create mode 100644 content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/04-guided-exercise-egress-plan.md create mode 100644 content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/09-reflection.md create mode 100644 content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/04-guided-exercise-endpoint-mismatch.md create mode 100644 content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/06-reflection.md create mode 100644 content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/01-lesson-devops-platform-thinking.md create mode 100644 content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/02-knowledge-check.md create mode 100644 content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/03-reflection.md create mode 100644 infra/scripts/validate-course-content.js create mode 100644 platform/api/migrations/024_phase6c_curriculum_completion.sql diff --git a/content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/00-lesson-kubectl-fluency.md b/content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/00-lesson-kubectl-fluency.md new file mode 100644 index 0000000..10c298e --- /dev/null +++ b/content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/00-lesson-kubectl-fluency.md @@ -0,0 +1,49 @@ +# Building kubectl Fluency: Why Twenty Small Reps Beat One Big Lecture + +## What this module is, and why it looks different from the last one + +ROOT 05's mental model module (m00) taught you the CONCEPT of desired state and gave you your +first `kubectl` commands. This module does something deliberately different: it's a breadth +drill across the everyday `kubectl` vocabulary you'll type dozens of times a day as a working +Kubernetes engineer — labels, JSONPath, dry-run manifests, patch, field selectors, sort-by, +diff, init containers, static Pods, and more. + +There's no single narrative thread connecting these twenty tasks, and that's intentional. Real +`kubectl` fluency isn't one big insight — it's having enough of these small, specific commands +already in muscle memory that you're not stopping to look up syntax while you're mid-incident. +Each task below explains the specific concept it covers in its own description; your job is to +work through them, not to memorize a lecture first. + +## How to approach this module + +- **Don't try to do them in one sitting if you don't need to.** These are independent — skip + around, come back to ones you found awkward, and don't feel obligated to treat this as one + continuous session. +- **Read each task's own explanation before reaching for the hint.** Every one of these + twenty tasks includes real context in its description — that context IS the teaching content + for that specific command. The hint is there for when you're stuck, not as the first thing to + read. +- **Notice which commands you reach for confidently versus which ones you have to think about.** + That gap is exactly what the reflection at the end of this module asks you to name. +- **This breadth matters more than it looks like it does.** `kubectl explain`, `--dry-run`, + JSONPath output, and `kubectl patch` in particular are commands experienced engineers use + constantly and newer engineers often don't know exist at all — this module exists specifically + to close that gap early. + +## Instructor Notes + +**Lesson goal:** Frame this module correctly before students hit it — it's a breadth-practice +bank, not a single-concept lesson, and the individual task descriptions carry the teaching +content. Without this framing, students sometimes expect a lecture and are confused when they +land directly on twenty short tasks. + +**Approximate timing:** 5 minutes to introduce; the module itself runs long (20 tasks) and is +meant to be worked through at each student's own pace, live or between sessions. + +**Facilitation for a live cohort:** Consider live-demoing 2-3 of the less obvious ones +(`kubectl explain`, JSONPath output, `kubectl patch`) rather than all twenty — the goal is to +unblock students on the commands they're least likely to have seen before, not to walk through +every task. + +**Expected takeaway:** Kubernetes fluency is built from a large vocabulary of small, reliable +commands, not just conceptual understanding — this module is where that vocabulary gets built. diff --git a/content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/21-reflection.md b/content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/21-reflection.md new file mode 100644 index 0000000..b604cb7 --- /dev/null +++ b/content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/21-reflection.md @@ -0,0 +1,40 @@ +# Reflection: Naming Your Gaps While They're Still Cheap to Close + +You just worked through twenty independent `kubectl` tasks. Before moving on, take a few +minutes to consolidate what actually stuck versus what you'd need to look up again. + +## Reflection questions + +1. Which THREE of these twenty commands do you feel genuinely confident using again right now, + without a hint? Name them specifically. +2. Which ONE command surprised you the most — either because you didn't know it existed, or + because it did something different than you expected? +3. `kubectl explain`, `kubectl patch`, and JSONPath output are commands a lot of engineers never + learn early and end up rediscovering the hard way, mid-incident, years later. Now that you've + used all three, where do you think you'd actually reach for one of them in a real debugging + situation? + +## Real Ops takeaway + +**Fluency gaps are cheap to close now and expensive to close later.** Right now, not knowing +`kubectl patch` costs you a few minutes rereading this module. In a live incident six months +from now, not knowing it costs you minutes you don't have while a Service is down. Naming your +gaps here, while the stakes are zero, is how you make sure you close them before they matter. + +**Breadth practice is not busywork.** It's easy to dismiss twenty small tasks as less valuable +than one big project — but the twenty small tasks are what make the big project fast to execute +later. This is the same discipline you'll rely on for the rest of ROOT_: build the vocabulary +now, apply it under pressure later. + +This module is complete. Continue to the next module. + +## Instructor Notes + +**Discussion questions for a live classroom:** question 3 works well as a round-robin — go +person to person and have each name a different command, so the group collectively surfaces +most of the twenty rather than converging on the same one or two obvious answers. + +**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing. + +**Expected takeaway:** Students should leave this module able to name their own remaining gaps +specifically, not just feel generally "done" with kubectl basics. diff --git a/content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/03-demonstration-job-lifecycle.md b/content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/03-demonstration-job-lifecycle.md new file mode 100644 index 0000000..e70b435 --- /dev/null +++ b/content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/03-demonstration-job-lifecycle.md @@ -0,0 +1,72 @@ +# Demonstration: Watching a Job Actually Complete + +## Creating the Job + +``` +$ kubectl apply -f pi-job.yaml +job.batch/pi-job created + +$ kubectl get jobs +NAME COMPLETIONS DURATION AGE +pi-job 0/1 3s 3s + +$ kubectl get pods +NAME READY STATUS RESTARTS AGE +pi-job-8x2kq 1/1 Running 0 3s +``` + +The Job's own status (`0/1` completions) is tracked separately from its Pod's status — this +matters because a Job can retry a FAILED Pod attempt without that looking like the healthy +restart behavior you'd see on a Deployment. + +## Once the Pod's process actually exits successfully + +``` +$ kubectl get pods +NAME READY STATUS RESTARTS AGE +pi-job-8x2kq 0/1 Completed 0 8s + +$ kubectl get jobs +NAME COMPLETIONS DURATION AGE +pi-job 1/1 7s 8s +``` + +`STATUS: Completed` — not `Running`, not `CrashLoopBackOff`. This is the ONE status a +long-running Deployment's Pod is never supposed to reach on its own; for a Job, it's success. +The Job object itself now reports `1/1` and stops — nothing recreates this Pod, because +"finished successfully" is the desired end state here, unlike a Deployment where exiting is +always treated as something to fix. + +## What a FAILING Job attempt looks like, briefly + +``` +$ kubectl get pods +NAME READY STATUS RESTARTS AGE +bad-job-abc12 0/1 Error 0 5s +bad-job-def34 0/1 Error 0 2s + +$ kubectl describe job bad-job +... +Events: + Warning BackoffLimitExceeded Job has reached the specified backoff limit +``` + +Two DIFFERENT Pods (`abc12`, `def34`), not one Pod restarting twice — each failed attempt gets +its own new Pod, up to `backoffLimit`. Once that limit is hit, the Job stops retrying and +reports the failure via Events rather than trying forever. + +## Instructor Notes + +**Demo script:** Run the successful case live; showing the FAILING case is optional if time is +short, but the `Completed` vs `Running`/`Error` distinction is worth pausing on regardless. + +**Approximate timing:** 8 minutes live; 5 minutes self-paced. + +**Common misconception to preempt:** students who've only seen Deployments sometimes expect +`kubectl get pods` to show the Job's Pod disappear once it's done. It doesn't, by default — it +stays as `Completed` until manually cleaned up or a TTL is configured, which is worth mentioning +briefly (not a hands-on topic here). + +**Optional extension:** ask what `kubectl get pods` would show for a CronJob's Pods across +several scheduled runs (answer: one Completed Pod per past run, accumulating — the same +"nothing auto-cleans this up" behavior, multiplied by the schedule). diff --git a/content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/04-guided-exercise-choosing-primitive.md b/content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/04-guided-exercise-choosing-primitive.md new file mode 100644 index 0000000..b796804 --- /dev/null +++ b/content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/04-guided-exercise-choosing-primitive.md @@ -0,0 +1,50 @@ +# Guided Exercise: Choosing the Right Workload Primitive + +Paper-and-thinking exercise before the Labs. + +## The scenario + +Your team needs four things running in the cluster: + +1. A nightly database backup that must run at 2am and finish before morning. +2. A log-shipping agent that must run on every node, including nodes added later. +3. A one-time script to backfill a new database column, run once during this deploy. +4. The main web application, always up, serving live traffic. + +## Your task + +For each of the four, name the correct primitive (Deployment, Job, CronJob, or DaemonSet) and +answer: what would go wrong if you used a Deployment for it instead? + +## A worked answer to check yourself against + +1. **CronJob.** A Deployment has no concept of "run on a schedule" — you'd need external + tooling to trigger it repeatedly, reinventing what CronJob already does. Using a bare Job + would run it once; CronJob is what recreates that Job on schedule. +2. **DaemonSet.** A Deployment's replica count is a number, not "one per node" — nothing + guarantees even coverage, and a newly added node wouldn't automatically get a copy the way a + DaemonSet guarantees. +3. **Job.** A Deployment expects its Pod to run forever; a script that's SUPPOSED to exit when + done would be treated as a crash and restarted forever, which is wrong for a one-time task. +4. **Deployment.** This is the actual always-on, N-replicas case the other three are explicitly + NOT designed for. + +## Self-check + +If you answered "Job" for #4 or "Deployment" for #1-3, revisit the lesson's decision table +before the Labs — getting this choice right the first time avoids building something that +technically runs but behaves wrong in a way that's easy to miss until it causes a real problem +(a backup CronJob accidentally deployed as a Job that only ever runs once, for example). + +## Instructor Notes + +**Facilitation:** Good as a quick individual exercise before revealing the answer — the "what +would go wrong" framing forces reasoning about the CONSEQUENCE of a wrong choice, not just +recall of a definition. + +**Approximate timing:** 8-10 minutes live with discussion; 8 minutes self-paced. + +**Optional extension:** ask what would need to be true for #1 (the nightly backup) to actually +be SAFE to run as a plain Job triggered manually once, instead of a CronJob — a good check that +students understand CronJob's value is specifically the SCHEDULING, not something inherent to +backups. diff --git a/content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/05-reflection.md b/content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/05-reflection.md new file mode 100644 index 0000000..4181837 --- /dev/null +++ b/content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/05-reflection.md @@ -0,0 +1,40 @@ +# Reflection: The Right Primitive Is a Correctness Decision, Not a Style Choice + +You just built a one-shot Job and a scheduled CronJob for real. Take a few minutes to connect +this to production thinking. + +## Reflection questions + +1. If your team accidentally deployed the database backup script as a Deployment instead of a + CronJob, what's the FIRST symptom you'd likely notice — and would it look like a bug in the + backup script itself, or something else? +2. A CronJob that fails silently every night for a week is a real, common incident pattern. What + would you want monitored to catch this quickly? +3. Why does "it ran once and worked" not prove a CronJob is configured correctly? + +## Real Ops takeaway + +**Workload-primitive mistakes often look like application bugs at first.** A backup script +"crashing" every few minutes on a Deployment isn't a bug in the script — the script did exactly +what it was told to do (exit when finished) and the Deployment is "correctly" restarting it +according to its own contract. Recognizing "wrong primitive," not "buggy code," as the root +cause saves real debugging time. + +**A schedule you never verify is a schedule you can't trust.** A CronJob failing every single +run since deployment is easy to miss if nobody checks — unlike a Deployment's crash-looping +Pods, which are visible in `kubectl get pods` by default, a CronJob's failed history requires +someone to actually look at `kubectl get jobs` or the CronJob's own status. Real teams monitor +CronJob success/failure explicitly rather than assuming silence means success. + +This module is complete. Continue to the next module. + +## Instructor Notes + +**Discussion questions for a live classroom:** question 1 is worth pushing on — many students' +first instinct is to debug the "crashing" script rather than question the workload type it's +running under. + +**Approximate timing:** 8 minutes live discussion; 5 minutes self-paced writing. + +**Optional extension:** ask students to name one alerting rule they'd add for a critical nightly +CronJob (e.g. "alert if no successful completion recorded in the last 25 hours"). diff --git a/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/00-lesson-configuration.md b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/00-lesson-configuration.md new file mode 100644 index 0000000..e163f9c --- /dev/null +++ b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/00-lesson-configuration.md @@ -0,0 +1,84 @@ +# Externalizing Configuration: ConfigMaps & Secrets + +## The problem: hardcoded configuration doesn't scale + +If your application's database URL, log level, or feature flags are baked directly into its +container image, changing any of them means rebuilding and redeploying the image — for a +one-line config change. Worse, the SAME image can't be reused across dev/staging/production if +each environment needs different settings compiled in. + +Kubernetes solves this by letting you inject configuration into a container at RUNTIME, +separately from the image itself — the same image, different config per environment. + +## ConfigMap: configuration that isn't secret + +A **ConfigMap** holds arbitrary key/value configuration data — log levels, feature flags, URLs, +whole config files. It's a plain Kubernetes object, readable by anyone with API access to that +Namespace, so it's for configuration that ISN'T sensitive. + +Two ways to consume a ConfigMap in a Pod: + +- **As environment variables** — each key becomes an env var the application reads normally. +- **As a mounted volume** — each key becomes a FILE inside the container, useful for + applications that expect to read configuration from disk rather than environment variables. + +```yaml +envFrom: +- configMapRef: + name: app-env +``` + +## Secret: the same idea, for sensitive data + +A **Secret** has the identical shape and consumption pattern as a ConfigMap (env vars or a +mounted volume) but is meant for sensitive values — passwords, API keys, tokens. Kubernetes +stores Secret data base64-ENCODED, not encrypted — this is an important distinction: +base64 is trivially reversible, so a Secret's real protection comes from Kubernetes' RBAC +(who can even read Secret objects at all, ROOT 09) and, in a properly configured cluster, +encryption at rest — not from the encoding itself. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: app-secret +type: Opaque +data: + api-key: c3VwZXJzZWNyZXQ= # base64 of "supersecret" — NOT encrypted +``` + +## Why the same mechanism, two different objects + +You might ask why Kubernetes doesn't just have one "Config" object with a "sensitive" flag. +Keeping them separate makes the DISTINCTION visible and enforceable — RBAC rules can grant +"read ConfigMaps" without granting "read Secrets," and a quick `kubectl get configmaps` never +accidentally exposes something that should have been a Secret. The separation is a deliberate +safety boundary, not redundant API surface. + +## Instructor Notes + +**Approximate timing:** 15 minutes read; 10 minutes live. + +**Demo plan:** Before the demonstration activity, it's worth showing `kubectl get secret +app-secret -o yaml` live and pointing out the base64 value, then decoding it on the spot +(`echo | base64 -d`) — the "this isn't actually encrypted" realization lands much better +seen than described. + +**Likely misconceptions to watch for:** +- Students very commonly assume Secrets are encrypted by Kubernetes automatically. Correct this + explicitly and early — it's a genuine, consequential security misunderstanding. +- The env-var vs. volume-mount choice is sometimes seen as arbitrary. Note one real + consideration: env vars are visible via `kubectl describe pod` and process inspection tools + inside the container more easily than a mounted file's contents — not a hard rule, but a + reason some teams prefer volume mounts specifically for Secrets. + +**Discussion question:** "If someone with `kubectl get secrets` access on your namespace can +trivially read every Secret's real value, what is a Secret actually protecting you from?" +(Answer: it protects against accidental exposure through normal channels like +`kubectl get configmaps`, source control, or logs — not against a determined actor with direct +API read access, which is exactly why RBAC restricting who has that access is the real control.) + +**Optional extension:** ask what changes about this picture on a cluster with encryption-at-rest +enabled for Secrets (answer: the ETCD-stored data itself becomes encrypted, protecting against +someone who gains access to the underlying etcd storage directly — still separate from and in +addition to RBAC controlling API-level access). diff --git a/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/01-demonstration-secret-not-encrypted.md b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/01-demonstration-secret-not-encrypted.md new file mode 100644 index 0000000..471800a --- /dev/null +++ b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/01-demonstration-secret-not-encrypted.md @@ -0,0 +1,66 @@ +# Demonstration: A Secret Is Encoded, Not Encrypted + +## Creating a Secret + +``` +$ kubectl create secret generic app-secret --from-literal=api-key=supersecret +secret/app-secret created +``` + +## Looking at it + +``` +$ kubectl get secret app-secret -o yaml +apiVersion: v1 +kind: Secret +metadata: + name: app-secret +data: + api-key: c3VwZXJzZWNyZXQ= +type: Opaque +``` + +## Decoding it — no special permission needed beyond reading the Secret itself + +``` +$ echo "c3VwZXJzZWNyZXQ=" | base64 -d +supersecret +``` + +Anyone who can run `kubectl get secret app-secret -o yaml` (i.e. anyone RBAC allows to READ +Secrets in this Namespace) can trivially recover the real value with a standard, built-in +command — no cracking, no special tooling. This is exactly why the lesson's point matters: the +protection here is WHO can read the Secret object at all (RBAC), not the base64 encoding itself. + +## Comparing to a ConfigMap + +``` +$ kubectl get configmap app-config -o yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + environment: production + log_level: info +``` + +A ConfigMap's values are stored in PLAIN TEXT — no encoding step at all. This is consistent with +ConfigMaps being explicitly for non-sensitive data; there's no pretense of protection to begin +with. + +## Instructor Notes + +**Demo script:** Run this live, actually decoding the value in front of the class — the gap +between "Secret" (a name implying protection) and what the command output shows is the entire +point, and it lands far better performed than read. + +**Approximate timing:** 6-8 minutes live; 5 minutes self-paced. + +**Common misconception to preempt:** this demonstration directly targets the single most common +misconception about Kubernetes Secrets — do not skip it even if time is short, since a wrong +belief here has real security consequences later. + +**Optional extension:** ask what a `SealedSecret` or an external secrets manager (mentioned by +name only, not built as a hands-on topic here) is trying to solve that a plain Kubernetes Secret +does not — a good forward pointer without requiring new infrastructure in this course. diff --git a/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/02-knowledge-check.md b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/02-knowledge-check.md new file mode 100644 index 0000000..6133ad4 --- /dev/null +++ b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/02-knowledge-check.md @@ -0,0 +1,48 @@ +# Knowledge Check: Configuration & Secrets + +--- + +**1. A teammate says "our database password is safe because it's stored as a Kubernetes +Secret." What's missing from that statement?** + +*Answer:* A Secret's data is base64-ENCODED, not encrypted — anyone who can read the Secret +object at all can trivially decode it. The real protection is RBAC (who is allowed to read +Secrets in that namespace) and, on a properly configured cluster, encryption at rest — not the +Secret object type itself. + +--- + +**2. Why does Kubernetes have two separate objects (ConfigMap and Secret) instead of one +"Config" object with a sensitivity flag?** + +*Answer:* Keeping them separate makes the distinction enforceable via RBAC — a role can grant +"read ConfigMaps" without granting "read Secrets" — and makes it visible at a glance +(`kubectl get configmaps` never risks accidentally listing something sensitive). + +--- + +**3. An application needs its configuration to change without rebuilding its container image +across dev/staging/production. Which Kubernetes mechanism solves this, and why not just bake +different configs into three separate images?** + +*Answer:* ConfigMaps (and Secrets, for sensitive values) injected at runtime via env vars or a +mounted volume. Baking config into the image means maintaining three separate images for what +should be identical application code — externalizing configuration lets the SAME image run +correctly in every environment. + +--- + +**4. True or false: mounting a ConfigMap as a volume and consuming it as environment variables +are functionally identical from the application's perspective.** + +*Answer:* False. An application reading environment variables needs code that reads env vars; an +application expecting a config FILE on disk needs the volume-mount approach. The right choice +depends on how the application itself is written to read configuration, not just personal +preference. + +## Instructor Notes + +**Facilitation:** Question 1 is the single most important takeaway from this module — confirm +it's genuinely understood, not just correctly guessed, before moving to the Labs. + +**Approximate timing:** 5-7 minutes. diff --git a/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/03-guided-exercise-choosing-mechanism.md b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/03-guided-exercise-choosing-mechanism.md new file mode 100644 index 0000000..deeb88c --- /dev/null +++ b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/03-guided-exercise-choosing-mechanism.md @@ -0,0 +1,50 @@ +# Guided Exercise: ConfigMap, Secret, Env Var, or Volume? + +Paper-and-thinking exercise before the Labs. + +## The scenario + +Your application needs four pieces of configuration: + +1. A log level (`debug`/`info`/`warn`) that ops sometimes needs to flip without redeploying. +2. A database password. +3. A large JSON config file the application reads from a specific path at startup. +4. A feature flag read once at container startup via an environment variable, per the + application's existing code. + +## Your task + +For each, decide: ConfigMap or Secret, and env var or volume mount — then justify each choice. + +## A worked answer to check yourself against + +1. **ConfigMap, env var.** Not sensitive; the application likely already reads it as an env var + (a common convention), and env vars are simple to change and trigger a rolling restart with. +2. **Secret, either — but env var is common here too**, since most applications expect + credentials via environment variables by convention. The KEY decision is Secret (sensitive), + not the delivery mechanism. +3. **ConfigMap, volume mount.** The application expects a FILE at a path, not an environment + variable — volume mount is the only mechanism that presents a ConfigMap as an actual file. +4. **ConfigMap (assuming not sensitive) or Secret (if it is), env var** — the application's + EXISTING code dictates this; you don't get to choose a mechanism the application wasn't + written to read. + +## Self-check + +If you chose "volume mount" for #4 or "env var" for #3, revisit the lesson — the deciding +factor here isn't which mechanism seems more "correct" in the abstract, it's what the +application actually expects. Getting this backwards means the application simply won't find +its configuration at all. + +## Instructor Notes + +**Facilitation:** Emphasize that #4's answer depends entirely on EXISTING application +behavior — a good moment to note that platform engineers frequently have to adapt Kubernetes +config delivery to match an application's assumptions, not the reverse. + +**Approximate timing:** 8-10 minutes live with discussion; 8 minutes self-paced. + +**Optional extension:** ask what would happen if a ConfigMap consumed as a volume mount is +UPDATED after the Pod is already running — a good bridge to the real (if not hands-on-tested +here) behavior that mounted ConfigMap files update automatically after a short delay, while +env-var-injected values do NOT update without a Pod restart. diff --git a/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/06-reflection.md b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/06-reflection.md new file mode 100644 index 0000000..65930e1 --- /dev/null +++ b/content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/06-reflection.md @@ -0,0 +1,39 @@ +# Reflection: Configuration Is Infrastructure, Secrets Are a Trust Boundary + +You just externalized configuration and handled a real Secret. Take a few minutes to connect +this to production practice. + +## Reflection questions + +1. Before this module, would you have assumed a Kubernetes Secret was encrypted? What's the + real-world consequence of a team believing that when it isn't true? +2. If your team's RBAC accidentally granted a low-trust role `get secrets` access across an + entire namespace, what's actually exposed — and would your team necessarily notice? +3. Why does separating configuration from the container image matter beyond convenience — think + about what it enables for testing or incident response. + +## Real Ops takeaway + +**A misunderstood Secret is a false sense of security.** Teams that believe Secrets are +encrypted by default sometimes skip additional controls (proper RBAC scoping, external secrets +management, encryption at rest) that they'd otherwise recognize as necessary. The gap between +perceived and actual protection is exactly where real incidents happen. + +**Externalized configuration is what makes rollback and incident response fast.** If a bad +config value ships (a wrong feature flag, an incorrect log level flooding storage), fixing it +means updating a ConfigMap/Secret and letting Pods pick up the change — not rebuilding and +redeploying an entire image. This is a direct, practical payoff of the separation this module +taught, not just an architectural nicety. + +This module is complete. Continue to the next module. + +## Instructor Notes + +**Discussion questions for a live classroom:** question 2 works well as a small-group exercise — +have students actually list what a namespace's Secrets typically contain in a real application +(DB creds, API keys, TLS certs) to make the exposure concrete. + +**Approximate timing:** 8 minutes live discussion; 5 minutes self-paced writing. + +**Optional extension:** ask students to draft a one-line policy statement their hypothetical +team would adopt about who is allowed `get`/`list` access to Secrets in production namespaces. diff --git a/content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/03-demonstration-ingress-routing.md b/content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/03-demonstration-ingress-routing.md new file mode 100644 index 0000000..7aae3cd --- /dev/null +++ b/content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/03-demonstration-ingress-routing.md @@ -0,0 +1,72 @@ +# Demonstration: Tracing a Request Through Ingress + +## The setup + +An Ingress `web-ingress` routes `app.example.com/` to Service `web-svc`, which fronts a +Deployment `web-app`. Nothing works yet from outside the cluster — let's see why, step by step. + +## Step 1: is the Ingress object even doing anything? + +``` +$ kubectl get ingress web-ingress +NAME CLASS HOSTS ADDRESS PORTS AGE +web-ingress traefik app.example.com 80 2m +``` + +`ADDRESS` is empty. On a real cluster, this column fills in once the Ingress controller has +actually processed the object and assigned it a reachable address — an empty address after a +couple of minutes is the first sign to check the controller itself, not your YAML. + +## Step 2: is the controller running at all? + +``` +$ kubectl get pods -n kube-system -l app.kubernetes.io/name=traefik +NAME READY STATUS RESTARTS AGE +traefik-7d8f9c6b5d-abcde 1/1 Running 0 10d +``` + +Running fine — so the ADDRESS delay in this case is transient, not a missing controller. (If +this returned no Pods at all, THAT would be the actual root cause — no controller means no +Ingress object ever gets acted on, exactly like the demonstration in ROOT 07's Ingress lesson +described.) + +## Step 3: once ADDRESS populates, does the route actually work? + +``` +$ kubectl get ingress web-ingress +NAME CLASS HOSTS ADDRESS PORTS AGE +web-ingress traefik app.example.com 10.0.1.50 80 3m + +$ curl -H "Host: app.example.com" http://10.0.1.50/ +...web-app's real response... +``` + +Success — but notice the `-H "Host: app.example.com"` header. Ingress routes by HOSTNAME, not +just by IP — hitting the Ingress controller's address WITHOUT the matching Host header would +route differently (or not at all), because that's literally what the Ingress rule is matching +on. + +## The general shape + +Symptom (nothing reachable from outside) has at least three DIFFERENT possible layers: the +Ingress object itself, the controller processing it, and the Service/Pod chain underneath it +(already covered in ROOT 07's Services module). Check them in that order — Ingress ADDRESS, +then controller health, then the underlying Service — rather than guessing at DNS or firewall +issues first. + +## Instructor Notes + +**Demo script:** If a real Ingress controller is available in the demo environment, run this +live including the Host-header nuance — students are frequently surprised that `curl ` +alone doesn't work the way they expect. + +**Approximate timing:** 8-10 minutes live; 5-7 minutes self-paced. + +**Common misconception to preempt:** students often expect an Ingress's IP/address to work like +a Service's ClusterIP (reachable directly, no extra header needed). Reinforce that Ingress +routing is fundamentally HOSTNAME/PATH-based, which is exactly what makes one Ingress able to +route many different domains through one entry point. + +**Optional extension:** ask what would happen if TWO Ingress objects both claimed the same host ++ path — a good forward pointer to why Ingress configuration conflicts are a real operational +concern on a shared cluster, without needing to build a hands-on lab around it here. diff --git a/content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/04-guided-exercise-network-isolation-plan.md b/content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/04-guided-exercise-network-isolation-plan.md new file mode 100644 index 0000000..598d5e5 --- /dev/null +++ b/content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/04-guided-exercise-network-isolation-plan.md @@ -0,0 +1,47 @@ +# Guided Exercise: Planning Network Isolation + +Paper-and-thinking exercise before the Lab. + +## The scenario + +A `database` Pod (label `app=database`) in the `netpol` namespace should only ever be reachable +by Pods labeled `role=backend` — not the frontend, not anything else in the cluster. + +## Your task + +1. Before writing any YAML: right now, with NO NetworkPolicy in place, can the frontend reach + the database? Why? +2. What would a NetworkPolicy that ONLY specifies `podSelector: { matchLabels: { app: database + } }` and no `ingress` rules at all do to traffic reaching the database — including from + `role=backend` Pods? +3. Sketch (in words, not full YAML) what the `ingress` rule needs to say to allow ONLY + `role=backend` traffic in. +4. If a new Pod is later created with `role=backend` in a DIFFERENT namespace, would your policy + as sketched in #3 allow it? What would need to change if cross-namespace access should be + allowed? + +## A worked answer to check yourself against + +1. Yes — by default, every Pod can reach every other Pod regardless of labels or namespace + (ROOT 07's Ingress & NetworkPolicy lesson). Nothing restricts this until a policy exists. +2. It would block ALL incoming traffic to the database, including from `role=backend` — an + empty `ingress` list under a policy that selects the Pod means "no traffic is allowed in," + not "no policy applies." +3. An `ingress` rule with `from: [{ podSelector: { matchLabels: { role: backend } } }]` — this + allows traffic specifically from Pods carrying that label, and nothing else. +4. Not automatically — a bare `podSelector` in the `from` clause matches Pods in the SAME + namespace as the policy by default. Allowing a different namespace requires adding a + `namespaceSelector` alongside the `podSelector` in the same `from` entry. + +## Instructor Notes + +**Facilitation:** Question 2 is the one most likely to trip students up — it directly reinforces +the "empty ingress rules means deny-all, not no-op" point from the lesson, which is worth +confirming actually landed before the Lab. + +**Approximate timing:** 10-12 minutes live with discussion; 10 minutes self-paced. + +**Optional extension:** ask what a security-minded team would ALSO want to restrict about this +database Pod's OWN outbound (egress) traffic, beyond who can reach it — a light forward pointer +to ROOT 09's security-control framing of NetworkPolicy, without requiring students to build an +egress rule here. diff --git a/content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/05-reflection.md b/content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/05-reflection.md new file mode 100644 index 0000000..8f0b5c6 --- /dev/null +++ b/content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/05-reflection.md @@ -0,0 +1,40 @@ +# Reflection: Reachability Should Be a Decision, Not a Default + +You just configured real Ingress routing and real network isolation. Take a few minutes to +connect this to production thinking. + +## Reflection questions + +1. Before this module, would you have assumed a fresh Kubernetes cluster isolates Pods from + each other by default? Has that changed? +2. If your team runs a multi-tenant cluster (several unrelated applications sharing it), what's + the risk of NEVER applying any NetworkPolicy at all? +3. Ingress lets many different domains share one entry point. What's the operational benefit of + that beyond just "fewer objects to manage"? + +## Real Ops takeaway + +**"No NetworkPolicy" is not neutral — it's full reachability.** Exactly like an unhardened +Pod's SecurityContext (ROOT 09 covers this in depth), the absence of a NetworkPolicy isn't a +safe default; it's the MOST permissive configuration available. On any cluster where more than +one team or application shares infrastructure, that's a real, often-overlooked exposure. + +**Centralizing external entry points through Ingress reduces attack surface, not just object +count.** Every separate LoadBalancer Service is a separate externally-reachable endpoint to +secure, monitor, and patch. Routing through one (or a small number of) Ingress controllers means +security controls — TLS termination, rate limiting, WAF rules — are configured and audited in +one place instead of scattered across every application team's own Service definitions. + +This module is complete. Continue to the next module. + +## Instructor Notes + +**Discussion questions for a live classroom:** question 2 is worth pushing on with a concrete +scenario — ask what a compromised low-value application (a demo app, a staging environment) +could reach on a cluster with zero NetworkPolicies, versus one with sensible defaults. + +**Approximate timing:** 8 minutes live discussion; 5 minutes self-paced writing. + +**Optional extension:** ask students to sketch (in words) what a reasonable DEFAULT-DENY policy +for an entire new namespace would look like, before any application-specific allow rules are +added — previews the security-control framing ROOT 09 builds on. diff --git a/content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/03-demonstration-rbac-denial.md b/content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/03-demonstration-rbac-denial.md new file mode 100644 index 0000000..aa549de --- /dev/null +++ b/content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/03-demonstration-rbac-denial.md @@ -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. diff --git a/content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/04-guided-exercise-least-privilege-role.md b/content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/04-guided-exercise-least-privilege-role.md new file mode 100644 index 0000000..24f1d38 --- /dev/null +++ b/content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/04-guided-exercise-least-privilege-role.md @@ -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=` 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. diff --git a/content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/07-reflection.md b/content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/07-reflection.md new file mode 100644 index 0000000..164872f --- /dev/null +++ b/content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/07-reflection.md @@ -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. diff --git a/content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/03-demonstration-default-deny.md b/content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/03-demonstration-default-deny.md new file mode 100644 index 0000000..a032e0f --- /dev/null +++ b/content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/03-demonstration-default-deny.md @@ -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 + +``` + +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 + + +$ 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). diff --git a/content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/04-guided-exercise-egress-plan.md b/content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/04-guided-exercise-egress-plan.md new file mode 100644 index 0000000..e257ffb --- /dev/null +++ b/content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/04-guided-exercise-egress-plan.md @@ -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. diff --git a/content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/09-reflection.md b/content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/09-reflection.md new file mode 100644 index 0000000..0aa127f --- /dev/null +++ b/content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/09-reflection.md @@ -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. diff --git a/content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/04-guided-exercise-endpoint-mismatch.md b/content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/04-guided-exercise-endpoint-mismatch.md new file mode 100644 index 0000000..e40ccca --- /dev/null +++ b/content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/04-guided-exercise-endpoint-mismatch.md @@ -0,0 +1,52 @@ +# Guided Exercise: Applying the Framework to a Broken Service, on Paper First + +Before the `endpoint-fix-task` Lab, walk through the framework yourself against a described +symptom — no cluster needed yet. + +## The scenario + +A teammate says: "`my-svc` isn't routing any traffic to our Pods, and I don't know why — the +Pods look healthy in `kubectl get pods`." + +## Your task + +Write out, step by step, what you would actually DO for each framework stage, before looking at +the worked answer: + +1. **Observe** — what's the precise, specific symptom here (not "it's broken")? +2. **Gather Evidence** — name the exact `kubectl` commands you'd run, in order, and what each + one is checking for. +3. **Form Hypothesis** — based on what ROOT 07's Services lesson taught you, what's the most + likely cause of "Pods healthy, but Service routes nowhere"? +4. **Test** — how would you CONFIRM that hypothesis before changing anything? +5. **Repair** — what's the minimal fix, assuming your hypothesis is correct? +6. **Validate** — what would you check afterward to PROVE the fix worked, not just assume it? + +## A worked answer to check yourself against + +1. Traffic sent to `my-svc` never reaches any Pod, despite the Pods themselves reporting + `Running`/`Ready` — this rules out "the Pods are unhealthy" as the cause. +2. `kubectl get endpoints my-svc` (does it show any Pod IPs at all?), then `kubectl describe + service my-svc` (what selector is it actually using?), then `kubectl get pods --show-labels` + (what labels do the real Pods actually carry?). +3. A Service-to-Pod label selector mismatch — exactly the failure mode ROOT 07's demonstration + walked through, where a Service's selector no longer matches the Pods' actual labels. +4. Directly compare the Service's selector (from `describe service`) against the Pods' labels + (from `get pods --show-labels`) side by side — confirmed if they genuinely don't match. +5. Correct the Service's selector (or the Pods' labels, whichever is actually wrong) so they + match again. +6. `kubectl get endpoints my-svc` now shows the Pod IPs populated — not just "the YAML looks + right now," but the actual Endpoints object reflecting real, matched Pods. + +## Instructor Notes + +**Facilitation:** This exercise deliberately reuses ROOT 07's own content — a good moment to +point out explicitly that the framework's value is applying PRIOR knowledge systematically, not +learning something entirely new each time. + +**Approximate timing:** 10-12 minutes live with discussion; 10 minutes self-paced. + +**Optional extension:** ask what would change about steps 2-4 if the symptom were instead +"Pods healthy, but ONE specific caller can't reach the Service while others can" — a good bridge +to considering NetworkPolicy (ROOT 09) as an alternative hypothesis once the simpler +selector-mismatch explanation is ruled out. diff --git a/content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/06-reflection.md b/content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/06-reflection.md new file mode 100644 index 0000000..2c4042a --- /dev/null +++ b/content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/06-reflection.md @@ -0,0 +1,45 @@ +# Reflection: A Framework You've Actually Used Is One You'll Trust Under Pressure + +You just solved two real, different failures — a CrashLoopBackOff and a broken Service +selector — using the same seven-step framework both times. Take a few minutes to consolidate +this. + +## Reflection questions + +1. Which framework step did you personally find easiest to skip or rush through across both + Labs? Why do you think that step specifically tempts shortcuts? +2. Compare how you approached `crashloop-fix` versus `endpoint-fix-task`. What was IDENTICAL + about your process, and what was different only because the evidence looked different? +3. Now that you've practiced this framework on two failures you'd never seen exact instructions + for, how confident do you feel applying it to a THIRD, completely unfamiliar Kubernetes + failure? What would you still want to double-check? + +## Real Ops takeaway + +**The framework's value shows up most on failures you've never seen before.** Anyone can +memorize "if X symptom, do Y fix" for a handful of common problems — that knowledge doesn't +transfer. Observe → Evidence → Hypothesis → Test → Repair → Validate → Prevent Recurrence +transfers to failures nobody has specifically taught you, because it's a PROCESS, not a lookup +table. + +**"Prevent Recurrence" is what separates incident response from firefighting.** Both Labs in +this module had a real, nameable prevention practice (verify command strings before deploying; +keep label keys consistent between a Deployment and its Service). Teams that consistently ask +this question after every fix accumulate fewer repeat incidents over time — not because they got +lucky, but because they closed the actual gap each time. + +This module is complete. Continue to the next module. + +## Instructor Notes + +**Discussion questions for a live classroom:** question 1 tends to produce honest, useful +answers — "Prevent Recurrence" and "Form Hypothesis" (skipping straight to a guessed fix) are +the two most commonly admitted shortcuts, both worth normalizing as a real temptation to resist +rather than treating as a personal failing. + +**Approximate timing:** 8-10 minutes live discussion; 5-7 minutes self-paced writing. + +**Optional extension:** ask students to apply the framework, from memory and without re-reading +this module, to a THIRD failure type they haven't practiced yet (e.g., a Pod stuck `Pending` — +not built as a hands-on Lab in this module, just a thought exercise) to test genuine +transferability. diff --git a/content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/01-lesson-devops-platform-thinking.md b/content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/01-lesson-devops-platform-thinking.md new file mode 100644 index 0000000..e558cae --- /dev/null +++ b/content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/01-lesson-devops-platform-thinking.md @@ -0,0 +1,88 @@ +# DevOps as an Operating Model, Not a Job Title + +## You've already been doing this — just not naming it yet + +Every course before this one taught you a Kubernetes skill: declaring desired state, hardening +a Pod, diagnosing a failure with a repeatable framework, running an incident with real +discipline. This module doesn't teach a new hands-on skill — it names the OPERATING MODEL those +skills are all part of, so you can recognize it and apply it beyond Kubernetes specifically. + +## DevOps: a set of practices, not a team you join + +"DevOps" is often misused as a job title ("I'm a DevOps engineer") or a team name ("throw it +over to DevOps"). Properly understood, DevOps is a set of PRACTICES for how development and +operations work together: shorter feedback loops, automation over manual process, and shared +ownership of what happens after code ships — not a separate team that operations gets outsourced +to. + +Concretely, you already practiced core DevOps ideas without the label: + +- **Declarative thinking** (ROOT 05) — describing desired state and letting a system enforce it, + instead of manually executing steps, is the same principle behind Infrastructure as Code. +- **Fast, safe rollback** (ROOT 06) — treating recovery as a rehearsed, ordinary action rather + than a crisis is a direct DevOps practice (fast feedback, low-drama recovery). +- **A repeatable troubleshooting framework** (ROOT 10) and **structured incident response** + (ROOT 12) — both are operational discipline, the "Ops" half of DevOps, practiced for real. + +## Platform Engineering: what DevOps looks like at scale + +**Platform engineering** is the next layer: instead of every application team reinventing how +to deploy safely, monitor, and secure their own workloads, a platform team builds SELF-SERVICE +tooling and golden paths that let other engineers move fast without needing deep infrastructure +expertise themselves. The Kubernetes cluster, the CI/CD pipeline, the observability stack — all +of it becomes a PRODUCT a platform team builds for INTERNAL customers (other engineers), not +just infrastructure that happens to exist. + +A useful lens: platform engineering exists to **reduce cognitive load**. A developer who can +deploy a well-configured, secure, observable service through a simple, guided path doesn't need +to personally understand every Kubernetes concept from ROOT 05-10 to ship safely — the platform +encodes that expertise once, for everyone. + +## Where Kubernetes fits in the larger picture + +Kubernetes is INFRASTRUCTURE, not the whole platform. A real platform engineering practice +typically layers several things around it: + +- **Declarative delivery** — describing what should be deployed and letting automation apply it + (the GitOps model — future ROOT 11 content, requires a Git+deployment-automation environment + this course doesn't yet provide). +- **Continuous integration/delivery** — automatically testing and shipping code changes (CI/CD — + future content, requires a CI runner environment). +- **Infrastructure as Code** — declaring cloud/infrastructure resources the same way you declare + Kubernetes objects (future content, requires a provisioning environment like Terraform). +- **Observability** — knowing what's actually happening in production beyond "is the Pod + Running" (future content, requires a metrics/logging/tracing stack). + +None of these require abandoning what you learned in ROOT 05-10 — they build directly on the +same mental models (desired vs. actual state, evidence-based diagnosis, least privilege) applied +to a wider set of systems. + +## Instructor Notes + +**Lesson goal:** Give students a name and a framework for practices they've already exercised, +and an honest map of what ROOT 11 will eventually cover once appropriate lab environments exist — +without pretending those labs exist today. + +**Approximate timing:** 20 minutes read; 15 minutes live with discussion. + +**Preparation:** None beyond having read `docs/ROOT11_DEVOPS_PLATFORM_SCOPE.md` to speak +accurately about what's planned versus what exists. + +**Likely misconceptions to watch for:** +- "DevOps" as a job title/team is a very common prior belief — correct this directly and early, + it's the central point of the lesson. +- Platform engineering is sometimes conflated with "just Kubernetes." Emphasize the self-service/ + golden-path angle — the PRODUCT framing, not just the technology. + +**Questions to ask students:** "Which ROOT_ course so far most directly practiced a DevOps +principle, even though it was never labeled that way?" (Most will cite ROOT 06's rollback +discipline or ROOT 12's incident framework — both are legitimate, good answers.) + +**When to let a student struggle vs. provide a hint:** Not applicable — this is an orientation +lesson with no hands-on task. + +**Optional extension:** ask students to name one manual process at a company they've worked for +(or heard about) that platform engineering-style self-service tooling could have eliminated. + +**Expected takeaway:** DevOps and platform engineering are the organizing ideas behind almost +everything ROOT_ has already taught — not a new, separate subject to learn from scratch. diff --git a/content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/02-knowledge-check.md b/content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/02-knowledge-check.md new file mode 100644 index 0000000..b758c8b --- /dev/null +++ b/content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/02-knowledge-check.md @@ -0,0 +1,46 @@ +# Knowledge Check: DevOps & Platform Thinking + +--- + +**1. A colleague says "DevOps is the team that handles our servers." What's wrong with that +statement?** + +*Answer:* DevOps describes a set of PRACTICES (shared ownership, fast feedback loops, +automation) for how development and operations work together — not a separate team that +operations gets handed off to. Treating it as a team name misses the point that these practices +are meant to be shared across engineering, not isolated in one group. + +--- + +**2. Which ROOT_ course's central skill is most directly an example of "declarative thinking" +applied outside Kubernetes objects specifically?** + +*Answer:* ROOT 05's desired-state model is the direct root of declarative thinking generally — +Infrastructure as Code applies the exact same principle (describe what should exist, let +automation enforce it) to cloud infrastructure rather than Kubernetes objects specifically. + +--- + +**3. True or false: platform engineering's main goal is building the most technically +sophisticated internal tooling possible.** + +*Answer:* False. Its main goal is reducing COGNITIVE LOAD for other engineers via self-service, +golden-path tooling — sophistication that doesn't make another engineer's job easier isn't the +point, even if it's technically impressive. + +--- + +**4. Why can't this course build a hands-on GitOps or CI/CD Lab the same way ROOT 05-10 built +Kubernetes Labs?** + +*Answer:* Those require different infrastructure the current Lab platform doesn't provide (a +Git repository + deployment-automation tool like ArgoCD for GitOps; a CI runner for CI/CD) — see +`docs/ROOT11_DEVOPS_PLATFORM_SCOPE.md`. Building a fake version of either would teach an +inaccurate mental model, which is worse than honestly deferring the hands-on practice. + +## Instructor Notes + +**Facilitation:** Question 4 is worth stating plainly in a live classroom — students should +understand this is a genuine platform limitation being handled honestly, not an oversight. + +**Approximate timing:** 5 minutes. diff --git a/content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/03-reflection.md b/content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/03-reflection.md new file mode 100644 index 0000000..ad6e9ee --- /dev/null +++ b/content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/03-reflection.md @@ -0,0 +1,45 @@ +# Reflection: Where You Already Practiced This + +You've now named the operating model behind most of what ROOT_ taught. Take a few minutes to +trace it back through your own experience in this program. + +## Reflection questions + +1. Pick one specific moment from ROOT 05-10 or ROOT 12 where you practiced a DevOps/platform + engineering principle without it being labeled that way. Name the principle explicitly now. +2. If you were designing a platform team's "golden path" for deploying a new service safely, + which THREE things from ROOT_ (health probes, RBAC, resource limits, NetworkPolicy, etc.) + would you bake in as defaults rather than leave to each team to remember? +3. What's one thing about GitOps, CI/CD, Infrastructure as Code, or observability you're + genuinely curious about, now that you have the Kubernetes foundation to eventually build + hands-on skill in it? + +## Real Ops takeaway + +**The skills you already have transfer further than "just Kubernetes."** Desired-state +thinking, least privilege, evidence-based troubleshooting, and structured incident response +aren't Kubernetes-specific facts — they're how experienced platform engineers reason about ANY +complex system. Recognizing that transfer is what turns "I learned Kubernetes" into "I think +like a platform engineer." + +**Honest scope beats fake completeness.** This course didn't pretend to teach you GitOps or +CI/CD hands-on today — it told you exactly what's missing and why, and pointed at what it would +take to build it for real. That's the same standard the rest of ROOT_ held itself to +throughout — a module that doesn't exist yet is more useful, honestly labeled, than one that +exists in name only. + +This module is complete. + +## Instructor Notes + +**Discussion questions for a live classroom:** question 2 works well as a small-group exercise — +have groups compare their "golden path defaults" lists and discuss disagreements. + +**Approximate timing:** 10 minutes live discussion; 8 minutes self-paced writing. + +**Optional extension:** ask students to research (outside class) one real company's public +engineering blog post about their platform engineering practice, and bring one concrete example +to share next session. + +**Expected takeaway:** ROOT_ built real, transferable engineering judgment — this module makes +that transfer explicit rather than leaving students to discover it on their own. diff --git a/infra/scripts/validate-course-content.js b/infra/scripts/validate-course-content.js new file mode 100644 index 0000000..4c71848 --- /dev/null +++ b/infra/scripts/validate-course-content.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node +// Phase 6C §24 — programmatic content validation: fails loudly if the course/module/activity +// content model has any structural defect. Run against a live Postgres (same connection the +// Platform API uses) plus the repo's own scenarios/data, scenarios/rootnode-path.json, and +// content/root/ files — cross-checks the database against the filesystem rather than trusting +// either alone. Exit code 1 on any failure; safe to wire into CI later, not built as CI here +// (no CI system exists in this repo to wire into). +// +// Usage: PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \ +// node infra/scripts/validate-course-content.js + +const fs = require('fs'); +const path = require('path'); +const REPO_ROOT = path.join(__dirname, '../..'); +// Reuses platform/api's own `pg` dependency rather than adding a new package.json/node_modules +// just for this script — same "no new dependency where an existing one already covers it" +// convention the rest of this codebase follows. +const { Client } = require(path.join(REPO_ROOT, 'platform/api/node_modules/pg')); +const DB_URL = process.env.PLATFORM_DB_URL || 'postgres://postgres:postgres@localhost:5433/rootnode_platform'; + +let errors = 0; +function fail(msg) { console.error(` FAIL — ${msg}`); errors++; } +function ok(msg) { console.log(` ok — ${msg}`); } + +async function main() { + const client = new Client({ connectionString: DB_URL }); + await client.connect(); + + const scenarioIds = new Set(fs.readdirSync(path.join(REPO_ROOT, 'scenarios/data')).filter((f) => f.endsWith('.json')).map((f) => f.replace('.json', ''))); + const assessmentIds = new Set((await client.query('SELECT assessment_definition_id FROM assessment_definitions')).rows.map((r) => r.assessment_definition_id)); + const courses = (await client.query('SELECT * FROM courses')).rows; + const modules = (await client.query('SELECT * FROM course_modules')).rows; + const activities = (await client.query('SELECT * FROM module_activities')).rows; + + const courseIds = new Set(courses.map((c) => c.course_id)); + const moduleIds = new Set(modules.map((m) => m.module_id)); + const activityIds = new Set(activities.map((a) => a.activity_id)); + + console.log(`Loaded ${courses.length} courses, ${modules.length} modules, ${activities.length} activities, ${scenarioIds.size} scenario files.\n`); + + // ── No duplicate IDs (PK already enforces this at the DB level, but verify the exported + // JS-level arrays too, catching an accidental cross-table id collision the DB doesn't check) ─ + for (const [label, ids] of [['course', courses.map((c) => c.course_id)], ['module', modules.map((m) => m.module_id)], ['activity', activities.map((a) => a.activity_id)]]) { + const seen = new Set(); const dupes = new Set(); + for (const id of ids) { if (seen.has(id)) dupes.add(id); seen.add(id); } + if (dupes.size) fail(`duplicate ${label} id(s): ${[...dupes].join(', ')}`); else ok(`no duplicate ${label} ids`); + } + + // ── No orphan modules/activities (referencing a course/module that doesn't exist) ──────── + const orphanModules = modules.filter((m) => !courseIds.has(m.course_id)); + if (orphanModules.length) fail(`orphan module(s) referencing a missing course: ${orphanModules.map((m) => m.module_id).join(', ')}`); else ok('no orphan modules'); + const orphanActivities = activities.filter((a) => !moduleIds.has(a.module_id)); + if (orphanActivities.length) fail(`orphan activity(ies) referencing a missing module: ${orphanActivities.map((a) => a.activity_id).join(', ')}`); else ok('no orphan activities'); + + // ── No invalid scenario/assessment references ──────────────────────────────────────────── + const badScenarioRefs = activities.filter((a) => a.scenario_id && !scenarioIds.has(a.scenario_id)); + if (badScenarioRefs.length) fail(`activity(ies) referencing a missing scenario file: ${badScenarioRefs.map((a) => `${a.activity_id}->${a.scenario_id}`).join(', ')}`); else ok('every lab activity scenario_id resolves to a real scenario file'); + const badAssessmentRefs = activities.filter((a) => a.assessment_definition_id && !assessmentIds.has(a.assessment_definition_id)); + if (badAssessmentRefs.length) fail(`activity(ies) referencing a missing assessment_definition: ${badAssessmentRefs.map((a) => `${a.activity_id}->${a.assessment_definition_id}`).join(', ')}`); else ok('every assessment/capstone activity resolves to a real assessment_definition'); + + // ── No impossible prerequisite references (a module/course pointing at an id that doesn't + // exist at all — distinct from Phase 6A's deliberate "PLANNED = vacuously satisfied" rule, + // this catches a genuine TYPO in a prerequisite id) ────────────────────────────────────── + for (const c of courses) { + for (const prereq of c.prerequisites || []) { + if (!courseIds.has(prereq)) fail(`course ${c.course_id} lists a prerequisite that doesn't exist: ${prereq}`); + } + } + for (const m of modules) { + for (const prereq of m.prerequisites || []) { + if (!moduleIds.has(prereq)) fail(`module ${m.module_id} lists a prerequisite that doesn't exist: ${prereq}`); + } + } + ok('every course/module prerequisite id resolves to a real course/module (or was already reported above)'); + + // ── No course-tree cycles (a module/course that is its own transitive prerequisite) ────── + function hasCycle(items, idKey) { + const byId = new Map(items.map((i) => [i[idKey], i])); + const state = new Map(); // id -> 'visiting' | 'done' + let cyclic = null; + function visit(id, chain) { + if (cyclic) return; + const item = byId.get(id); + if (!item) return; + if (state.get(id) === 'visiting') { cyclic = [...chain, id]; return; } + if (state.get(id) === 'done') return; + state.set(id, 'visiting'); + for (const prereq of item.prerequisites || []) visit(prereq, [...chain, id]); + state.set(id, 'done'); + } + for (const item of items) visit(item[idKey], []); + return cyclic; + } + const courseCycle = hasCycle(courses, 'course_id'); + if (courseCycle) fail(`course prerequisite cycle: ${courseCycle.join(' -> ')}`); else ok('no course prerequisite cycles'); + const moduleCycle = hasCycle(modules, 'module_id'); + if (moduleCycle) fail(`module prerequisite cycle: ${moduleCycle.join(' -> ')}`); else ok('no module prerequisite cycles'); + + // ── No sequence collisions within one module/course (two siblings sharing a sequence + // number is a real authoring bug — found and fixed once already in Phase 6B, see + // 023_phase6b_curriculum.sql's ROOT 07 m02/m04 comment) ────────────────────────────────── + function checkSequenceCollisions(items, groupKey, label) { + const groups = new Map(); + for (const item of items) { + const key = item[groupKey]; + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(item.sequence); + } + let collided = 0; + for (const [key, seqs] of groups) { + const dupes = seqs.filter((s, i) => seqs.indexOf(s) !== i); + if (dupes.length) { fail(`${label} sequence collision in ${key}: sequence ${[...new Set(dupes)].join(',')} used more than once`); collided++; } + } + if (!collided) ok(`no ${label} sequence collisions`); + } + checkSequenceCollisions(modules, 'course_id', 'module'); + checkSequenceCollisions(activities, 'module_id', 'activity'); + + // ── Instructor notes never leak to students (content.js's own splitContent, exercised + // directly here rather than trusted). content.js resolves content_path against + // CONTENT_ROOT_DIR if set, defaulting to one level up from its own src/ (correct only INSIDE + // the Docker image, where the Dockerfile COPYs content/ next to src/) — running it directly + // on the host here needs this override so it resolves against the real repo root instead. + process.env.CONTENT_ROOT_DIR = REPO_ROOT; + const { loadContent } = require(path.join(REPO_ROOT, 'platform/api/src/content.js')); + let leakChecked = 0, leaked = 0; + for (const a of activities) { + if (!a.content_path) continue; + const c = loadContent(a.content_path); + leakChecked++; + if (c && c.shared && c.shared.includes('## Instructor Notes')) { fail(`${a.activity_id}: 'shared' (student-visible) content still contains the Instructor Notes heading — split failed`); leaked++; } + } + if (!leaked) ok(`instructor notes correctly excluded from student-visible content in all ${leakChecked} activities with a content_path`); + + // ── No content_path referencing a missing file ─────────────────────────────────────────── + let missingContent = 0; + for (const a of activities) { + if (!a.content_path) continue; + if (!fs.existsSync(path.join(REPO_ROOT, a.content_path))) { fail(`${a.activity_id}: content_path does not exist on disk: ${a.content_path}`); missingContent++; } + } + if (!missingContent) ok('every content_path resolves to a real file on disk'); + + // ── A 'lab' activity must have a scenario_id; an 'assessment'/'capstone' must have an + // assessment_definition_id (the DB CHECK constraints already enforce this — verified here + // too so this script fails loudly even against a hand-edited row that somehow bypassed it) ─ + const labsMissingScenario = activities.filter((a) => a.activity_type === 'lab' && !a.scenario_id); + if (labsMissingScenario.length) fail(`lab activity(ies) with no scenario_id: ${labsMissingScenario.map((a) => a.activity_id).join(', ')}`); else ok('every lab activity has a scenario_id'); + const assessmentsMissingDef = activities.filter((a) => ['assessment', 'capstone'].includes(a.activity_type) && !a.assessment_definition_id); + if (assessmentsMissingDef.length) fail(`assessment/capstone activity(ies) with no assessment_definition_id: ${assessmentsMissingDef.map((a) => a.activity_id).join(', ')}`); else ok('every assessment/capstone activity has an assessment_definition_id'); + + // ── Section 16/17 — a module marked READY (self-paced-sufficient) must not defer explanation + // to a live instructor in its student-visible content. Scans the actual 'shared' half of + // content.js's real split (never the instructor-notes half, which is explicitly ALLOWED to + // say "explain live"), and only against READY modules — instructor-led-only PARTIAL modules + // (see docs/ROOT_PILOT_CURRICULUM.md) are not held to the self-paced bar. ─────────────────── + const readyModuleIds = new Set(modules.filter((m) => m.status === 'ready').map((m) => m.module_id)); + const deferralPhrases = [/your instructor will explain/i, /ask your instructor/i, /your instructor will cover/i]; + let deferralsFound = 0; + for (const a of activities) { + if (!a.content_path || !readyModuleIds.has(a.module_id)) continue; + const c = loadContent(a.content_path); + if (!c || !c.shared) continue; + if (deferralPhrases.some((re) => re.test(c.shared))) { fail(`${a.activity_id}: self-paced-marked module's student-visible content defers to a live instructor`); deferralsFound++; } + } + if (!deferralsFound) ok('no READY module\'s student-visible content defers explanation to a live instructor'); + + // ── Duplicate scenario mapping — flagged, not failed, since Phase 6A/6B both document + // DELIBERATE reuse (rootnode-probe-port-mismatch in both the gold module and Real Ops; + // pod-security-context/rbac-clusterrole as documented independent-practice duplicates). This + // prints the list for a human to confirm against docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md's + // 'duplicate concepts identified' section rather than failing the build. ─────────────────── + const scenarioUsage = new Map(); + for (const a of activities) { + if (!a.scenario_id) continue; + if (!scenarioUsage.has(a.scenario_id)) scenarioUsage.set(a.scenario_id, []); + scenarioUsage.get(a.scenario_id).push(a.activity_id); + } + const reused = [...scenarioUsage.entries()].filter(([, ids]) => ids.length > 1); + console.log(` info — ${reused.length} scenario(s) intentionally reused across multiple activities (see docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md):`); + for (const [sid, ids] of reused) console.log(` ${sid}: ${ids.join(', ')}`); + + await client.end(); + + console.log(`\n${errors === 0 ? 'PASS' : 'FAIL'} — ${errors} error(s)`); + process.exit(errors ? 1 : 0); +} + +main().catch((e) => { console.error('FATAL', e); process.exit(1); }); diff --git a/platform/api/migrations/024_phase6c_curriculum_completion.sql b/platform/api/migrations/024_phase6c_curriculum_completion.sql new file mode 100644 index 0000000..f74c8fb --- /dev/null +++ b/platform/api/migrations/024_phase6c_curriculum_completion.sql @@ -0,0 +1,225 @@ +-- Phase 6C — curriculum completion, assessment bank, ROOT 11 scope. Additive on top of Phase +-- 6B's 023_phase6b_curriculum.sql. No schema change was needed — is_required (Phase 6B) and +-- the existing assessment_definitions table already cover every genuine Phase 6C requirement. +-- See docs/ROOT_PILOT_READINESS_STANDARD.md for the bar these upgrades were checked against +-- and docs/PHASE6C_CURRICULUM_COMPLETION.md for which modules were upgraded and why. + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Upgrade 1: root-06-m03-jobs-cronjobs-daemonsets — lesson+KC+2labs -> full gold pattern. +-- Existing activities resequenced to make room; scenario_id/activity_type/completion_behavior +-- of every existing row is untouched, so no student's derived completion changes meaning. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE module_activities SET sequence = 3, version = version + 1, updated_at = now() WHERE activity_id = 'root-06-m03-a02-knowledge-check'; +UPDATE module_activities SET sequence = 5, version = version + 1, updated_at = now() WHERE activity_id = 'root-06-m03-a03-lab-job-task'; +UPDATE module_activities SET sequence = 6, version = version + 1, updated_at = now() WHERE activity_id = 'root-06-m03-a04-lab-cronjob-task'; +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-06-m03-a01b-demonstration-job-lifecycle', 'root-06-m03-jobs-cronjobs-daemonsets', 2, 'demonstration', 'Demonstration: Watching a Job Actually Complete', + 'content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/03-demonstration-job-lifecycle.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-06-m03-a02b-guided-exercise-choosing-primitive', 'root-06-m03-jobs-cronjobs-daemonsets', 4, 'guided_exercise', 'Guided Exercise: Choosing the Right Workload Primitive', + 'content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/04-guided-exercise-choosing-primitive.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-06-m03-a05-reflection', 'root-06-m03-jobs-cronjobs-daemonsets', 7, 'reflection', 'Reflection: The Right Primitive Is a Correctness Decision, Not a Style Choice', + 'content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/05-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-06-m03-jobs-cronjobs-daemonsets'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Upgrade 2: root-07-m04-ingress-networkpolicy-foundations — lesson+KC+2labs -> full gold pattern. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE module_activities SET sequence = 3, version = version + 1, updated_at = now() WHERE activity_id = 'root-07-m04-a02-knowledge-check'; +UPDATE module_activities SET sequence = 5, version = version + 1, updated_at = now() WHERE activity_id = 'root-07-m04-a03-lab-ingress-task'; +UPDATE module_activities SET sequence = 6, version = version + 1, updated_at = now() WHERE activity_id = 'root-07-m04-a04-lab-network-policy'; +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-07-m04-a01b-demonstration-ingress-routing', 'root-07-m04-ingress-networkpolicy-foundations', 2, 'demonstration', 'Demonstration: Tracing a Request Through Ingress', + 'content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/03-demonstration-ingress-routing.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-07-m04-a02b-guided-exercise-network-isolation', 'root-07-m04-ingress-networkpolicy-foundations', 4, 'guided_exercise', 'Guided Exercise: Planning Network Isolation', + 'content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/04-guided-exercise-network-isolation-plan.md', NULL, NULL, 'manual_marker', 12, 'ready'), +('root-07-m04-a05-reflection', 'root-07-m04-ingress-networkpolicy-foundations', 7, 'reflection', 'Reflection: Reachability Should Be a Decision, Not a Default', + 'content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/05-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-07-m04-ingress-networkpolicy-foundations'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Upgrade 3: root-07-m02-configuration-and-secrets — converted IN PLACE from a bare 5-lab bank +-- into a full teaching module (no new module created; this is the last networking/config +-- module with no front-door teaching, an honest gap Phase 6B's own doc flagged). The 5 +-- existing lab activities are untouched (id/scenario_id/type/completion_behavior) other than +-- their sequence number shifting to make room. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE module_activities SET sequence = sequence + 4, version = version + 1, updated_at = now() WHERE module_id = 'root-07-m02-configuration-and-secrets'; +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-07-m02-a00-lesson-configuration', 'root-07-m02-configuration-and-secrets', 1, 'lesson', 'Externalizing Configuration: ConfigMaps & Secrets', + 'content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/00-lesson-configuration.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-07-m02-a00b-demonstration-secret-not-encrypted', 'root-07-m02-configuration-and-secrets', 2, 'demonstration', 'Demonstration: A Secret Is Encoded, Not Encrypted', + 'content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/01-demonstration-secret-not-encrypted.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-07-m02-a00c-knowledge-check', 'root-07-m02-configuration-and-secrets', 3, 'knowledge_check', 'Knowledge Check: Configuration & Secrets', + 'content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/02-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-07-m02-a00d-guided-exercise-choosing-mechanism', 'root-07-m02-configuration-and-secrets', 4, 'guided_exercise', 'Guided Exercise: ConfigMap, Secret, Env Var, or Volume?', + 'content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/03-guided-exercise-choosing-mechanism.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-07-m02-a99-reflection', 'root-07-m02-configuration-and-secrets', 10, 'reflection', 'Reflection: Configuration Is Infrastructure, Secrets Are a Trust Boundary', + 'content/root/root-07-kubernetes-networking-config/m02-configuration-and-secrets/06-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready') +ON CONFLICT (activity_id) DO NOTHING; +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-07-m02-configuration-and-secrets'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Upgrade 4: root-09-m02-serviceaccounts-and-rbac — lesson+KC+6labs -> full gold pattern. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE module_activities SET sequence = 3, version = version + 1, updated_at = now() WHERE activity_id = 'root-09-m02-a02-knowledge-check'; +UPDATE module_activities SET sequence = sequence + 2, version = version + 1, updated_at = now() WHERE module_id = 'root-09-m02-serviceaccounts-and-rbac' AND activity_id IN + ('root-09-m02-a03-lab-serviceaccount-pod','root-09-m02-a04-lab-automount-token','root-09-m02-a05-lab-rbac-role','root-09-m02-a06-lab-rbac-clusterrole','root-09-m02-a07-lab-rbac-least-privilege','root-09-m02-a08-lab-rbac-clusterrole-independent'); +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-09-m02-a01b-demonstration-rbac-denial', 'root-09-m02-serviceaccounts-and-rbac', 2, 'demonstration', 'Demonstration: Watching RBAC Actually Deny Something', + 'content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/03-demonstration-rbac-denial.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-09-m02-a02b-guided-exercise-least-privilege-role', 'root-09-m02-serviceaccounts-and-rbac', 4, 'guided_exercise', 'Guided Exercise: Writing a Least-Privilege Role', + 'content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/04-guided-exercise-least-privilege-role.md', NULL, NULL, 'manual_marker', 12, 'ready'), +('root-09-m02-a09-reflection', 'root-09-m02-serviceaccounts-and-rbac', 11, 'reflection', 'Reflection: Permissions Should Reflect What a Workload Actually Needs', + 'content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/07-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready') +ON CONFLICT (activity_id) DO NOTHING; +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-09-m02-serviceaccounts-and-rbac'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Upgrade 5: root-09-m03-network-and-supplychain-security — lesson+KC+6labs -> full gold pattern. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE module_activities SET sequence = 3, version = version + 1, updated_at = now() WHERE activity_id = 'root-09-m03-a02-knowledge-check'; +UPDATE module_activities SET sequence = sequence + 2, version = version + 1, updated_at = now() WHERE module_id = 'root-09-m03-network-and-supplychain-security' AND activity_id IN + ('root-09-m03-a03-lab-default-deny','root-09-m03-a04-lab-egress-namespace','root-09-m03-a05-lab-egress-networkpolicy-guided','root-09-m03-a06-lab-tls-ingress','root-09-m03-a07-lab-image-pull-secret','root-09-m03-a08-lab-network-policy-metadata-advanced'); +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-09-m03-a01b-demonstration-default-deny', 'root-09-m03-network-and-supplychain-security', 2, 'demonstration', 'Demonstration: Default-Deny, Before and After', + 'content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/03-demonstration-default-deny.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-09-m03-a02b-guided-exercise-egress-plan', 'root-09-m03-network-and-supplychain-security', 4, 'guided_exercise', 'Guided Exercise: Planning Egress Restriction', + 'content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/04-guided-exercise-egress-plan.md', NULL, NULL, 'manual_marker', 12, 'ready'), +('root-09-m03-a09-reflection', 'root-09-m03-network-and-supplychain-security', 11, 'reflection', 'Reflection: Reachability and Trust Are Decisions You Make on Purpose', + 'content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/09-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-09-m03-network-and-supplychain-security'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Upgrade 6: root-10-m03-troubleshooting-framework — lesson+demo+KC+2labs -> full gold pattern +-- (only guided_exercise + reflection were missing). +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE module_activities SET sequence = 5, version = version + 1, updated_at = now() WHERE activity_id = 'root-10-m03-a04-lab-crashloop-fix'; +UPDATE module_activities SET sequence = 6, version = version + 1, updated_at = now() WHERE activity_id = 'root-10-m03-a05-lab-endpoint-fix'; +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-10-m03-a03b-guided-exercise-endpoint-mismatch', 'root-10-m03-troubleshooting-framework', 4, 'guided_exercise', 'Guided Exercise: Applying the Framework to a Broken Service, on Paper First', + 'content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/04-guided-exercise-endpoint-mismatch.md', NULL, NULL, 'manual_marker', 12, 'ready'), +('root-10-m03-a06-reflection', 'root-10-m03-troubleshooting-framework', 7, 'reflection', 'Reflection: A Framework You''ve Actually Used Is One You''ll Trust Under Pressure', + 'content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/06-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-10-m03-troubleshooting-framework'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Upgrade 7: root-05-m01-pods-and-kubectl-basics — a 20-task kubectl-fluency drill bank with no +-- bookending at all. Each individual task already carries its own explanation (per-scenario +-- description/hints), so the real gap was framing, not per-command teaching: a short lesson +-- explaining WHY this module is 20 independent tasks rather than one narrative, plus a +-- reflection that makes students name their own remaining gaps. All 20 existing lab/MCQ +-- activities are untouched. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-05-m01-a00-lesson-kubectl-fluency', 'root-05-m01-pods-and-kubectl-basics', 0, 'lesson', 'Building kubectl Fluency: Why Twenty Small Reps Beat One Big Lecture', + 'content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/00-lesson-kubectl-fluency.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-05-m01-a21-reflection', 'root-05-m01-pods-and-kubectl-basics', 21, 'reflection', 'Reflection: Naming Your Gaps While They''re Still Cheap to Close', + 'content/root/root-05-kubernetes-foundations/m01-pods-and-kubectl-basics/21-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready') +ON CONFLICT (activity_id) DO NOTHING; +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-05-m01-pods-and-kubectl-basics'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- root-09-m04-cluster-hardening-and-runtime — READY via documented exception. Phase 6B already +-- documented this module as deliberately survey-depth ("not core, not daily-driver"). It now +-- has a lesson, two required hands-on labs, two optional deeper labs, and a real timed +-- assessment checkpoint (this migration's security-assessment wiring above) — sufficient for +-- its intentionally lighter scope without forcing a demonstration/guided-exercise pattern that +-- would misrepresent it as core daily-driver content. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-09-m04-cluster-hardening-and-runtime'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- root-capstone-m01-operate-and-recover — READY via documented exception, not new content. Its +-- single 'capstone' activity IS the terminal exam this module exists to deliver; the readiness +-- teaching/prep work already lives in the preceding root-capstone-m02-capstone-readiness module +-- (already READY). Adding a lesson/reflection here would duplicate that module's job. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-capstone-m01-operate-and-recover'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- root-12-m01-real-ops-incidents — READY via documented exception, not new content. Its +-- lesson + 4 deliberately-unguided incidents + reflection IS the intentional shape for this +-- module (see docs/ROOT_PILOT_READINESS_STANDARD.md's flexibility principle and +-- docs/PHASE6C_CURRICULUM_COMPLETION.md for the full reasoning) — a knowledge check or guided +-- exercise here would undermine its explicit "you are responsible for this environment, not +-- handed a hint system" design. No activities added; status promoted to reflect the review. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE course_modules SET status = 'ready', version = version + 1, updated_at = now() WHERE module_id = 'root-12-m01-real-ops-incidents'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Section 9 — Kubernetes security assessment bank. Wires the 7 previously-unwired CKS MCQs +-- (docs/PHASE6B_ROOT_CURRICULUM_EXPANSION.md's ASSESSMENT-BANK classification) into a real +-- assessment_definition using the EXISTING Phase 3B assessment engine — no new mechanism, no +-- duplicated scenario content. Required scenarios are hands-on CORE labs already in ROOT 09; +-- random pools mix additional hands-on practice with the recall-style MCQs. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +INSERT INTO assessment_definitions (assessment_definition_id, type, title, description, duration_minutes, hint_policy, pool_spec, completion_rule) +VALUES ( + 'rootnode-security-assessment', 'exam', 'RootNode Kubernetes Security Assessment', + 'A timed skills check across Kubernetes security fundamentals: SecurityContext hardening, least-privilege RBAC, NetworkPolicy as a security control, and secrets handling — combining hands-on tasks with conceptual reasoning questions. Example exam definition, following the same pattern as rootnode-quick-assessment.', + 60, 'disabled', + '{"required": ["cks-pod-security-context","cks-rbac-least-privilege","cks-network-policy"], + "random": [ + {"count":2,"from":["cks-non-root-enforce","cks-drop-capabilities","cks-readonly-filesystem","cks-seccomp-profile"]}, + {"count":1,"from":["serviceaccount-pod","cks-automount-token","rbac-role","cks-rbac-clusterrole"]}, + {"count":1,"from":["cks-egress-namespace","networkpolicy-egress","cks-tls-ingress","cks-image-pull-secret"]}, + {"count":2,"from":["cks-mcq-api-server","cks-mcq-audit-policy","cks-mcq-kubelet-auth","cks-mcq-psp-replacement","cks-mcq-runtime-security","cks-mcq-sandboxing","cks-mcq-image-footprint"]} + ]}', + '{"type":"all_scenarios_passed"}' +) ON CONFLICT (assessment_definition_id) DO NOTHING; + +-- Given real curriculum visibility as an 'assessment' activity at the end of ROOT 09's +-- advanced/survey module — the natural home for a security checkpoint, not a new module. +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-09-m04-a06-assessment-security', 'root-09-m04-cluster-hardening-and-runtime', 6, 'assessment', 'RootNode Kubernetes Security Assessment', NULL, NULL, 'rootnode-security-assessment', 'auto_assessment', 60, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Section 11/12 — ROOT 11 gets an honest, lesson-only orientation module. No fake labs: the +-- current Lab platform provides Kubernetes/k3s only, not a Git+CI runner, ArgoCD, Terraform, or +-- Prometheus/Grafana environment (docs/ROOT11_DEVOPS_PLATFORM_SCOPE.md documents exactly what +-- would be needed for those). ROOT 11 moves from PLANNED to PARTIAL — real content exists, no +-- longer an unexplained empty placeholder — while staying honest that no hands-on lab exists +-- for this course yet. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE courses SET status = 'partial', updated_at = now() WHERE course_id = 'root-11-devops-platform-engineering'; + +-- prerequisites is left empty at the MODULE level (course_modules.prerequisites references +-- other MODULE ids, not course ids) — the course-level gate (courses.prerequisites, already +-- 'root-10-kubernetes-troubleshooting-observability' since Phase 6A's 022_course_content.sql) +-- already requires ROOT 10 complete before a student can enter ROOT 11 at all. Found by +-- infra/scripts/validate-course-content.js — an earlier draft of this migration incorrectly +-- put a course id in a module-prerequisites array. +INSERT INTO course_modules (module_id, course_id, title, description, sequence, objectives, prerequisites, estimated_duration_minutes, status) VALUES +('root-11-m01-devops-platform-thinking', 'root-11-devops-platform-engineering', 'From Sysadmin to Platform Thinking', + 'DevOps as an operating model, platform engineering, and where Kubernetes fits in the larger platform ecosystem — lesson-only orientation; no hands-on Lab exists for this course yet (see docs/ROOT11_DEVOPS_PLATFORM_SCOPE.md).', + 1, '{"Explain DevOps as an operating model rather than a job title","Explain what platform engineering adds on top of DevOps practice","Describe how declarative infrastructure, automation, and feedback loops reduce cognitive load for engineers"}', + '{}', 30, 'partial') +ON CONFLICT (module_id) DO NOTHING; + +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-11-m01-a01-lesson-devops-platform-thinking', 'root-11-m01-devops-platform-thinking', 1, 'lesson', 'DevOps as an Operating Model, Not a Job Title', + 'content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/01-lesson-devops-platform-thinking.md', NULL, NULL, 'manual_marker', 20, 'ready'), +('root-11-m01-a02-knowledge-check', 'root-11-m01-devops-platform-thinking', 2, 'knowledge_check', 'Knowledge Check: DevOps & Platform Thinking', + 'content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/02-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-11-m01-a03-reflection', 'root-11-m01-devops-platform-thinking', 3, 'reflection', 'Reflection: Where You Already Practiced This', + 'content/root/root-11-devops-platform-engineering/m01-devops-platform-thinking/03-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- Section 13/14 — readiness-probe-task was an unwired scenario (a constructive "author a +-- readiness probe" Lab) that doesn't duplicate root-10-m01's existing +-- rootnode-probe-port-mismatch Lab (a diagnostic "find the misconfigured probe" task). Wired in +-- as OPTIONAL practice using the existing is_required model (Phase 6B) rather than a second +-- practice system — students who want more probe-authoring reps can do it without it blocking +-- required progression. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +UPDATE module_activities SET sequence = 7, version = version + 1, updated_at = now() WHERE activity_id = 'root-10-m01-a06-reflection-real-ops-takeaway'; +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, is_required, estimated_duration_minutes, status) VALUES +('root-10-m01-a05b-lab-readiness-probe-optional', 'root-10-m01-health-probes-and-troubleshooting', 6, 'lab', 'Optional Practice: Configure a Readiness Probe', + NULL, 'readiness-probe-task', NULL, 'auto_scenario', false, 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING;