RBAC, service accounts and pod security

Who may do what to the API, what identity a pod carries, and how to stop containers running as root with a full set of capabilities.

RBAC: roles, bindings and verbs

Authorization is a decision the API server makes on every request: does this identity have this verb on this resource in this scope? RBAC answers it with roles (what is allowed) and bindings (who gets it).

KindScopeGrants
RoleOne namespaceVerbs on resources inside that namespace
ClusterRoleCluster-wideVerbs on cluster-scoped resources, or the same rules in every namespace
RoleBindingOne namespaceA Role or a ClusterRole to a subject, in that namespace
ClusterRoleBindingCluster-wideA ClusterRole to a subject across the whole cluster
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: storefront
rules:
  - apiGroups: [""]                 # "" means the core API group
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-reader
  namespace: storefront
subjects:
  - kind: ServiceAccount
    name: ci
    namespace: storefront
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
kubectl auth can-i get pods -n storefront --as=system:serviceaccount:storefront:ci
kubectl auth can-i --list -n storefront --as=system:serviceaccount:storefront:ci
kubectl create role deployer --verb=get,list,watch,update --resource=deployments -n storefront
kubectl describe clusterrole cluster-admin     # note the wildcards
  • Subjects are users, groups, or service accounts. There is no User object: users come from your identity provider or client certificate.
  • cluster-admin is bound to system:masters. Nothing should ever run with it.
  • roleRef in a binding is immutable. Changing the role means creating a new binding.
  • Avoid wildcards in resources and verbs. They silently grant access to resources that do not exist yet, including future custom resources.
  • Verb create on pods is effectively node-level code execution. Treat it as privileged, because it is.

Service accounts and token projection

A service account is the identity a pod uses when it talks to the API server. Every namespace has a default one, and a pod that does not name one gets it — with whatever the namespace grants to default, which should be nothing.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: api
  namespace: storefront
automountServiceAccountToken: false    # opt in per pod, not by default
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    spec:
      serviceAccountName: api
      automountServiceAccountToken: false   # this pod never calls the API
      containers:
        - name: api
          image: ghcr.io/example/api:2.1.0
  • Since Kubernetes 1.24 there is no long-lived token Secret by default: the kubelet requests a short-lived, audience-bound, automatically rotating token through the TokenRequest API.
  • The token is mounted at /var/run/secrets/kubernetes.io/serviceaccount/token. Anything that can read the pod's filesystem can use it, which is another argument for read-only root filesystems.
  • Turn automounting off and mount a token explicitly only in the containers that need one.
  • Give each workload its own service account. Sharing one makes the audit log useless and the blast radius large.
  • For external systems, use workload identity federation rather than copying a cloud credential into a Secret.
kubectl create serviceaccount api -n storefront
kubectl get serviceaccounts -A
kubectl get rolebindings,clusterrolebindings -A -o wide | grep storefront
kubectl auth whoami --as=system:serviceaccount:storefront:api

Keeping containers unprivileged

A container that runs as root inside a namespace can often become root on the node. securityContext closes that path, and Pod Security Admission enforces a minimum standard per namespace.

spec:
  securityContext:                 # applies to every container in the pod
    runAsNonRoot: true
    runAsUser: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: api
      image: ghcr.io/example/api:2.1.0
      securityContext:             # per container
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        privileged: false
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}
SettingStops
runAsNonRootContainers that would start as uid 0
allowPrivilegeEscalation: falsesetuid binaries and gaining more privileges than the parent
readOnlyRootFilesystemWriting over binaries or dropping a payload on the container filesystem
capabilities.drop: ALLRaw sockets, mounting, changing ownership and the rest of the default set
seccompProfile: RuntimeDefaultMost dangerous syscalls
privileged: falseFull access to the host device tree and kernel capabilities
# enforce a baseline on a namespace, with a visible warning first
kubectl label namespace storefront \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted

kubectl get pod api-abc -o jsonpath='{.spec.securityContext}'
⚠️
Anything granted create on pods, or the ability to mount hostPath volumes, can reach the node's filesystem and often its credentials. Restricting who may create pods matters more than restricting what a pod image contains.

FAQ

Why does my container fail to start after adding runAsNonRoot?
The image's user is uid 0 or has no numeric user, so the runtime has nothing to switch to. Set runAsUser to a numeric id that exists in the image, or rebuild the image with a non-root USER.
How do I give a pod access to just one Secret?
Use a dedicated service account with a Role that grants get on that one Secret name via resourceNames, bind it to that service account, and set serviceAccountName on the pod.

Networking, DNS and NetworkPolicy Helm and packaging applications

Last refreshed 2026-09-18.