Kubernetes Security & RBAC
Every request that hits the Kubernetes API server — whether from kubectl, a controller, or a Pod running your application — passes through the same three gates: authentication (who are you?), authorization (are you allowed to do this?), and admission control (does this object meet cluster policy?). Role-Based Access Control (RBAC) is the authorization layer, and it is the single most important tool you have for containing the blast radius when a credential leaks or a workload is compromised. In a shared production cluster, a well-scoped RBAC policy is the difference between "one Pod was breached" and "the attacker read every Secret in the cluster." This lesson walks through the identity model, RBAC objects, Pod Security Standards, and secret handling that a real cluster needs.
- Explain the authentication, authorization, and admission pipeline
- Grant least-privilege access with Roles, ClusterRoles, and their bindings
- Bind permissions to ServiceAccounts for in-cluster workloads
- Enforce Pod Security Standards with the built-in admission controller
- Handle Secrets safely with encryption at rest and external stores
The API request pipeline
Understanding RBAC starts with knowing where it sits. When a request arrives, the API server resolves an identity, then checks authorization, then runs admission plugins before persisting anything to etcd.
User object — users are external identities the API server trusts.Note
Roles vs. ClusterRoles
A Role holds a set of permissions scoped to a single namespace. A ClusterRole holds the same kind of rules but applies cluster-wide, and can also grant access to cluster-scoped resources (like nodes or persistentvolumes) and non-resource URLs (like /healthz).
yamlapiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: payments name: pod-reader rules: - apiGroups: [""] # "" is the core API group resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list"]
The apiGroups, resources, and verbs fields form the rule. The empty string "" denotes the core group (pods, services, configmaps), while named groups like apps or batch cover higher-level objects. Subresources such as pods/log and pods/exec are addressed with a slash.
Warning
resources: ["*"] and verbs: ["*"] outside of break-glass admin roles. A wildcard grant silently expands as new CRDs are installed, so a role that looked harmless can suddenly allow writing to a new resource type.Bindings: connecting identities to roles
A rule grants nothing until a binding attaches it to a subject. A RoleBinding grants a Role (or a ClusterRole) within one namespace; a ClusterRoleBinding grants a ClusterRole across the whole cluster.
yamlapiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: read-pods namespace: payments subjects: - kind: ServiceAccount name: app-sa namespace: payments roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io
A useful pattern is to define a reusable ClusterRole once and reference it from a RoleBinding in each namespace — the permissions then apply only to that namespace, not cluster-wide.
ServiceAccounts for workloads
Human users authenticate with certificates or OIDC, but Pods authenticate as ServiceAccounts. Every Pod is assigned one (the namespace default SA if you do not specify), and a short-lived, auto-rotated token is mounted at /var/run/secrets/kubernetes.io/serviceaccount/token via a projected volume.
yamlapiVersion: v1 kind: ServiceAccount metadata: name: app-sa namespace: payments automountServiceAccountToken: false # opt in per-Pod instead --- apiVersion: apps/v1 kind: Deployment metadata: name: api namespace: payments spec: selector: matchLabels: { app: api } template: metadata: labels: { app: api } spec: serviceAccountName: app-sa automountServiceAccountToken: true # only where the app calls the API containers: - name: api image: registry.example.com/api:1.4.2
Pro Tip
If a workload does not talk to the Kubernetes API, set automountServiceAccountToken: false so a compromised container has no token to replay against the API server.
Pod Security Standards
RBAC controls who can act on the API; it does not stop a Pod from running as root or mounting the host filesystem. That is the job of Pod Security Standards (PSS), enforced by the built-in Pod Security Admission controller (PodSecurityPolicy was removed in v1.25). PSS defines three levels — privileged (unrestricted), baseline (blocks known privilege escalations), and restricted (hardened best practice) — applied per namespace via labels.
yamlapiVersion: v1 kind: Namespace metadata: name: payments labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: v1.30 pod-security.kubernetes.io/warn: restricted pod-security.kubernetes.io/audit: restricted
The enforce mode rejects non-compliant Pods, while warn and audit let you roll a policy out safely — surfacing violations to kubectl users and the audit log before you flip enforcement on. A restricted namespace requires Pods to drop capabilities, run as non-root, and disallow privilege escalation:
yamlspec: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - name: api image: registry.example.com/api:1.4.2 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"]
Handling Secrets safely
Kubernetes Secrets are only base64-encoded, not encrypted, and by default they are stored in plaintext in etcd. Anyone who can read the Secret object or the etcd datastore can read the value. Two controls matter most: encryption at rest and tight RBAC on secrets.
yaml# EncryptionConfiguration passed to the API server via --encryption-provider-config apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: ["secrets"] providers: - aescbc: keys: - name: key1 secret: <base64-encoded-32-byte-key> - identity: {} # fallback so existing secrets still decrypt
For production, prefer a managed KMS provider (kms provider plugin) over a static local key, and keep secret material out of Git entirely.
SealedSecret that is safe to commit; a cluster-side controller decrypts it back into a real Secret. Good for a GitOps-first workflow.Warning
Auditing and testing permissions
Before shipping an RBAC change, test it. kubectl auth can-i evaluates a subject's effective permissions without you having to reason through every binding by hand.
bash# Check what the app's ServiceAccount can do kubectl auth can-i list pods \ --as=system:serviceaccount:payments:app-sa -n payments # -> yes kubectl auth can-i delete secrets \ --as=system:serviceaccount:payments:app-sa -n payments # -> no # List everything a subject is allowed to do in a namespace kubectl auth can-i --list \ --as=system:serviceaccount:payments:app-sa -n payments
Best Practices & Common Pitfalls
Pro Tip
kubectl-who-can, rbac-lookup, and rakkess reverse the question — "who can delete secrets?" — which is exactly what you need during an incident or a periodic access review.Next Steps
Next Lesson
GitOps & CI/CD