Skip to content
Documentation Background

RBAC and the API security pipeline

Every action in a Kubernetes cluster - from a human operator running kubectl to a Pod calling the API directly - is an HTTP request to the API server. To protect cluster state, each request traverses a mandatory three-stage security pipeline before it can be executed.


flowchart TD
    Req["📥 Incoming API Request"]
    APIServer["☸️ API Server"]

    subgraph Pipeline["Mandatory Three-Stage Security Pipeline"]
        direction TB
        AuthN["1️⃣ Authentication (authN)<br/><i>Who is making the request?</i>"]
        AuthZ["2️⃣ Authorization (authZ)<br/><i>Is the action permitted? (RBAC)</i>"]
        Adm["3️⃣ Admission Control<br/><i>Mutating & validating controllers</i>"]

        AuthN -->|"Identity verified"| AuthZ
        AuthZ -->|"Authorized"| Adm
    end

    Exec["✅ Approved & Executed<br/><i>Persisted to etcd</i>"]

    Err401["❌ HTTP 401 Unauthorized<br/><i>Unverified credentials / token</i>"]
    Err403["❌ HTTP 403 Forbidden<br/><i>Denied by RBAC policy</i>"]
    ErrAdm["❌ Request Rejected<br/><i>Policy check fails (writes only)</i>"]

    Req -->|"TLS connection"| APIServer
    APIServer --> AuthN

    AuthN -->|"Unverified"| Err401
    AuthZ -->|"Unauthorized"| Err403
    Adm -->|"Policy fails"| ErrAdm

    Adm -->|"Passed (or read-only)"| Exec

The three stages are sequential and non-negotiable. A request that fails any stage is rejected immediately; the remaining stages are not evaluated.


Authentication proves who is making the request. If credential verification fails, the API server returns HTTP 401 Unauthorized and halts.

Kubernetes authentication flow

Kubernetes has no built-in user database. Identity management is fully offloaded to external systems. Supported mechanisms:

  • X.509 client certificates - the default for most cluster bootstrapping tools and local dev environments (Docker Desktop, kubeadm)
  • Webhook tokens - the API server forwards tokens to an external HTTP endpoint for verification
  • Enterprise/cloud IAM - Active Directory, AWS IAM, Google Cloud IAM, Azure AD; managed services (EKS, GKE, AKS) provide out-of-the-box integration

Once identity is verified, the request advances to authorization.

kubectl and other clients read API server endpoints and user credentials from a kubeconfig file (~/.kube/config on Linux/macOS, %USERPROFILE%\.kube\config on Windows). It has four top-level sections:

SectionPurpose
clustersAPI server URL and the cluster’s CA public key (certificate-authority-data)
usersUser credentials: bearer tokens or base64-encoded X.509 certificate data
contextsNamed user + cluster + namespace combinations
current-contextThe active context kubectl targets for all commands
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS0tLS1C... # cluster CA public key
server: https://<api-server-url>:443
name: prod-cluster
users:
- name: ops-user
user:
token: eyJhbGciOiJSUzI1NiIs... # bearer token credential
contexts:
- context:
cluster: prod-cluster
namespace: default
user: ops-user
name: ops-context
current-context: ops-context

kubectl config subcommands let you inspect and modify kubeconfig files without editing YAML directly.

CommandWhat it does
kubectl config viewDisplay the merged, active configuration across all loaded kubeconfig files
kubectl config current-contextPrint the name of the active context
kubectl config use-context <name>Switch current-context to a different cluster, user, or namespace target
kubectl config set-credentials <user> --client-key=... --client-certificate=... --embed-certs=trueAdd or update a user credential entry in the kubeconfig file

The --embed-certs=true flag writes raw base64-encoded certificate data directly into the kubeconfig instead of storing file paths, making the file portable across machines.

When client certificates are used, the API server extracts identity directly from the certificate fields signed by the cluster’s CA:

  • CN (Common Name) maps to the Kubernetes username
  • O (Organization) maps to Kubernetes group memberships
Kubernetes X.509 identity decoding

To decode embedded identity from a running kubeconfig:

Terminal window
kubectl config view --raw -o json \
| jq ".users[] | select(.name==\"ops-user\")" \
| jq -r '.user["client-certificate-data"]' \
| base64 -d | openssl x509 -text | grep "Subject:"
# Expected output:
# Subject: O=kubeadm:cluster-admins, CN=kubernetes-admin

