$ skip to content
devresurge

04 — Security primitives: least privilege at the kernel

ELI5 hook

Even inside a frosted-glass room with a snack budget, you shouldn’t get the master keys to the whole building. Linux splits “root powers” into capabilities, filters scary system calls with seccomp, and can stamp processes with mandatory labels (AppArmor / SELinux). Kubernetes turns these knobs per Pod.

Mental model

Defense in depth for a container:

flowchart TB
  ns[Namespaces: what you see]
  cg[cgroups: what you spend]
  cap[Capabilities: which superpowers]
  sec[seccomp: which syscalls]
  lsm[LSM: mandatory policy]
  ns --> cap --> sec --> lsm
  cg --> cap
LayerQuestion it answers
User / UIDWho are you numerically?
CapabilitiesWhich privileged ops remain?
seccompWhich syscalls are allowed?
LSMDoes policy allow this label/path/socket?
Read-only rootfs / dropsCan you rewrite your own image?

What Linux does

Capabilities

Historically: UID 0 could do everything. Now many privileges are bits: CAP_NET_ADMIN, CAP_SYS_ADMIN, CAP_NET_RAW, …

# Capabilities of current shell (often none special)
grep Cap /proc/self/status
# Decode with:
capsh --decode=$(grep CapEff /proc/self/status | awk '{print $2}')

Dropping capabilities is how “root in a container” becomes less catastrophic — still not harmless.

seccomp

A syscall filter. Profiles blacklist or allowlist calls like reboot, mount, ptrace. Docker/containerd apply a default profile; Kubernetes can set seccompProfile.

LSM (AppArmor / SELinux)

Mandatory Access Control: even root-with-capabilities can be denied. Nodes usually run one major LSM style. Pods can request profiles (appArmorProfile, SELinux options).

User namespaces

Map container UID 0 → unprivileged host UID. Strong isolation win; not every cluster enables it everywhere yet (growing support via Kubernetes user namespace features).

How Kubernetes uses it

Pod Security surfaces (modern clusters: Pod Security Admission / PSS levels: Privileged, Baseline, Restricted):

KnobSpec field (examples)
Run as non-rootsecurityContext.runAsNonRoot
Drop capssecurityContext.capabilities.drop: ["ALL"]
Add capscapabilities.add (avoid unless needed)
seccompseccompProfile.type: RuntimeDefault
Privilegedprivileged: true — nearly host-equivalent; treat as root on node
Hostpath / hostNetwork / hostPIDEscape hatches that weaken isolation
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop: ["ALL"]
  seccompProfile:
    type: RuntimeDefault

Admission controllers and policies (OPA/Gatekeeper, Kyverno, PSS) exist because defaults are for convenience, not for your threat model.

Lab — inspect what you actually have

# Inside any container you can start:
docker run --rm alpine sh -c 'id; grep CapEff /proc/self/status; ls /proc/1'

# Privileged vs not (feel the difference carefully on a disposable VM)
docker run --rm alpine grep CapEff /proc/self/status
docker run --rm --privileged alpine grep CapEff /proc/self/status

On a kind cluster (if available):

kubectl run cap-demo --image=alpine --restart=Never -- sleep 3600
kubectl exec cap-demo -- grep Cap /proc/1/status
kubectl delete pod cap-demo

Gotchas

  • Privileged containers — Close to “be the node.” Needed rarely (e.g. some CSI/node agents); never for apps by default.
  • CAP_SYS_ADMIN — The junk drawer of power; almost as bad as privileged for escape potential.
  • Root + writable hostPath — Classic breakout pattern.
  • “We dropped ALL capabilities” — Great, but still combine with non-root, read-only rootfs, seccomp, and no host namespaces.
  • PSS Restricted — May break naive images that expect to write to / or bind low ports.

Checkpoint

You now have the Linux trifecta: see (namespaces), spend (cgroups), act (capabilities/seccomp/LSM).

Next: 05 — OCI images — packing a root filesystem into a suitcase.


Quiz: /quizzes/k8s-linux-foundations/ — take it before moving on.