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:
+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.
|
||||
Reference in New Issue
Block a user