Skip to content

Identity, RBAC & Network Policy

Scanning tries to keep vulnerable code out. This page assumes some will get in anyway — a dependency you couldn’t patch in time, a zero-day, a stolen credential — and asks the next question: when something is compromised, how much can it actually do? The answer is set long before the breach, by how much privilege you handed out. This is the discipline of least privilege, applied to three different actors: the humans operating the system, the workloads running in it, and the network traffic between them.

The governing principle is one sentence: grant every actor the minimum it needs and nothing more. A component that can only do its one job is a contained blast radius; a component that can do anything is a master key waiting to be stolen. Most real-world breaches are not one clever exploit — they’re one small foothold that could reach everything because nothing was locked down between the foothold and the crown jewels.

First, two words people confuse: authn vs authz

Section titled “First, two words people confuse: authn vs authz”

Every access decision has two distinct steps, and conflating them is a classic source of holes.

  • Authentication (authn)who are you? Proving identity: a password, a token, a certificate, an OIDC login. The output is a verified identity.
  • Authorization (authz)what are you allowed to do? Given a verified identity, deciding whether this specific action is permitted.
request ──► AUTHN ──► "you are svc-payments" ──► AUTHZ ──► "may you delete pods?" ──► allow / deny
who? (identity established) what? (permission checked)

Authn without authz is a building where the front door checks your ID but every interior door is unlocked. Least privilege lives almost entirely in the authz step — the identity is just the input.

RBAC (Role-Based Access Control) is how Kubernetes answers the authz question. It’s built from four objects that compose cleanly:

  • A Role (namespace-scoped) or ClusterRole (cluster-wide) is a set of permissions — verbs (get, list, create, delete) on resources (pods, secrets, deployments).
  • A RoleBinding / ClusterRoleBinding grants a Role to a subject — a user, a group, or a ServiceAccount.
# A Role that can only READ pods in the "web" namespace — nothing else.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: web
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"] # note: no create/delete, no secrets
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: web-app-can-read-pods
namespace: web
subjects:
- kind: ServiceAccount
name: web-app
namespace: web
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io

This is least privilege made literal: the web-app ServiceAccount can read pods in its own namespace and do nothing else. If that pod is compromised, the attacker inherits exactly those permissions — they cannot read Secrets, cannot delete workloads, cannot touch other namespaces. Compare that to the depressingly common anti-pattern of binding cluster-admin to a ServiceAccount “to make it work” — now one compromised pod owns the cluster.

RBAC needs an identity to authorize, which raises the authn question for machines: how does a workload prove who it is — to the Kubernetes API, and to external systems like a cloud database or a secret store? The naive answer is “give the pod a long-lived API key,” which is just another static secret to leak.

Workload identity is the better pattern: the platform issues each workload a short-lived, verifiable identity (a ServiceAccount token, or a federated cloud identity) that external systems trust directly, with no stored credential. The pod proves “I am svc-payments in namespace prod” and the cloud grants access based on that, rather than on a secret the pod is carrying. It’s the same philosophy as dynamic secrets — an identity that’s short-lived and verifiable beats a credential that’s long-lived and stealable.

NetworkPolicy: from flat network to default-deny

Section titled “NetworkPolicy: from flat network to default-deny”

RBAC controls the API. But pods also talk to each other over the network — east-west traffic — and by default a Kubernetes network is flat: every pod can reach every other pod on every port. That means a single compromised frontend pod can probe the database, the metrics endpoint, the admin service, the entire cluster. The foothold becomes the whole house.

FLAT (default) DEFAULT-DENY (NetworkPolicy)
────────────── ────────────────────────────
frontend ─┬─► backend frontend ───► backend ───► db
├─► db (!) (frontend CANNOT reach db directly)
├─► admin (!) backend ───► db (allowed, explicit)
└─► metrics (!) everything else ──► DENIED
one foothold can reach everything blast radius = one explicit edge

A NetworkPolicy is a Kubernetes object that restricts which pods may talk to which, by label. The key move is to start with a default-deny policy — nothing may talk to a pod — and then add explicit allow rules for the connections that should exist. The network flips from “allow by default, the attacker’s playground” to “deny by default, you must justify every edge.”

# 1) Default-deny ALL ingress to every pod in the namespace.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: prod
spec:
podSelector: {} # selects every pod in the namespace
policyTypes: ["Ingress"]
# no ingress rules => nothing is allowed in
---
# 2) Now explicitly allow ONLY backend pods to reach the db on 5432.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-db
namespace: prod
spec:
podSelector:
matchLabels: { app: db }
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels: { app: backend }
ports:
- protocol: TCP
port: 5432

Now a compromised frontend cannot open a connection to the database at all — the policy drops the packet. The blast radius of that foothold shrinks from “the whole cluster” to “the handful of edges you explicitly allowed.”

Put these together and a philosophy emerges: zero-trust — the network location of a request grants it nothing. Being “inside the cluster” is not a permission. Every call must carry a verified identity and be explicitly authorized, every time. RBAC, workload identity, and default-deny NetworkPolicy are the first three pillars of that model.

