---
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."
tags: ["kubernetes", "security", "least-privilege", "rbac", "service-accounts"]
draft: false
hero: "/blog/workload-least-privilege.webp"
heroAlt: "Rows of glowing cube-pods each clutch a keycard, facing a sealed vault door none of them open; one keycard dissolves into particles, revoked."

---

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
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.

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)

## 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
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
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
security-per-effort change in the whole estate, and it's effectively free.

## Then take away what root gave you

Identity is one axis; what the process can do on the node is another. A container that runs as root with the
full Linux capability set is one escape away from the host, regardless of how few API rights its token has.
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
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
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
(a VPN sidecar that needs `NET_ADMIN`, a time daemon that needs `SYS_TIME`). Run the rest at `warn` for the
restricted profile, and every apply tells you exactly which workload is one refactor away from the top tier.

## Least privilege is a direction, not a pile of Roles

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.
