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