RBAC and Security in Kubernetes
Overview
Kubernetes exposes a powerful API that, if left unguarded, allows any caller to read secrets, execute commands in containers, or destroy entire namespaces. Role-Based Access Control (RBAC) is the primary authorization mechanism for controlling who can do what to which resources. Beyond RBAC, Kubernetes security is layered: admission controllers enforce policy at creation time, network policies limit lateral movement, and secrets management controls sensitive data access.
This document covers the full security stack from API authentication through workload isolation. It is essential reading for anyone operating a production cluster where multiple teams share infrastructure, or where compliance requirements demand least-privilege enforcement.
Prerequisites
- Kubernetes API resource model (namespaces, resources, subresources)
- Understanding of X.509 certificates and JWT tokens
- Basic familiarity with Linux network namespaces and iptables
- Kubernetes service accounts and pod identity concepts
- Understanding of admission webhooks
Historical Context
Early Kubernetes (pre-1.6) used a simpler authorization mode called Attribute-Based Access Control (ABAC), which required policy files on disk and a kubelet restart to update. RBAC was introduced in Kubernetes 1.6 (2017) as a beta feature and became stable in 1.8, becoming the de facto standard.
PodSecurityPolicy (PSP) was a controversial admission controller that enforced security constraints on pods (prevent privileged containers, require read-only root filesystem, etc.). It was deprecated in 1.21 (2021) and removed in 1.25 (2022) due to usability problems — the interaction between PSPs and RBAC was error-prone and the feature was inconsistently implemented. It was replaced by Pod Security Admission (PSA), a simpler built-in admission controller with three predefined levels.
Bound service account tokens (1.21, GA in 1.22) replaced long-lived JWT tokens that were never rotated. The new tokens are time-limited, audience-limited, and rotated automatically by the kubelet.
RBAC Model
RBAC in Kubernetes has four core objects:
RBAC Subject → Binding → Role → Resources
Subject (who):
- User (external: X.509 CN, OIDC sub, static token)
- Group (X.509 O, OIDC groups claim)
- ServiceAccount (in-cluster pod identity)
Role (namespace-scoped) or ClusterRole (cluster-scoped):
- rules: list of permissions
- apiGroups: ["", "apps", "batch"]
- resources: ["pods", "deployments", "jobs"]
- verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Binding:
- RoleBinding: binds Role or ClusterRole to subjects in a namespace
- ClusterRoleBinding: binds ClusterRole to subjects cluster-wide
Role Spec
A Role grants permissions within a single namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: pod-reader
rules:
- apiGroups: [""] # "" = core API group
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"] # subresource
verbs: ["get"]
apiGroups: The empty string "" refers to the core API group (pods, services, configmaps, secrets, nodes). Named groups like apps cover deployments, daemonsets, statefulsets. batch covers jobs and cronjobs.
Verbs: Map to HTTP methods on the Kubernetes API:
- get → GET single resource
- list → GET collection
- watch → GET with ?watch=true (streaming)
- create → POST
- update → PUT (full replace)
- patch → PATCH (partial update)
- delete → DELETE
- deletecollection → DELETE collection
- escalate, bind, impersonate — special verbs for RBAC itself
ClusterRole
ClusterRoles work like Roles but are not namespace-scoped. They are required for:
- Cluster-scoped resources: nodes, namespaces, clusterroles, persistentvolumes
- Non-resource URLs: /healthz, /metrics, /version
- Granting the same permissions across all namespaces (via ClusterRoleBinding)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/healthz", "/livez", "/readyz"]
verbs: ["get"]
RoleBinding and ClusterRoleBinding
RoleBinding (namespace-scoped):
- Can bind a Role (same namespace)
- Can bind a ClusterRole (but permission applies only in binding's namespace)
- Subjects: User, Group, ServiceAccount
ClusterRoleBinding (cluster-scoped):
- Can only bind a ClusterRole
- Grants cluster-wide permission
- CANNOT be restricted to a namespace
Important: A ClusterRole bound via a RoleBinding is scoped to that namespace only. This is a common pattern: define a standard namespace-admin ClusterRole once, then bind it per-namespace via RoleBindings. This avoids duplicating Role definitions across namespaces.
Cross-namespace access:
ServiceAccount in namespace A CANNOT access resources in namespace B
using a RoleBinding (which is namespace-scoped).
To grant cross-namespace access, you need:
ClusterRoleBinding → ServiceAccount in A → ClusterRole (cluster-wide)
There is no "access namespace B from A" without cluster-wide binding.
This is a deliberate security boundary.
RBAC Subject → Role → Resource Flow
kubectl get pods -n production (as user "alice")
1. API Server receives request:
Verb: get (list)
Resource: pods
Namespace: production
User: alice (from X.509 CN or OIDC token)
2. Authentication:
Validates identity → alice is who she says she is
3. Authorization (RBAC):
Query: can alice LIST pods in production?
a. Find all Bindings where alice is a subject:
- RoleBinding "dev-binding" in production namespace
→ binds Role "pod-reader"
b. Evaluate Role "pod-reader" rules:
- apiGroups: [""]
- resources: ["pods"] ✓
- verbs: ["get", "list", "watch"] ✓ (list matches)
c. ALLOW
4. Admission Controllers (if mutating/creating):
Not applicable for read-only request.
5. etcd read → response to alice
Deny example:
alice tries: kubectl delete pod bad-pod -n production
RBAC check: can alice DELETE pods in production?
Role "pod-reader" has verbs: ["get", "list", "watch"] — no "delete"
→ DENY: 403 Forbidden
Service Accounts and Pod Identity
Service accounts are Kubernetes objects in a namespace that provide an identity for processes running inside Pods.
Legacy tokens (pre-1.21):
- Long-lived JWT mounted as a Secret
- Never expired, could be used forever if leaked
- Auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token
Bound service account tokens (1.21+, default):
Token properties:
- Audience-bound: only valid for the Kubernetes API server
(or specific audience if projected volume specifies it)
- Time-bound: expires in 1 hour by default; kubelet rotates before expiry
- Pod-bound: token is invalidated when pod is deleted
- Signed by API server's service account signing key
Token location (same path, but now a projected volume):
/var/run/secrets/kubernetes.io/serviceaccount/
├── token (JWT, rotated every ~50 minutes)
├── ca.crt (cluster CA for TLS verification)
└── namespace (pod's namespace)
Disabling automounting (for security-conscious deployments):
apiVersion: v1
kind: ServiceAccount
metadata:
name: no-api-access
automountServiceAccountToken: false
Common RBAC Patterns
View-only role (safe for developers reading production):
kind: ClusterRole
rules:
- apiGroups: ["", "apps", "batch", "networking.k8s.io"]
resources: ["pods", "deployments", "services", "ingresses",
"jobs", "replicasets", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
# Note: excludes secrets — even view-only roles should not read secrets
Namespace admin (full control of one namespace):
kind: ClusterRole # defined once, bound per-namespace via RoleBinding
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
# Bind via RoleBinding in target namespace — cannot affect other namespaces
Cross-namespace access is impossible via RoleBinding alone. If service A in namespace payments must read ConfigMaps in namespace shared-config, you need either:
1. A ClusterRoleBinding (gives access to all namespaces — too broad)
2. Copy the ConfigMap into each namespace (operational burden)
3. Use a custom aggregation pattern or external secret sync tool
RBAC Debugging
# Check if a user/service account can perform an action
kubectl auth can-i list pods -n production --as=alice
kubectl auth can-i list pods -n production \
--as=system:serviceaccount:production:my-service-account
# Check all permissions for a service account (verbose)
kubectl auth can-i --list --as=system:serviceaccount:default:myapp
# Describe what a role allows
kubectl describe role pod-reader -n production
kubectl describe clusterrole view
# Find all bindings for a service account
kubectl get rolebindings,clusterrolebindings -A -o json | \
jq '.items[] | select(.subjects[]?.name=="myapp" and
.subjects[]?.namespace=="default")'
# Reconcile RBAC from a manifest (idempotent apply for RBAC objects)
kubectl auth reconcile -f rbac-manifest.yaml
Common RBAC mistakes:
- Granting * verbs on secrets to developers (leaks all secrets)
- Forgetting pods/log, pods/exec, pods/portforward subresources (these require explicit grants)
- Using ClusterRoleBinding where RoleBinding would suffice (over-privileges)
- Not auditing service account token usage — enable audit logs and monitor for unexpected API calls
Admission Controllers for Security
After RBAC authorization, admission controllers can still reject or mutate requests. Key security admission controllers:
Pod Security Admission (PSA) — replaces PSP:
Three policy levels:
privileged: unrestricted (for system namespaces like kube-system)
baseline: prevents known privilege escalations:
- no hostPID/hostIPC/hostNetwork
- no privileged containers
- no hostPath volumes (most dangerous)
- restricts host ports
- limits /proc mask
restricted: hardened:
- all baseline restrictions +
- requires non-root user
- requires read-only root filesystem (recommended)
- drops all capabilities, only allows NET_BIND_SERVICE
- requires seccomp profile (RuntimeDefault or Localhost)
- requires allowPrivilegeEscalation: false
Applied per-namespace via labels:
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
Other key admission controllers:
- LimitRanger — applies default resource requests/limits
- ResourceQuota — enforces namespace resource caps
- MutatingAdmissionWebhook / ValidatingAdmissionWebhook — custom policy (OPA Gatekeeper, Kyverno)
Network Policy
Network policies are L3/L4 firewall rules enforced by the CNI plugin (Calico, Cilium, Weave). They are Pod-selector-based: not IP-based like traditional firewalls.
Default behavior: NO network policies = all traffic allowed.
To lock down a namespace, start with a deny-all default:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # selects ALL pods
policyTypes:
- Ingress
- Egress
---
Then open specific paths:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Selector types:
- podSelector: match pods by label (within same namespace)
- namespaceSelector: match all pods in namespaces with given label
- ipBlock: CIDR range (for external traffic or cross-cluster)
- Combined podSelector + namespaceSelector: AND condition
Important limitation: Network policies are enforced at OSI layer 3/4 only (IP + TCP/UDP/SCTP). They do NOT understand HTTP paths, hostnames, or TLS SNI. For L7 policy, use a service mesh (Istio, Linkerd) or Cilium's Layer 7 policy extensions.
Secrets Management
Kubernetes Secrets are not encrypted at rest by default:
Default behavior:
Secret value → base64 encoded → stored in etcd
Base64 is encoding, NOT encryption.
Anyone with etcd access can decode all secrets.
Options for encryption at rest:
1. EncryptionConfiguration (built-in):
- AES-CBC with PKCS#7 (legacy, avoid)
- AES-GCM (recommended, 1.28+)
- KMS envelope encryption (recommended for production):
Secret → encrypt with DEK → DEK encrypted by KMS provider
(AWS KMS, GCP Cloud KMS, HashiCorp Vault)
2. External Secrets Operator (ESO):
- Syncs secrets from Vault, AWS Secrets Manager, Azure Key Vault
into Kubernetes Secrets
- Secrets still land in etcd as regular Secrets
- Operator rotates them on schedule
3. Vault Agent Sidecar Injector:
- Vault secrets injected as files into pod filesystem
- Never touch Kubernetes Secrets or etcd at all
- Most secure option for sensitive data
Least-privilege for secrets:
# Grant access to only specific secret names
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-password", "api-key"] # named resource restriction
verbs: ["get"]
Debugging Notes
# Audit RBAC: who can access secrets?
kubectl auth can-i get secrets -n production --list | grep "yes"
# Check for overly-permissive bindings
kubectl get clusterrolebindings -o json | jq '.items[] |
select(.roleRef.name=="cluster-admin") |
{name: .metadata.name, subjects: .subjects}'
# Verify network policy is enforced by CNI
kubectl exec -it test-pod -- wget -qO- http://restricted-service:8080
# Should fail if NetworkPolicy is correct
# Check if secrets are encrypted at rest
kubectl get secrets <secret> -o json | base64 -d # should fail/be encrypted
# Or check etcdctl directly (requires etcd access):
ETCDCTL_API=3 etcdctl get /registry/secrets/default/mysecret | xxd | head
# Unencrypted: shows k8s\x00\n... plus readable content
# Encrypted: shows k8s:enc:aescbc:... random bytes
Security Implications
- A compromised service account with
get secretsin any namespace can escalate privileges if other service accounts' tokens are stored as secrets there. pods/execsubresource permission is equivalent torootaccess on the node. Never grant broadly.- Workload Identity (GKE, EKS IRSA, AKS Workload Identity) binds Kubernetes service accounts to cloud IAM roles without static credentials — preferred over mounting cloud credentials as secrets.
- RBAC does not control what a container can do on the node (privileged containers bypass everything). PodSecurity or OPA Gatekeeper is needed to enforce this.
- The
defaultservice account exists in every namespace and is auto-mounted by default. It should have no RBAC permissions.
Performance Implications
- RBAC authorization is evaluated in memory from a cache; it does not hit etcd per request after initial load. Authorization latency is negligible (<1ms).
- Large numbers of RoleBindings (thousands) can slow the RBAC cache reconciliation. Use aggregated ClusterRoles to reduce binding count.
- Network policies are enforced by the kernel (via eBPF in Cilium, iptables in Calico legacy mode). High numbers of policies can slow pod startup as rules are installed.
Modern Usage
- Workload Identity Federation: Cloud providers (AWS, GCP, Azure) support binding Kubernetes service accounts to cloud IAM roles via OIDC. This eliminates static cloud credentials entirely.
- Kyverno and OPA Gatekeeper: Policy-as-code engines that go beyond PSA to enforce custom organizational policies (naming conventions, required labels, image registry restrictions).
- Cilium Network Policy: Extends standard NetworkPolicy with L7 HTTP/gRPC rules, DNS policy, and identity-aware policies using cryptographic workload identity.
Future Directions
- CEL-based admission (1.28, GA 1.30): Validating Admission Policies written in Common Expression Language — no webhook overhead, native to API server.
- Fine-grained secrets access with SOCI (Secret Object Control Interface): Proposed mechanism for attributing secret access to specific workloads at the API server level.
- RBAC improvements: Field-level access control (grant read of pod spec but not env vars) is a long-requested feature under active discussion.
Exercises
-
Create a service account
read-only-sain namespacestaging. Grant it permission to list pods and read pod logs but explicitly deny access to secrets. Verify withkubectl auth can-i. -
Apply a
default-deny-allNetworkPolicy to a test namespace. Deploy two pods. Verify they cannot communicate. Write a NetworkPolicy that allows communication only on port 5000. Test both permitted and blocked traffic. -
Enable audit logging on a local cluster (minikube or kind). Create a secret, then read it as a service account. Find the audit log entries for the secret access.
-
Configure PSA on a namespace with the
restrictedlevel. Attempt to deploy a privileged pod and observe the rejection message. Fix the pod spec to comply with restricted policy. -
Examine the bound service account token mounted in a running pod. Decode the JWT (without verification) using
base64 -d. Identify the expiry claim (exp) and compare to the token issued time.
References
- Kubernetes RBAC documentation: kubernetes.io/docs/reference/access-authn-authz/rbac/
- Pod Security Admission: kubernetes.io/docs/concepts/security/pod-security-admission/
- Network Policies: kubernetes.io/docs/concepts/services-networking/network-policies/
- Kubernetes Secrets encryption at rest: kubernetes.io/docs/tasks/administer-cluster/encrypt-data/
- "The 2019 Capital One breach" — AWS metadata service exploitation, relevant to cloud credential management in Kubernetes
- Aqua Security Kubernetes Security Guide (2023)
- NCC Group: "Kubernetes Pentest Methodology" — comprehensive attack surface analysis
- KEP-2579: Reduction of Secret-based Service Account Tokens
- CNCF TAG Security: Cloud Native Security Whitepaper