The ops-user label is just a friendly config alias. The actual username and group evaluated by the API server are embedded inside the certificate itself.


Authorization determines what an authenticated identity can do. Kubernetes uses Role-Based Access Control (RBAC) with a strict deny-by-default posture: a freshly authenticated user has zero permissions until explicit allow rules are defined.

RBAC supports only allow rules. There are no explicit deny rules. Any action not covered by an allow rule is automatically forbidden (HTTP 403 Forbidden). This additive model eliminates rule-conflict resolution, simplifies auditing, and prevents accidental exposure from conflicting policies.

Kubernetes also supports multiple authorization modules simultaneously (RBAC, ABAC, Webhook). As soon as any module approves a request, evaluation stops and the request advances to admission control.

Kubernetes RBAC flow

Every permission evaluation resolves three dimensions:

ElementDescriptionExamples
Subject (who)The authenticated entity making the requestUsers, Groups, ServiceAccounts
Action (what)The REST verb describing the operationget, list, create, update, patch, delete, watch
Resource (which)The API object type and its API grouppods, deployments, secrets

Because the Kubernetes API is REST-based, RBAC verbs map directly to HTTP methods:

Verb(s)HTTP methodCommon response
createPOST201 Created
get, list, watchGET200 OK
updatePUT200 OK
patchPATCH200 OK
deleteDELETE200 OK

All verbs return 403 Access Denied when the subject lacks permission.

Kubernetes RBAC objects and scope

RBAC uses four API objects under rbac.authorization.k8s.io/v1:

ScopePermission definitionSubject binding
NamespacedRoleRoleBinding
Cluster-wideClusterRoleClusterRoleBinding

A Role defines allowed verbs on resources within a single namespace. It must declare metadata.namespace. A Role object alone has no effect until a RoleBinding assigns it to a subject.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: backend # scope is limited to this namespace
name: read-deployments
rules:
- verbs: ["get", "watch", "list"]
apiGroups: ["apps"]
resources: ["deployments"]

A RoleBinding grants the permissions from a Role (or ClusterRole) to subjects within a specific namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-deployments
namespace: backend
subjects:
- kind: User
name: dev-user
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: read-deployments
apiGroup: rbac.authorization.k8s.io

A ClusterRole defines permissions at the cluster level, covering all namespaces and non-namespaced objects such as Nodes and StorageClasses. It omits metadata.namespace.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: read-deployments # no namespace field — cluster-wide scope
rules:
- verbs: ["get", "watch", "list"]
apiGroups: ["apps"]
resources: ["deployments"]

A ClusterRoleBinding grants a ClusterRole to subjects across every namespace in the cluster without restriction.

A ClusterRole can be referenced by a namespace-scoped RoleBinding. This avoids duplicating identical Role definitions across namespaces while still scoping each binding’s effect to its own namespace:

flowchart TD
    CR["🌐 ClusterRole<br/><code>read-deployments</code><br/><i>(Single cluster-wide definition)</i>"]

    subgraph nsA["📁 Namespace A"]
        RB_A["📄 RoleBinding<br/><code>roleRef:</code><br/><code>read-deployments</code>"]
    end

    subgraph nsB["📁 Namespace B"]
        RB_B["📄 RoleBinding<br/><code>roleRef:</code><br/><code>read-deployments</code>"]
    end

    CR -->|"referenced via roleRef"| RB_A
    CR -->|"referenced via roleRef"| RB_B

The bound subject receives permissions only inside the namespace that holds the RoleBinding. Other namespaces remain inaccessible.

Inside a Role or ClusterRole, each entry under rules ties together three fields:

rules:
- verbs: ["get", "watch", "list"] # allowed operations
apiGroups: ["apps"] # API group that owns the resource
resources: ["deployments"] # target object type

The apiGroups field identifies which API group owns the resource:

  • Core group ("") - early Kubernetes resources: pods, secrets, services. Written as an empty string in YAML.
  • Named groups - all other resources require an explicit group name.
apiGroupresourceAPI path
"" (core)pods/api/v1/namespaces/{ns}/pods
"" (core)secrets/api/v1/namespaces/{ns}/secrets
"apps"deployments/apis/apps/v1/namespaces/{ns}/deployments
"storage.k8s.io"storageclasses/apis/storage.k8s.io/v1/storageclasses

To discover all available resources, their API groups, and supported verbs on a live cluster:

Terminal window
kubectl api-resources --sort-by name -o wide
# Expected output (truncated):
# NAME APIGROUP KIND VERBS
# deployments apps Deployment [create delete get list patch update watch]
# pods Pod [create delete get list patch update watch]
# secrets Secret [create delete get list patch update watch]

Kubernetes ships with four pre-defined ClusterRoles for common access profiles. Bind them with a RoleBinding for namespace-scoped access or a ClusterRoleBinding for cluster-wide access.

ClusterRoleReadWriteRoles/RoleBindingsSecrets
cluster-adminAll namespacesAll namespacesYesYes
adminNamespaceNamespaceYes (within namespace)Yes
editNamespaceNamespaceNoYes
viewNamespaceNoNoNo

Key distinctions:

  • admin vs edit: admin can modify Roles and RoleBindings, which means it can grant or escalate permissions within the namespace. edit withholds that access deliberately.
  • edit vs view: edit grants access to Secrets so workloads can mount credentials during deployment. view excludes Secrets to prevent read-only accounts from inspecting sensitive data.
Kubernetes aggregated roles

An aggregated ClusterRole composes rules from multiple existing ClusterRoles using label selectors, avoiding duplicated rules entries when you need to combine permission sets.

# Source role 1: grants pod listing
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: list-pods
labels:
rbac-pod-list: "true" # label that the aggregator selects on
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["list"]
---
# Source role 2: grants service deletion
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: delete-services
labels:
rbac-service-delete: "true"
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["delete"]
---
# Aggregated role: inherits rules from both sources via label selection
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-and-service-manager
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac-pod-list: "true"
- matchLabels:
rbac-service-delete: "true"
rules: [] # populated automatically from matching ClusterRoles

kubectl describe clusterrole pod-and-service-manager resolves the selectors and renders the merged PolicyRule matrix. Any ClusterRole you add with a matching label is picked up automatically without editing the aggregated role.

kubectl auth can-i is the canonical tool for testing whether a subject has a specific permission without executing the actual operation.

Terminal window
# Test whether a user can list pods in the default namespace
kubectl auth can-i list pods --as dev-user -n default
# yes
# Test a service account
kubectl auth can-i list pods --as system:serviceaccount:monitoring:metrics-reader -n monitoring
# no
# List all permissions granted to a subject in a namespace
kubectl auth can-i --list --as dev-user -n default
# Expected output structure:
# Resources Non-Resource URLs Resource Names Verbs
# pods [] [] [list get watch]
# deployments.apps [] [] [list get watch]

The --list flag outputs the full permission matrix for the subject in the specified namespace, including non-resource URL paths.

kubectl get roles and kubectl get rolebindings show only metadata. Use kubectl describe to see rules and bound subjects:

Terminal window
kubectl describe role pod-reader -n monitoring
# PolicyRule:
# Resources Non-Resource URLs Resource Names Verbs
# --------- ----------------- -------------- -----
# pods [] [] [get list watch]
Terminal window
kubectl describe rolebinding metrics-reader-binding -n monitoring
# Role:
# Kind: Role
# Name: pod-reader
# Subjects:
# Kind Name Namespace
# ---- ---- ---------
# ServiceAccount metrics-reader monitoring

Cluster Admin Demo

Most kubeadm-bootstrapped clusters ship with a pre-configured cluster-admin ClusterRole and binding. Tracing an admin request end-to-end illustrates how the pieces connect.

Step 1 — Decode the certificate identity:

Terminal window
kubectl config view --raw -o json \
| jq ".users[] | select(.name==\"ops-user\")" \
| jq -r '.user["client-certificate-data"]' \
| base64 -d | openssl x509 -text | grep "Subject:"
# Subject: O=kubeadm:cluster-admins, CN=kubernetes-admin

The user is kubernetes-admin; the group is kubeadm:cluster-admins.

Step 2 — Inspect the ClusterRole:

Terminal window
kubectl describe clusterrole cluster-admin
# PolicyRule:
# Resources Non-Resource URLs Resource Names Verbs
# --------- ----------------- -------------- -----
# *.* [] [] [*]
# [*] [] [] [*]

Wildcards on all dimensions grant complete control over every object in every namespace.

Step 3 — Locate the binding:

Terminal window
kubectl get clusterrolebindings | grep cluster-admin
kubectl describe clusterrolebindings kubeadm:cluster-admins
# Subjects:
# Kind Name
# ---- ----
# Group kubeadm:cluster-admins