The piece still missing is encrypting and authenticating the traffic itself between services, so that even a position on the network buys an attacker nothing — they can’t read or spoof a connection they don’t have an identity for. That’s mutual TLS (mTLS), and a service mesh automates it across every service. We foreshadow it deliberately here because it’s the natural completion of this page, and it gets the full treatment later in Service Meshes & mTLS.

Under the hood — the token mounted in every pod

Section titled “Under the hood — the token mounted in every pod”

The “ServiceAccount token” the caution box warns about isn’t an abstraction — it’s a real file sitting inside almost every running pod, by default at /var/run/secrets/kubernetes.io/serviceaccount/token. It’s a JWT the pod presents to the Kubernetes API to prove “I am this ServiceAccount,” and the API server validates it on every call. That convenience is also the risk: if the pod is compromised, so is that token, and it carries exactly whatever RBAC you bound to the ServiceAccount.

Modern clusters made the token much safer than it used to be. Where early Kubernetes mounted a long-lived token stored in a Secret, current versions issue projected, time-bound, audience-scoped tokens through the TokenRequest API — the token auto-rotates, expires, and is only valid for a specific audience, so a stolen copy is useful for a short window rather than forever. This is workload identity in miniature, and it’s why two settings matter so much: scope the ServiceAccount’s RBAC tightly, and set automountServiceAccountToken: false on pods that never call the API so there’s no token to steal at all.

The manual, error-prone step this page removes is trusting by default — handing out broad permissions and flat networks because scoping each one by hand is tedious, then hoping nothing inside ever turns hostile. RBAC, workload identity, and NetworkPolicy replace that hope with declared, reviewable, version-controlled grants: every permission and every network edge is a manifest in git, readable in a diff, enforced by the platform.

Production gets safer not by preventing every breach — you can’t — but by containing the ones that happen. A compromised pod with a tightly-scoped ServiceAccount and a default-deny network is a dead end; the same pod with cluster-admin and a flat network is game over. The cost is the discipline of writing and maintaining those grants instead of taking the open-by-default shortcut — and that discipline is exactly what the last page of this Part automates and enforces: Compliance as Code.

Five questions for least privilege across humans, workloads, and traffic:

  • Why does it exist? Because some breach will get through — an unpatched dep, a zero-day, a stolen credential — so the real question is how much can a compromised actor do?, decided by how much privilege you handed out beforehand.
  • What problem does it solve? Trusting by default: RBAC scopes what each identity may do, workload identity replaces stealable long-lived keys with short-lived verifiable ones, and default-deny NetworkPolicy turns a flat network (one foothold reaches everything) into a handful of explicit allowed edges.
  • What are the trade-offs? You take on the discipline of writing and maintaining every grant and network edge as reviewed manifests; NetworkPolicy is inert unless your CNI (Calico, Cilium) enforces it; and the default ServiceAccount token is a master key the moment it’s bound to cluster-admin.
  • When should I avoid it? You don’t avoid least privilege — but a single-tenant throwaway cluster may not justify per-edge NetworkPolicy; the payoff scales with the blast-radius stakes.
  • What breaks if I remove it? A compromised pod with cluster-admin and a flat network is a cluster takeover instead of a dead end — containment evaporates.
  1. Distinguish authentication from authorization with a concrete example. Why is “authn without authz” described as a building with a guarded front door but unlocked interior doors?
  2. Walk through the four RBAC objects and how they compose. If a pod using the pod-reader Role is compromised, what can and can’t the attacker do — and why?
  3. What problem does workload identity solve that a long-lived API key handed to a pod does not? Connect it to the dynamic-secrets idea.
  4. By default a Kubernetes network is flat. Explain why that turns one compromised pod into a cluster-wide problem, and how a default-deny NetworkPolicy changes the blast radius.
  5. Define zero-trust in one sentence, and using the book’s thread, name the manual step this page removes and the safety it buys even when a breach still happens.
Show answers
  1. Authn proves who you are (e.g. an OIDC login establishing “you are svc-payments”); authz decides what that identity may do (e.g. “may it delete pods?”). Authn without authz is a guarded front door with unlocked interior doors because you’ve verified identity but checked no permissions — anyone who gets in can do anything.
  2. A Role/ClusterRole is a set of permissions (verbs on resources); a RoleBinding/ClusterRoleBinding grants it to a subject (user, group, or ServiceAccount). A compromised pod-reader pod inherits exactly those permissions — read pods in its own namespace — and cannot read Secrets, delete workloads, or touch other namespaces, because nothing granted it those verbs/resources.
  3. A long-lived API key in a pod is just another static secret that can leak and stays valid until noticed. Workload identity gives the pod a short-lived, verifiable identity that external systems trust directly, with no stored credential — same philosophy as dynamic secrets: short-lived and verifiable beats long-lived and stealable.
  4. Flat means every pod can reach every other pod on every port, so one compromised pod can probe the database, admin, and metrics services — the whole cluster. A default-deny NetworkPolicy drops all traffic unless an explicit allow rule permits it, so the foothold can only reach the few edges you declared; blast radius shrinks from “everything” to “the explicit edges.”
  5. Zero-trust: network location grants no permission — every request must carry a verified identity and be explicitly authorized, every time. The manual step removed is trusting by default (broad grants and flat networks out of convenience); the safety bought is containment — a breach with tightly scoped RBAC and default-deny networking is a dead end rather than a cluster takeover.