advanced
devops

Security & RBAC

ServiceAccounts, Roles, RoleBindings, Pod Security Standards, and secrets

kubernetes fundamentals
34 min read

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.

Learning Objectives
  • 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.

Verifies identity via client certificates, bearer tokens (including ServiceAccount tokens), or OIDC. Kubernetes has no User object — users are external identities the API server trusts.

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).

yaml
apiVersion: 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.

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.

yaml
apiVersion: 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.

yaml
apiVersion: 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

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.

yaml
apiVersion: 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:

yaml
spec: 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.

Encrypt a Secret into a SealedSecret that is safe to commit; a cluster-side controller decrypts it back into a real Secret. Good for a GitOps-first workflow.

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


Next Steps

Continue Learning
ArgoCD deployments.

Next Lesson

GitOps & CI/CD