The ClusterRoleBinding links the group kubeadm:cluster-admins to ClusterRole/cluster-admin.

Full pipeline:

flowchart TD
    Kubeconfig["🔑 1. kubeconfig file<br/><i>Client credentials</i><br/><code>CN=kubernetes-admin</code> · <code>O=kubeadm:cluster-admins</code>"]
    CRB["🔗 2. ClusterRoleBinding<br/><i>Links group to cluster-wide role</i><br/><code>group: kubeadm:cluster-admins</code> ➔ <code>cluster-admin</code>"]
    CR["👑 3. ClusterRole: cluster-admin<br/><i>Full cluster administrator privileges</i><br/><code>*</code> verbs on <code>*</code> resources across all namespaces"]

    Kubeconfig -->|"authN pass (identity extracted)"| CRB
    CRB -->|"authZ pass (permissions matched)"| CR

Admission control enforces organizational and operational policies on write requests after they pass authN and authZ. Read operations (get, list, watch) bypass admission control entirely because they do not alter cluster state.

Two categories of admission controller run in sequence:

TypeModifies request?Execution orderTypical use
MutatingYesFirstInject defaults, add sidecars, set imagePullPolicy: Always
ValidatingNoSecondBlock non-compliant manifests, enforce security constraints
flowchart TD
    AuthZ["🛡️ Approved by authZ"]

    Phase1["✏️ Phase 1: Mutating Admission<br/><i>Modifies payload to meet cluster standards</i><br/><code>e.g., AlwaysPullImages, sidecar injection</code>"]
    Phase2["🔍 Phase 2: Validating Admission<br/><i>Inspects finalized payload; approves or rejects</i><br/><code>e.g., NodeRestriction, PodSecurity constraints</code>"]

    Etcd["💾 Persisted to etcd<br/><i>Object written to cluster state</i>"]

    AuthZ -->|"write requests only"| Phase1
    Phase1 -->|"mutated payload"| Phase2
    Phase2 -->|"all checks pass"| Etcd

Mutating controllers run first so that all payload modifications are in place before validation. If any single controller in either phase rejects the request, the API server aborts immediately without evaluating the remaining controllers. All enabled admission controllers must approve a request for it to succeed.

AlwaysPullImages (mutating) sets spec.containers.imagePullPolicy: Always on every new Pod. This forces the container runtime to pull images from the registry on every start, preventing use of stale or unauthorized locally cached images.

NodeRestriction (validating) restricts a node’s kubelet to modifying only its own Node and Pod objects, preventing a compromised node from escalating access to other nodes.

To verify which plugins are enabled on the API server control plane Pod:

Terminal window
kubectl describe pod kube-apiserver-desktop-control-plane \
--namespace kube-system | grep admission
# --enable-admission-plugins=NodeRestriction

Mutating vs. validating: the env label example

Section titled “Mutating vs. validating: the env label example”
Mutating vs. validating admission controllers

Consider a policy requiring all resources in a production namespace to carry env=prod:

  • Mutating approach: detects the missing label and automatically injects metadata.labels.env: prod before the object reaches validation.
  • Validating approach: detects the missing label and rejects the request outright with an error.

Both approaches enforce the policy. The mutating controller applies a silent fix; the validating controller demands the submitter fix the manifest themselves.

Plugins are enabled on the API server via the --enable-admission-plugins flag. In clusters bootstrapped with kubeadm, the API server runs as a static Pod; edit its manifest at /etc/kubernetes/manifests/kube-apiserver.yaml to add or remove plugins.

Terminal window
# Verify which plugins are currently active
kubectl describe pod kube-apiserver-<node> -n kube-system | grep admission

Common built-in plugins and their purpose:

PluginTypePurpose
NamespaceLifecycleValidatingPrevents object creation in terminating namespaces; protects default, kube-system, and kube-public from deletion
PodSecurityValidatingEnforces Pod Security Standards (Privileged, Baseline, Restricted) against the Pod’s security context fields
LimitRangerMutating + validatingApplies default resource requests/limits when omitted; rejects Pods that exceed the namespace’s defined ranges
ResourceQuotaValidatingTracks aggregate resource consumption per namespace and rejects requests that would exceed the quota
AlwaysPullImagesMutatingSets imagePullPolicy: Always on every new Pod, preventing stale or unauthorized locally cached images
NodeRestrictionValidatingRestricts a node’s kubelet to modifying only its own Node and Pod objects

