RBAC, ServiceAccounts and Pod Security Admission
Who can do what, which identity workloads use, and why PodSecurityPolicy tutorials will waste your time — it was removed in v1.25.
Two Different Questions
Kubernetes security splits into two questions that get confused constantly:
- Who can call the API, and what can they do? — RBAC.
- What is a running pod allowed to do on its node? — Pod Security Admission and
securityContext.
RBAC won't stop a container running as root. Pod Security Admission won't stop someone deleting your Deployments. You need both.
RBAC: Four Objects
A Role lists permitted verbs on resources within one namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]A RoleBinding grants it to a subject:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: production
subjects:
- kind: ServiceAccount
name: monitoring
namespace: production
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.ioRBAC is purely additive — there are no deny rules. A subject can do the union of everything granted to it, and nothing else. Access is denied by default.
The namespaced/cluster-scoped distinction
This is where permissions accidentally become enormous:
| Object | Scope |
|---|---|
| Role + RoleBinding | One namespace |
| ClusterRole + ClusterRoleBinding | The entire cluster |
| ClusterRole + RoleBinding | The ClusterRole's rules, limited to that binding's namespace |
That third combination is genuinely useful — define a reusable ClusterRole once, then bind it per namespace.
A ClusterRoleBinding applies across every namespace. Binding a ClusterRole with get secrets to a ServiceAccount via ClusterRoleBinding grants it every Secret in the cluster — including other teams' database credentials and cloud keys. If you meant "this namespace", use a RoleBinding. This substitution is one of the most common real-world privilege-escalation paths.
ServiceAccounts
Every pod runs as a ServiceAccount — the default one in its namespace if you don't specify. That's the identity it presents to the API server.
apiVersion: v1
kind: ServiceAccount
metadata:
name: api
namespace: production
automountServiceAccountToken: false # don't mount unless needed
---
spec:
template:
spec:
serviceAccountName: apiBy default a token is mounted into every pod at /var/run/secrets/kubernetes.io/serviceaccount/. Most applications never call the Kubernetes API and don't need it — and a mounted token is something an attacker who compromises your container can use. Set automountServiceAccountToken: false unless the workload genuinely talks to the API.
Modern tokens are projected: short-lived, audience-bound, and automatically rotated, rather than permanent Secrets. If you find guidance about long-lived ServiceAccount token Secrets, it predates this change.
Check what a ServiceAccount can actually do:
kubectl auth can-i --list --as=system:serviceaccount:production:api
kubectl auth can-i delete deployments --as=system:serviceaccount:production:api -n productionkubectl auth can-i is the fastest way to verify RBAC without deploying anything — and worth running on any ServiceAccount you inherit.
Check yourself
A ClusterRole granting get/list on secrets is bound to a ServiceAccount using a ClusterRoleBinding, intending to let a monitoring agent read secrets in its own namespace. What was actually granted?
Pod Security: Don't Follow PSP Tutorials
PodSecurityPolicy was deprecated in v1.21 and removed entirely in v1.25. It no longer exists in the API. Tutorials that tell you to write a PSP — and there are many — are describing a mechanism your cluster does not have.
Its replacement is Pod Security Admission, built into the API server. Instead of writing policy objects, you label a namespace with one of three standards:
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restrictedThe three Pod Security Standards:
| Standard | Meaning |
|---|---|
| privileged | Unrestricted — anything goes |
| baseline | Blocks known privilege escalations; minimally restrictive |
| restricted | Heavily restricted, following current hardening best practice |
And three modes, which can be combined:
enforce— reject non-compliant pods.warn— allow, but return a warning to the user.audit— allow, but record it in the audit log.
The adoption path this enables is the reason to like PSA. Label namespaces with warn and audit at restricted first, leaving enforce unset. Nothing breaks, and you collect a complete list of what would fail. Fix those workloads, then switch on enforce. Going straight to enforcement on a live namespace breaks deployments immediately.
securityContext: Satisfying restricted
The restricted standard requires specific securityContext settings:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: api
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}Each field does real work:
runAsNonRoot— refuse to start if the image would run as UID 0.allowPrivilegeEscalation: false— block gaining privileges via setuid binaries.readOnlyRootFilesystem: true— the container can't write to its own filesystem, so add a writableemptyDirwherever it genuinely needs one.capabilities.drop: ["ALL"]— remove all Linux capabilities. Add back only what's needed (NET_BIND_SERVICEfor ports below 1024).seccompProfile: RuntimeDefault— restrict available syscalls.
These are the Kubernetes expression of the Docker hardening guide's least-privilege settings. Building images that run as non-root with a numeric USER makes satisfying restricted almost automatic — the two roadmaps meet here.
Beyond PSA: Policy Engines
Pod Security Admission covers the built-in standards only. For organisation-specific rules — required labels, permitted registries, mandatory resource limits — use a policy engine such as Kyverno or OPA Gatekeeper. Both run as admission webhooks and can validate, mutate, or generate resources.
A common pairing: PSA for the baseline pod-hardening standards, a policy engine for everything specific to you.
An Audit Checklist
# Who has cluster-admin?
kubectl get clusterrolebindings -o json | \
jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name'
# What can a given ServiceAccount do?
kubectl auth can-i --list --as=system:serviceaccount:production:api
# Which namespaces have no Pod Security labels?
kubectl get ns -o json | \
jq -r '.items[] | select(.metadata.labels["pod-security.kubernetes.io/enforce"]==null) | .metadata.name'
# Which pods mount a ServiceAccount token unnecessarily?
kubectl get pods -A -o json | \
jq -r '.items[] | select(.spec.automountServiceAccountToken != false) | "\(.metadata.namespace)/\(.metadata.name)"'Check yourself
A team finds a tutorial explaining how to write a PodSecurityPolicy to enforce non-root containers. They're on Kubernetes 1.37. What should they do?
What's Next
The cluster is secured. The next guide covers managing manifests at scale — Helm and Kustomize, and the render-before-apply habit that makes any generated configuration safe to trust.