blog: 'The most secure credential is the one you never mount'
Least-privilege for Kubernetes workloads — own ServiceAccount + no auto-mounted API token, then non-root + dropped caps, then Pod Security as the enforced floor. Adds a matching house-style SVG diagram (workload-least-privilege).
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: "The most secure credential is the one you never mount"
|
||||
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
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||

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