copy pass: straighten quotes and arrows, thin the dash asides, drop the app-count caption from the constellation
build-and-deploy / build (push) Failing after 13m18s

This commit is contained in:
2026-08-17 23:12:42 +10:00
parent 78e8740408
commit 3cfd59a464
23 changed files with 257 additions and 271 deletions
+10 -11
View File
@@ -1,7 +1,7 @@
---
title: "Every pod holds a key to a door it never opens"
date: 2026-06-23
summary: "Least privilege for Kubernetes workloads doesn't start with an RBAC role it starts with revoking the API token every pod silently carries, then layering identity, non-root, and Pod Security on top."
summary: "Least privilege for Kubernetes workloads doesn't start with an RBAC role - it starts with revoking the API token every pod silently carries, then layering identity, non-root, and Pod Security on top."
tags: ["kubernetes", "security", "least-privilege", "rbac", "service-accounts"]
draft: false
hero: "/blog/workload-least-privilege.webp"
@@ -10,23 +10,23 @@ heroAlt: "Rows of glowing cube-pods each clutch a keycard, facing a sealed vault
---
Open a shell in almost any Kubernetes pod and look in `/var/run/secrets/kubernetes.io`. There's a token
there a live credential for the cluster API, mounted automatically, signed and ready. Now ask the harder
there - a live credential for the cluster API, mounted automatically, signed and ready. Now ask the harder
question: does this workload ever actually *call* the Kubernetes API? For a media server, a credential vault,
a DNS sinkhole, a search proxy the answer is no. Never. Not once.
a DNS sinkhole, a search proxy - the answer is no. Never. Not once.
So every one of those pods is holding a key to a door it never opens. That's not least privilege. That's a
standing credential waiting to be stolen.
![Workload least privilege each pod gets its own ServiceAccount with no API token, runs non-root with capabilities dropped, inside a namespace whose Pod Security level is pinned; the cluster API it never calls stays out of reach](/diagrams/workload-least-privilege.svg?v=2)
![Workload least privilege - each pod gets its own ServiceAccount with no API token, runs non-root with capabilities dropped, inside a namespace whose Pod Security level is pinned; the cluster API it never calls stays out of reach](/diagrams/workload-least-privilege.svg?v=2)
## Identity first, RBAC second
The reflex when someone says "least-privilege RBAC" is to start writing Roles. But a Role narrows what a
credential *can do* it does nothing about a credential that shouldn't exist. The first move is cheaper and
credential *can do* - it does nothing about a credential that shouldn't exist. The first move is cheaper and
safer: give every workload its **own** ServiceAccount instead of the shared namespace default, and set
`automountServiceAccountToken: false`.
That single line changes no runtime behaviour no capabilities, no user, no restart semantics because
That single line changes no runtime behaviour - no capabilities, no user, no restart semantics - because
the app wasn't using the token anyway. You've simply stopped handing out a key. The handful of components
that genuinely talk to the API (the GitOps controller, the load-balancer, the secrets operator) get a
scoped Role and keep their token. Everything else gets an identity and nothing else. It's the highest
@@ -39,15 +39,15 @@ full Linux capability set is one escape away from the host, regardless of how fe
So the next layer is the container itself: run as a non-root UID, drop **all** capabilities and add back only
the few a workload truly needs, forbid privilege escalation, and pin a `RuntimeDefault` seccomp profile.
This is where honesty matters. Some images anything running several processes under an init system, or the
classic "start as root, drop to a user" pattern break the moment you drop capabilities, because something
This is where honesty matters. Some images - anything running several processes under an init system, or the
classic "start as root, drop to a user" pattern - break the moment you drop capabilities, because something
inside still expects to `chown` a directory or bind a port. The lesson isn't "give up"; it's that non-root is
a *per-app, verified* change, while own-identity-and-no-token is a *universal, safe* one. Do the free thing
everywhere; do the careful thing where you've tested it.
## Make the floor non-negotiable
Authoring all of this per workload is good, but authoring isn't enforcing a future manifest can forget. The
Authoring all of this per workload is good, but authoring isn't enforcing - a future manifest can forget. The
enforcement floor is **Pod Security Admission**, set at the namespace. Pin each namespace's level explicitly
rather than inheriting a cluster default you can't see: `baseline` where root init-containers still live,
`restricted` for the apps that have earned it, and a documented `privileged` exception for the genuine cases
@@ -56,8 +56,7 @@ restricted profile, and every apply tells you exactly which workload is one refa
## The principle
Least privilege isn't a pile of Roles. It's a direction: start by removing the access that nothing is using
the auto-mounted token first — then narrow what genuinely remains, then make the boundary enforce itself. Give
Least privilege isn't a pile of Roles. It's a direction: start by removing the access that nothing is using - the auto-mounted token first - then narrow what genuinely remains, then make the boundary enforce itself. Give
every workload its own identity, hand it no credential it doesn't need, strip it to the smallest process it can
be, and let the namespace refuse anything bigger. The credential you never mount can't leak, can't be replayed,
and can't be the thing you forgot to revoke.