Service accounts are the identity mechanism for workloads running inside Pods. Where human users authenticate with X.509 certificates or external tokens, Pods authenticate with a service account bearer token that Kubernetes manages automatically.

The API server identifies an in-cluster caller as system:serviceaccount:<namespace>:<serviceaccount-name> and evaluates its RBAC permissions like any other subject.

Every namespace gets a default ServiceAccount automatically. When a Pod does not declare spec.serviceAccountName, Kubernetes assigns it to default.

Outside kube-system, the default ServiceAccount carries only the system:discovery ClusterRole, which grants access to basic API discovery endpoints and nothing else. Any attempt to list, create, or modify cluster resources returns HTTP 403 Forbidden. The default ServiceAccount is re-created immediately if deleted.

Create a dedicated ServiceAccount for any workload that needs specific API access:

Terminal window
# Imperative
kubectl create serviceaccount metrics-reader -n monitoring
# Declarative
apiVersion: v1
kind: ServiceAccount
metadata:
name: metrics-reader
namespace: monitoring

Attach it to a Pod via spec.serviceAccountName:

spec:
serviceAccountName: metrics-reader

Then bind a Role or ClusterRole to that ServiceAccount using a RoleBinding or ClusterRoleBinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: metrics-reader-binding
namespace: monitoring
subjects:
- kind: ServiceAccount
name: metrics-reader
namespace: monitoring
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io

Containers inside a Pod connect to the API server at the internal DNS address https://kubernetes.default.svc. Kubernetes automatically mounts the ServiceAccount token at a well-known path:

/var/run/secrets/kubernetes.io/serviceaccount/token

Workloads attach the token to API calls via the Authorization header:

Terminal window
# From inside a container shell
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -s -k \
-H "Authorization: Bearer $TOKEN" \
https://kubernetes.default.svc/api/v1/namespaces/monitoring/pods

If the ServiceAccount has no permission for the requested verb, the API server returns HTTP 403 Forbidden.


CommandEffect
kubectl config viewShow merged kubeconfig
kubectl config current-contextPrint active context name
kubectl config use-context <name>Switch active context
kubectl config set-credentials <user> --client-key=... --client-certificate=... --embed-certs=trueRegister user credentials
ObjectScopeDefinesBinds
RoleSingle namespaceAllowed actionsvia RoleBinding
RoleBindingSingle namespacesubjects to a Role or ClusterRole
ClusterRoleCluster-wideAllowed actionsvia ClusterRoleBinding or RoleBinding
ClusterRoleBindingCluster-widesubjects to a ClusterRole
Role typeBinding typeEffective scope
RoleRoleBindingPermissions apply only within the namespace where both objects reside
ClusterRoleRoleBindingClusterRole rules apply, but restricted to the RoleBinding’s namespace only
ClusterRoleClusterRoleBindingPermissions apply across all namespaces and non-namespaced resources
StageHandles read requests?Failure response
Authentication (authN)YesHTTP 401 Unauthorized
Authorization (authZ)YesHTTP 403 Forbidden
Mutating admissionNoImmediate abort
Validating admissionNoImmediate abort

SymptomLikely causeFix
HTTP 401 from kubectlCredentials in kubeconfig are invalid or expiredRotate/reissue the client certificate or token; run kubectl config view to verify the user entry
HTTP 403 on a specific resourceNo Role/ClusterRole grants the requested verbInspect bindings with kubectl describe rolebinding; add a rule for the missing verb and resource
HTTP 403 even after adding a RoleRoleBinding is in the wrong namespace, or the subject name does not match the CN in the certificateVerify metadata.namespace on the binding and that subjects[].name matches the decoded CN
HTTP 403 for a ServiceAccountServiceAccount has no RoleBinding, or the binding references the wrong namespace or nameCheck kubectl auth can-i --list --as system:serviceaccount:<ns>:<sa> to see granted permissions
Admission webhook rejectionA mutating or validating webhook is blocking the requestCheck the error message for the controller name; inspect it with kubectl describe validatingwebhookconfigurations
imagePullPolicy: Always applied unexpectedlyAlwaysPullImages mutating controller is enabledExpected behavior; confirm with kubectl describe pod kube-apiserver-<node> -n kube-system | grep admission