OneLinersCommand workbench
Guides
Containers & Kubernetes / Security / Identity & Access

Design least-privilege Kubernetes RBAC and ServiceAccounts

Derive exact Kubernetes API permissions from observed calls, create a dedicated ServiceAccount with no ambient token, bind a narrow namespaced Role, mount a short-lived audience-bound token only where needed, and prove both allowed and denied behavior.

150 min13 stepsChanges system stateRevision 2
Save or explore
Save to collectionCreate a collection in the sidebar first.
0 of 13 steps completed
Goal

Give one workload the minimum Kubernetes API capability required for its function while preventing credential sprawl, wildcard grants, cross-namespace expansion, and indirect privilege escalation through Secrets, workload control, tokens, RBAC, or impersonation.

Supported environments
  • Kubernetes 1.36, current supported
  • RBAC authorization rbac.authorization.k8s.io/v1
  • Bound service account tokens TokenRequest API
Prerequisites
  • Authorization architecture Confirm RBAC and any other API-server authorizers, identity-provider group mappings, admission policies, audit availability, and emergency control-plane access.
  • Observed API requirements Collect the exact allowed verb, API group, resource, subresource, namespace, and resource-name tuples from source, audit logs, or a representative staging run.
  • Desired-state owner Identify the repository, Helm release, GitOps application, and reviewers that own ServiceAccounts, Roles, bindings, and workload specifications.
  • Impersonation tester Use a controlled administrator authorized to impersonate {{serviceAccountName}} for `kubectl auth can-i` tests; do not distribute a workload token.kubectl auth can-i impersonate serviceaccounts -n {{workloadNamespace}}
  • Application token behavior Verify that the Kubernetes client reopens a rotated projected token, validates TLS, does not print credentials, and fails safely when authorization is withdrawn.
  • Incident and audit controls Prepare desired-state revocation, Pod scale-down, audit-log correlation, and token-compromise response before enabling the binding.
Operating boundary

OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.

Full guide

What you will build

System
  • A namespaced workload identity called release-observer with no implicit default-token mount, a narrowly scoped Role for the exact API groups, resources, names, and verbs it requires, and a RoleBinding whose subject and target namespace are explicit.
  • A short-lived projected service-account token with a bounded audience and expiration, mounted only into the Pod that needs Kubernetes API access rather than every Pod using the namespace default identity.
  • A repeatable authorization test matrix that proves required operations succeed and dangerous adjacent operations fail, plus an audit procedure for escalation verbs, Secret access, workload creation, token requests, impersonation, aggregation, and stale bindings.
Observable outcome
  • The intended ServiceAccount can get, list, and watch Deployments and ReplicaSets in {{workloadNamespace}} and get only the named configuration object; it cannot update workloads, read arbitrary Secrets, create Pods, exec into Pods, or access another namespace.
  • A test Pod receives a rotating projected token for the Kubernetes API audience, while ordinary Pods and the default ServiceAccount have automount disabled and no bearer credential appears unless explicitly requested.
  • Operators can trace each effective permission from subject to binding to role rule, detect escalation paths that a simple list of ClusterRoleBindings misses, remove stale access without deleting an unrelated identity, and verify revocation with fresh authorization checks.

Architecture

How the parts fit together

Kubernetes authentication establishes a request identity; authorization decides whether that identity may perform one verb on one API resource, subresource, name, and namespace. A ServiceAccount is a namespaced non-human identity, not a permission bundle. Role and ClusterRole objects contain additive rules, while RoleBinding and ClusterRoleBinding objects attach those rules to users, groups, or ServiceAccounts. There are no deny rules in Kubernetes RBAC, so least privilege requires controlling every grant path and testing negative cases. A RoleBinding grants within its own namespace even when it references a ClusterRole. A ClusterRoleBinding grants cluster-wide. Tokens are bearer credentials: modern projected service-account tokens are short-lived and bound to a Pod, audience, and object lifetime, but they remain usable by anyone who steals them within that scope. Workload creation, Pod exec, Secret reads, token creation, role bind or escalate, certificate approval, and impersonation can all lead to privileges greater than the rule names suggest.

ServiceAccountProvides the namespaced identity system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}}. It receives no application permission until a binding grants a role.
RoleDefines allowed verbs on Deployments, ReplicaSets, and one named ConfigMap inside {{workloadNamespace}} only.
RoleBindingConnects the exact ServiceAccount subject to the exact Role in the target namespace. Its namespace defines where the grant applies.
Projected tokenGives one Pod a time-limited bearer token with an explicit API audience. The kubelet rotates it and object binding helps invalidate it when the Pod is deleted.
Kubernetes authorizerEvaluates all applicable additive grants. One forgotten ClusterRoleBinding or group membership can make a negative test succeed despite a narrow local Role.
Admission and policy controlsConstrain risky resource creation and issuer or workload behavior that RBAC alone cannot express as a deny rule.
Audit and reconciliationRecord authorization-relevant changes, compare desired manifests with live objects, and identify stale subjects, wildcard permissions, and escalation verbs.
  1. Describe the workload's API calls from code, audit logs, and observed failures; separate necessary reads from convenient broad discovery.
  2. Inventory existing ServiceAccounts, tokens, Roles, ClusterRoles, bindings, namespace labels, admission policy, and authorization modes before adding another grant.
  3. Disable implicit token mounting, create a dedicated ServiceAccount, and define one namespaced Role with explicit API groups, resources, verbs, and resourceNames where supported.
  4. Validate manifests with server-side dry run and `kubectl auth reconcile --dry-run=server` before persisting any grant.
  5. Apply the ServiceAccount, Role, and RoleBinding through the owning repository, then prove an allow-and-deny matrix using impersonation from an administrator authorized to impersonate the subject.
  6. Deploy one Pod with an explicit projected token, short expiry, audience, read-only mount, non-root security context, and a minimal image that does not print the credential.
  7. If cross-namespace access is truly required, place a RoleBinding in the destination namespace and bind the source ServiceAccount to a destination Role; never turn the problem into a broad ClusterRoleBinding.
  8. Continuously audit aggregation, wildcards, Secret and workload creation, bind, escalate, impersonate, token, exec, and stale subject paths; remove access by deleting the smallest binding and retest.

Assumptions

  • The API server uses RBAC authorization and the operator knows any additional authorizers or admission controls in the chain. A successful `can-i` result is interpreted in the context of all enabled authorizers.
  • The example workload needs Kubernetes API access. If it only consumes application data, no ServiceAccount token should be mounted and no RBAC grant should be created.
  • The operator can inspect source code, API audit logs, or a representative staging run to enumerate required calls. Guessing broad read permissions is not accepted as requirements discovery.
  • The cluster supports short-lived bound ServiceAccount tokens and projected token volumes. Long-lived Secret-backed tokens are not created.
  • Human access is authenticated through an external identity provider and group mapping. ServiceAccounts are not used as a substitute for long-lived human kubeconfigs.
  • A repository or GitOps owner controls RBAC manifests. Direct emergency changes are recorded and reconciled back to desired state.
  • The operator performing impersonation tests is separately authorized to impersonate the ServiceAccount. Application subjects do not receive impersonation rights.

Key concepts

Verb
The authorization action such as get, list, watch, create, update, patch, delete, bind, escalate, impersonate, or a non-resource URL verb. Similar-sounding verbs have materially different scope.
Resource and subresource
An API object type such as pods or deployments and an optional subresource such as pods/log, pods/exec, or serviceaccounts/token. Subresources need explicit review because they can expose capabilities unlike the parent object.
Role versus ClusterRole
A Role contains rules usable in one namespace. A ClusterRole can contain cluster-scoped or reusable namespaced rules; its binding determines whether the grant is namespace-scoped or cluster-wide.
RoleBinding versus ClusterRoleBinding
A RoleBinding grants permissions only in its namespace. A ClusterRoleBinding applies a ClusterRole to subjects across the cluster and therefore has a much larger blast radius.
resourceNames
An optional restriction to named objects for verbs such as get, update, or delete. It cannot generally make list or watch safe for a single name because those requests authorize collection access.
Projected token
A short-lived JWT issued by the TokenRequest API and mounted by kubelet with an audience, expiry, and binding to an object such as a Pod.
Audience
A token claim identifying the intended recipient. A service must reject a token whose audience does not include that service; API tokens should not become generic credentials for unrelated systems.
Privilege escalation
A path where a subject creates or controls another identity, credential, workload, role, binding, admission object, or privileged action to gain capabilities not obvious from its direct read rules.
Additive authorization
RBAC permissions accumulate from all roles, bindings, and groups. There is no RBAC deny rule that subtracts a permission granted elsewhere.

Before you copy

Values used in this guide

{{workloadNamespace}}

Namespace containing the workload identity, Role, RoleBinding, Pod, and named configuration object.

Example: release-ops
{{serviceAccountName}}

Dedicated non-human identity used only by the release-observer workload.

Example: release-observer
{{roleName}}

Namespaced Role defining the reviewed API permissions.

Example: release-observer-read
{{roleBindingName}}

Namespaced RoleBinding that grants {{roleName}} to {{serviceAccountName}}.

Example: release-observer-read
{{configMapName}}

Single configuration object the observer may get without permission to list all ConfigMaps.

Example: release-observer-settings
{{tokenAudience}}

Intended recipient audience for the projected token. The example uses the Kubernetes API service identity.

Example: https://kubernetes.default.svc
{{destinationNamespace}}

Optional second namespace whose objects require a separate Role and RoleBinding after explicit approval.

Example: release-target

Security and production boundaries

  • Reading Secrets is frequently equivalent to stealing credentials. Workload creation can mount Secrets or powerful ServiceAccounts. Treat both as escalation-capable even when the role name says developer or editor.
  • The `bind` and `escalate` verbs bypass ordinary protections around granting roles. `impersonate` can act as another identity. `serviceaccounts/token` can mint bearer tokens. These verbs require exceptional review.
  • Pod exec, attach, port-forward, ephemeral containers, and logs are subresources with distinct operational and data-exposure risks; grant each only when the workload function truly requires it.
  • Wildcard API groups, resources, or verbs automatically include future resources and capabilities. Avoid them outside tightly controlled platform components.
  • Disabling automount on a ServiceAccount is a safe default, but a Pod specification can still request a projected token. Admission controls may be needed to enforce token policy across untrusted workload authors.
  • A short-lived token reduces exposure duration but does not make exfiltration harmless. Use egress policy, minimal images, read-only filesystems, runtime controls, and audit logs around workloads that hold API credentials.

Stop before continuing if

  • Stop if the workload owner cannot enumerate required API calls or asks for cluster-admin merely to avoid authorization errors.
  • Stop if the cluster authorization modes, external authorizers, or identity-provider group mappings are unknown.
  • Stop if any negative test unexpectedly succeeds; find the other binding or authorizer before deployment.
  • Stop if the application prints tokens, mounts them read-write, copies them into images, or sends them to a recipient with a different audience.
  • Stop if the proposed role includes wildcard verbs or resources, Secret reads, workload creation, token requests, impersonation, bind, or escalate without a separate threat review.
  • Stop if a cross-namespace requirement is solved by a cluster-wide binding without evidence that every namespace needs the access.
  • Stop if desired-state ownership would recreate a revoked binding after an emergency removal.
01

verification

Inventory authorization modes, identities, and existing grants

read-only

Capture the current ServiceAccounts, Roles, ClusterRoles, bindings, default token behavior, and namespace ownership before adding a new subject.

Why this step matters

RBAC grants are additive and can arrive through cluster-wide bindings or identity groups outside the target namespace. A baseline prevents a narrow new Role from masking broader inherited access.

What to understand

Record API-server authorization modes from managed-cluster documentation or control-plane configuration. An external webhook authorizer can allow a request even when RBAC does not.

List namespaced roles and bindings plus cluster-wide bindings whose subjects match the workload namespace or service-account groups.

Inspect the default ServiceAccount and representative Pods for automount behavior. Namespace age and zero Secret count do not alone prove no tokens are projected at runtime.

Store a sanitized baseline of object UIDs, roleRefs, subjects, labels, annotations, and managing fields so later drift can be attributed to an owner.

System changes

  • No cluster changes; creates an external review record of existing identity and authorization state.

Syntax explained

kubectl auth can-i --list
Shows an approximate effective permission inventory for the current identity; it is not a substitute for individual allow and deny tests.
-o wide
Adds roleRef and subject context without dumping token material.
Command
Fill variables0/1 ready

Values stay on this page and are never sent or saved.

kubectl version && kubectl get namespace {{workloadNamespace}} -o yaml && kubectl -n {{workloadNamespace}} get serviceaccount,role,rolebinding -o wide && kubectl get clusterrolebinding -o wide && kubectl auth can-i --list -n {{workloadNamespace}}
Example output / evidence
Client Version: v1.36.0
NAME      SECRETS   AGE
builder   0         61d
No resources found in release-ops namespace.
Resources                                       Non-Resource URLs   Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                  []               [create]

Checkpoint: Known authorization baseline

kubectl -n {{workloadNamespace}} get role,rolebinding,serviceaccount -o yaml

Continue whenEvery existing grant and ServiceAccount has an owner, and no binding already targets {{serviceAccountName}}.

Stop whenAuthorization modes, group mappings, object ownership, or an existing same-name identity cannot be established.

If this step fails

The namespace contains many generated bindings with no obvious owner.

Likely causeHelm, GitOps, an operator, or an identity integration creates RBAC dynamically.

Safe checks
  • kubectl -n {{workloadNamespace}} get rolebinding -o yaml --show-managed-fields
  • helm list -A

ResolutionIdentify the managing system and change desired state there; do not layer a conflicting manual grant.

Security notes

  • Do not use a real workload token to discover permissions. Administrator impersonation and API review endpoints provide evidence without distributing credentials.

Alternatives

  • Perform inventory in a staging namespace with equivalent admission and authorizer configuration before production.

Stop conditions

  • Stop when any unknown cluster-admin or service-account-group binding could affect the test subject.
02

decision

Translate observed API calls into an explicit permission matrix

read-only

Document each required verb, API group, resource, subresource, namespace, and name, then list adjacent operations that must remain denied.

Why this step matters

Least privilege starts from a request tuple, not a friendly role name. Separating required calls from denied neighbors prevents accidental grant of write, Secret, exec, or cluster-scoped capabilities.

What to understand

Use audit logs or a staging run to see exact requests. A client library may perform list and watch even when product requirements mention only get.

Distinguish API groups: Deployments and ReplicaSets use apps, while ConfigMaps use the core group represented by an empty string in RBAC.

Distinguish resources from subresources. Reading pods does not automatically grant pods/log; updating deployments does not grant deployments/scale unless specified.

Record namespace and object-name bounds. The example allows collection reads for release metadata but only `get` on {{configMapName}}.

Write negative expectations first: no Secrets, Pod creation, exec, writes, token minting, impersonation, role mutation, or other namespace access.

System changes

  • No API change; produces the reviewed allow-and-deny requirements artifact used to generate manifests and tests.

Syntax explained

--verbs=list
Filters API discovery to resources supporting list so collection requirements can be mapped accurately.
--api-version=apps/v1
Pins schema explanation to the intended stable API group and version.
Command
kubectl api-resources --verbs=list --namespaced=true && kubectl explain deployment --api-version=apps/v1 && kubectl explain replicaset --api-version=apps/v1
Example output / evidence
NAME          SHORTNAMES   APIVERSION   NAMESPACED   KIND
configmaps    cm           v1           true         ConfigMap
deployments   deploy       apps/v1      true         Deployment
replicasets   rs           apps/v1      true         ReplicaSet
KIND: Deployment
VERSION: apps/v1

Checkpoint: Approved request matrix

Continue whenEvery allow has business justification and every dangerous adjacent operation has a negative test.

Stop whenRequirements contain wildcards, cluster-admin, unknown subresources, or unbounded Secret and workload access.

If this step fails

The application team cannot name its Kubernetes API calls.

Likely causeThe client library behavior was never observed or access was previously developed against cluster-admin.

Safe checks
  • Review application source for Kubernetes client methods
  • Review staging API audit events for the ServiceAccount

ResolutionInstrument a representative staging run under no permissions and derive one denied call at a time; do not guess a broad role.

Security notes

  • Audit logs used for requirements may contain object metadata or request bodies. Apply retention and redaction policy and never collect bearer tokens.

Alternatives

  • Replace direct API reads with mounted ConfigMaps or a narrow internal observer API when only summarized data is required.

Stop conditions

  • No role is authored until the permission and denial matrix is reviewed.
03

config

Disable implicit API credentials on the namespace default identity

caution

Patch desired state for the default ServiceAccount so ordinary Pods do not receive API tokens unless their specifications opt in deliberately.

Why this step matters

Most workloads do not need the Kubernetes API. Removing ambient bearer credentials reduces what an application compromise can steal and makes API access an explicit workload decision.

What to understand

Apply this through the namespace's owning manifests. A direct patch that GitOps reverts gives only temporary protection and misleading audit results.

Changing the ServiceAccount affects newly created Pods; existing Pods keep their mounted volumes until recreated. Inventory workloads before rollout.

Pod-level `automountServiceAccountToken` can override ServiceAccount behavior. Admission policy is necessary where workload authors are not trusted to follow the default.

Image pulls and the Pod's serviceAccountName identity remain available; disabling API token automount is not the same as deleting the ServiceAccount.

System changes

  • Changes the default ServiceAccount desired behavior for future Pod API credential mounts in {{workloadNamespace}}.
  • May require controlled recreation of existing Pods to remove already-mounted projected credentials.

Syntax explained

--type=merge
Adds one scalar field without replacing unrelated ServiceAccount metadata.
automountServiceAccountToken:false
Prevents automatic injection of the default Kubernetes API token unless the Pod explicitly overrides it.
Configuration
Fill variables0/1 ready

Values stay on this page and are never sent or saved.

kubectl -n {{workloadNamespace}} patch serviceaccount default --type=merge -p '{"automountServiceAccountToken":false}' && kubectl -n {{workloadNamespace}} get serviceaccount default -o yaml
Example output / evidence
serviceaccount/default patched
apiVersion: v1
automountServiceAccountToken: false
kind: ServiceAccount
metadata:
  name: default
  namespace: release-ops

Checkpoint: Default identity has no ambient token

kubectl -n {{workloadNamespace}} get serviceaccount default -o jsonpath='{.automountServiceAccountToken}{"\n"}'

Continue whenPrints false, and a newly created ordinary Pod has no kube-api-access projected volume.

Stop whenAn existing workload depends on ambient API access and has no migration plan or GitOps will immediately revert the field.

If this step fails

A workload fails after recreation.

Likely causeIt implicitly called the API through the default token and had no documented ServiceAccount or Role.

Safe checks
  • kubectl -n {{workloadNamespace}} logs deployment/affected-workload --tail=120
  • kubectl -n {{workloadNamespace}} get pod -o yaml

ResolutionRoll back that workload revision temporarily, derive its exact requirements, and create a dedicated identity rather than restoring ambient tokens namespace-wide.

Security notes

  • This is defense in depth, not a deny rule. A Pod author able to modify specifications can explicitly request a token unless admission policy prevents it.

Alternatives

  • Set automountServiceAccountToken false on every workload template when changing the namespace default is not yet safe.

Stop conditions

  • Stop before disrupting an undocumented controller that relies on the default identity; inventory and migrate it first.
04

config

Create a dedicated ServiceAccount with automount disabled

caution

Define one stable workload identity, disable automatic token injection, and attach ownership labels without embedding credentials.

Why this step matters

A dedicated identity makes policy, audit events, revocation, and ownership attributable to one workload. Disabling automount keeps credential delivery separate from identity creation.

What to understand

The identity string includes namespace and name. Recreating a ServiceAccount with the same name can inherit surviving RoleBindings, so stale binding cleanup matters.

Modern ServiceAccounts normally show zero Secret-backed tokens. Do not manually create a kubernetes.io/service-account-token Secret for convenience.

Labels help inventory ownership but do not grant or restrict access. Authorization comes only from applicable bindings and other authorizers.

Do not reuse this identity across unrelated Deployments or CronJobs. Shared identities make audit attribution and safe revocation impossible.

System changes

  • Creates system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} without any RBAC permission.
  • Establishes a future token principal whose credentials are mounted only by explicit Pod projection.

Syntax explained

metadata.namespace
Forms part of the ServiceAccount identity and must match RoleBinding subject.namespace exactly.
automountServiceAccountToken:false
Makes token use explicit at the Pod level instead of ambient.
File release-observer-serviceaccount.yaml
Configuration
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{serviceAccountName}}
  namespace: {{workloadNamespace}}
  labels:
    app.kubernetes.io/name: release-observer
    app.kubernetes.io/part-of: platform-operations
automountServiceAccountToken: false
Example output / evidence
serviceaccount/release-observer created
NAME               SECRETS   AGE
release-observer   0         1s

Checkpoint: Identity exists with no grant or static Secret

kubectl -n {{workloadNamespace}} get serviceaccount {{serviceAccountName}} -o yaml

Continue whenautomount is false, no token Secret is attached, and no binding references the identity yet.

Stop whenA same-name identity or binding already exists under another owner.

If this step fails

The ServiceAccount immediately shows an unexpected Secret reference.

Likely causeLegacy token automation, another controller, or an existing manifest created a long-lived token Secret.

Safe checks
  • kubectl -n {{workloadNamespace}} get secret -o yaml
  • kubectl -n {{workloadNamespace}} get events --sort-by=.lastTimestamp | tail -n 60

ResolutionIdentify the owner and remove the legacy token path through desired state; do not expose the Secret while investigating.

Security notes

  • ServiceAccount creation is not harmless when stale bindings exist. Audit bindings before and after using a previously used name.

Alternatives

  • Use a different identity per workload component when their required APIs or lifecycle owners differ.

Stop conditions

  • Do not continue if this name would inherit an unknown surviving ClusterRoleBinding.
05

config

Define explicit namespaced read rules and one named ConfigMap read

caution

Create separate rules for apps resources and the exact configuration object, with no wildcard, write verb, Secret, Pod, or subresource permission.

Why this step matters

Separate rule blocks express different scopes accurately: collection reads for release objects and one-object get for configuration. The manifest excludes powerful adjacent resources and verbs by construction.

What to understand

The core API group is represented by an empty string, not `core`. Using the wrong group creates a silent authorization mismatch.

Get, list, and watch are distinct verbs. Watches normally start from a list and then follow changes; grant all three only because the observer genuinely requires collection state.

`resourceNames` restricts the ConfigMap get. Do not add list or watch to that rule because collection authorization would reveal every ConfigMap.

Deployments and ReplicaSets expose operational metadata that may still be sensitive. Namespace scope and application need remain part of least privilege even for reads.

The Role intentionally omits pods, pods/log, secrets, deployments/scale, and all writes. Additions require new evidence and negative tests.

System changes

  • Creates a namespaced Role object containing rules but grants nothing until a binding references it.

Syntax explained

apiGroups:["apps"]
Targets Deployment and ReplicaSet resources in the apps API group.
verbs:[get,list,watch]
Allows reading individual objects, collections, and change streams but no mutation.
resourceNames:[{{configMapName}}]
Restricts ConfigMap get to one reviewed object name.
File release-observer-role.yaml
Configuration
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: {{roleName}}
  namespace: {{workloadNamespace}}
rules:
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["{{configMapName}}"]
    verbs: ["get"]
Example output / evidence
role.rbac.authorization.k8s.io/release-observer-read created
NAME                    CREATED AT
release-observer-read   2026-07-28T13:04:00Z

Checkpoint: Role contains only approved tuples

kubectl -n {{workloadNamespace}} get role {{roleName}} -o yaml

Continue whenTwo explicit rules, no wildcards, no Secrets, no Pods, no write or escalation verbs.

Stop whenAny generated chart broadens rules, aggregation is introduced, or resourceNames is combined with list/watch.

If this step fails

The Role is accepted but the application later cannot read a resource.

Likely causeRequirements missed an exact API group, subresource, or verb.

Safe checks
  • kubectl -n {{workloadNamespace}} describe role {{roleName}}
  • Review the denied API audit event

ResolutionAdd only the specific reviewed tuple and a new negative-neighbor test; never replace the role with view or cluster-admin.

Security notes

  • Read access to workload specs can expose environment variable names, image locations, and architecture. Do not assume all reads are non-sensitive.

Alternatives

  • Use a custom aggregated data service if collection reads expose more metadata than the observer should receive.

Stop conditions

  • Reject wildcard or Secret access and any verb not present in the approved matrix.
06

config

Bind the exact Role to the exact ServiceAccount in one namespace

caution

Create a RoleBinding whose roleRef and subject identify one namespaced role and one workload identity, then inspect the immutable reference.

Why this step matters

The binding is the actual grant. Explicit subject namespace and Role kind prevent accidental attachment to a similarly named identity or broader ClusterRole.

What to understand

A RoleBinding's namespace defines where permissions apply. The ServiceAccount may be referenced from another namespace, but that does not change the target scope.

roleRef is immutable. To change role kind or name, replace the binding through desired state rather than patching around API validation.

A RoleBinding can reference a ClusterRole and still grant only within the binding namespace. This is useful for reuse but not needed for one custom role.

Do not bind broad built-in roles merely because the resource name looks familiar. Effective rules, aggregation, and future changes matter.

System changes

  • Grants {{serviceAccountName}} the rules in {{roleName}} within {{workloadNamespace}}.

Syntax explained

subjects[].namespace
Disambiguates the namespaced ServiceAccount identity.
roleRef.kind: Role
References the custom namespaced rule set, not a cluster-scoped built-in role.
File release-observer-rolebinding.yaml
Configuration
Fill variables0/4 ready

Values stay on this page and are never sent or saved.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: {{roleBindingName}}
  namespace: {{workloadNamespace}}
subjects:
  - kind: ServiceAccount
    name: {{serviceAccountName}}
    namespace: {{workloadNamespace}}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: {{roleName}}
Example output / evidence
rolebinding.rbac.authorization.k8s.io/release-observer-read created
NAME                    ROLE                         AGE
release-observer-read   Role/release-observer-read  1s

Checkpoint: One subject, one role, one target namespace

kubectl -n {{workloadNamespace}} describe rolebinding {{roleBindingName}}

Continue whenSubject is {{workloadNamespace}}/{{serviceAccountName}} and Role is {{roleName}}.

Stop whenThe subject is a group, the default ServiceAccount, another namespace, or roleRef points to a broader role.

If this step fails

Applying an update reports roleRef is immutable.

Likely causeDesired state changed the referenced role kind or name.

Safe checks
  • kubectl -n {{workloadNamespace}} get rolebinding {{roleBindingName}} -o yaml
  • kubectl diff -f release-observer-rolebinding.yaml

ResolutionReview the new grant, then replace only the binding through the owning system and rerun the authorization matrix.

Security notes

  • Changing subjects is authorization-sensitive even when roleRef stays constant. Require review of every subject addition.

Alternatives

  • Reference a reviewed reusable ClusterRole from selected RoleBindings when multiple namespaces need identical namespaced rules.

Stop conditions

  • Do not create a ClusterRoleBinding for a one-namespace requirement.
07

verification

Validate desired RBAC with server-side dry run and reconciliation

read-only

Submit the exact manifest set to admission without persisting drift, inspect the proposed reconciliation, and review differences before apply.

Why this step matters

Server-side dry run exercises schema, admission, and current-object conflicts. Authorization reconciliation reveals RBAC changes before they become effective and is safer than trial grants.

What to understand

Use the same files and applying identity intended for deployment. A copied manifest or administrator dry run does not prove the automation actor can apply safely.

Review the full proposed Role rules and subjects, not only the successful exit code. Admission validates structure, not business least privilege.

Server-side apply field ownership can conflict with Helm or GitOps. Managed-field conflicts are ownership evidence, not a reason to force.

`kubectl auth reconcile` can create or update RBAC; dry-run=server is essential during review. Persist changes only through the authoritative owner.

System changes

  • No persistent change; the API server validates proposed objects and reconciliation against live state.

Syntax explained

--server-side --dry-run=server
Exercises API-server field management and admission while guaranteeing the object is not stored.
kubectl auth reconcile
Compares RBAC desired state with live grants; without dry run it can mutate authorization.
Command
kubectl apply --server-side --dry-run=server -f release-observer-rbac.yaml -o yaml && kubectl auth reconcile -f release-observer-rbac.yaml --dry-run=server
Example output / evidence
serviceaccount/release-observer serverside-applied (server dry run)
role.rbac.authorization.k8s.io/release-observer-read serverside-applied (server dry run)
rolebinding.rbac.authorization.k8s.io/release-observer-read serverside-applied (server dry run)
rolebinding.rbac.authorization.k8s.io/release-observer-read reconciled (server dry run)

Checkpoint: Exact desired grant passes admission with no ownership surprise

kubectl diff -f release-observer-rbac.yaml

Continue whenDiff contains only the reviewed ServiceAccount, Role rules, and RoleBinding subject.

Stop whenField ownership conflicts, admission denial, wildcard expansion, or unexpected subject changes appear.

If this step fails

Dry run reports a field-manager conflict.

Likely causeAnother deployment owner manages the same RBAC object or field.

Safe checks
  • kubectl -n {{workloadNamespace}} get role {{roleName}} -o yaml --show-managed-fields
  • kubectl -n {{workloadNamespace}} get rolebinding {{roleBindingName}} -o yaml --show-managed-fields

ResolutionChange the authoritative source or choose a distinct object; never force ownership before coordination.

Security notes

  • Dry run does not prove the role is safe. Human review and a negative authorization matrix remain mandatory.

Alternatives

  • Use policy-as-code validation in CI in addition to, not instead of, server-side admission.

Stop conditions

  • Do not persist a manifest whose live diff includes unrelated role or subject removal.
08

verification

Prove required allows and dangerous denials with impersonation

read-only

Run individual authorization checks as the ServiceAccount for every required and forbidden operation before deploying a token-bearing Pod.

Why this step matters

Positive tests prove usability; negative tests prove boundaries. Both are required because a hidden binding can silently broaden effective access beyond the custom Role.

What to understand

Test exact resource names and subresources rather than relying on `--list`. Each result is easier to review and maps directly to the approved matrix.

Use an administrator separately authorized to impersonate the subject. Do not grant impersonation to the workload or retrieve its token for testing.

Include writes, Secrets, Pods, exec, logs, token requests, role changes, impersonation, and another namespace in the denial set.

Run tests after every binding or group change. RBAC results are live and additive; a previously safe Role can become effectively broad without changing its YAML.

Store the matrix and results in change evidence. A sequence with expected yes/no values is an executable security contract.

System changes

  • No resource changes; sends authorization review requests evaluated as the workload subject.

Syntax explained

--as=system:serviceaccount:namespace:name
Uses Kubernetes impersonation to evaluate the named ServiceAccount without exposing its token.
resource/name
Tests object-name authorization such as the one permitted ConfigMap.
Command
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get deployments.apps -n {{workloadNamespace}} && kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} watch replicasets.apps -n {{workloadNamespace}} && kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get configmap/{{configMapName}} -n {{workloadNamespace}} && kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get secrets -n {{workloadNamespace}} && kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} create pods -n {{workloadNamespace}}
Example output / evidence
yes
yes
yes
no
no

Checkpoint: Allow and deny matrix matches exactly

kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} --list -n {{workloadNamespace}}

Continue whenRequired reads are yes; Secrets, writes, Pod creation, exec, token minting, RBAC mutation, impersonation, and other namespace access are no.

Stop whenAny expected denial returns yes or required operation returns no without a documented reason.

If this step fails

All checks return no, including known grants.

Likely causeThe tester lacks impersonation permission, the subject string is wrong, or the RoleBinding is not in the expected namespace.

Safe checks
  • kubectl auth can-i impersonate serviceaccounts -n {{workloadNamespace}}
  • kubectl -n {{workloadNamespace}} describe rolebinding {{roleBindingName}}

ResolutionCorrect the tester or exact subject and binding; never use the workload's bearer token as a shortcut.

Security notes

  • Impersonation is powerful and should be limited to controlled administrators and audit contexts. Each impersonated request should be auditable.

Alternatives

  • Use SelfSubjectAccessReview inside a representative test workload when administrator impersonation is unavailable, without exporting the token.

Stop conditions

  • Deployment is blocked on any unexpected yes in the negative matrix.
09

config

Mount one short-lived, audience-bound token into the workload

caution

Deploy the observer with automount disabled and an explicit projected token, read-only filesystem, non-root user, and bounded expiration.

Why this step matters

Explicit projection makes the credential recipient, path, audience, lifetime, and workload identity visible in the Pod specification while preserving automatic kubelet rotation.

What to understand

The sample image is a placeholder for a reviewed application image. Pin an immutable digest under the organization's supply-chain policy before production.

The token file is read-only and resides in a projected volume. The application must reopen it as kubelet rotates content atomically before expiry.

The audience must match the intended recipient. A Kubernetes API token should not be accepted by an unrelated internal service.

The security context limits common post-compromise actions but cannot prevent a compromised process from reading its own API token. Least RBAC and network egress controls remain essential.

A one-hour expiry is a balance, not a universal value. Shorter lifetimes reduce exposure but require reliable client refresh and API availability.

System changes

  • Creates a Pod that requests a bound, rotating ServiceAccount token for {{tokenAudience}}.
  • Introduces a live bearer credential inside the observer container and corresponding TokenRequest activity.

Syntax explained

automountServiceAccountToken:false
Prevents the default token volume so only the explicit projection exists.
audience: {{tokenAudience}}
Scopes the token to a recipient that validates the matching aud claim.
expirationSeconds:3600
Requests a one-hour token subject to API-server limits and kubelet rotation.
allowPrivilegeEscalation:false
Prevents gaining additional Linux privileges through setuid or similar process behavior.
File release-observer-pod.yaml
Configuration
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

apiVersion: v1
kind: Pod
metadata:
  name: release-observer
  namespace: {{workloadNamespace}}
  labels:
    app.kubernetes.io/name: release-observer
spec:
  serviceAccountName: {{serviceAccountName}}
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: observer
      image: registry.k8s.io/e2e-test-images/agnhost:2.53
      args: ["pause"]
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: api-token
          mountPath: /var/run/secrets/tokens
          readOnly: true
  volumes:
    - name: api-token
      projected:
        sources:
          - serviceAccountToken:
              audience: {{tokenAudience}}
              expirationSeconds: 3600
              path: api-token
Example output / evidence
pod/release-observer created
pod/release-observer condition met
NAME               READY   STATUS    RESTARTS   AGE
release-observer   1/1     Running   0          8s

Checkpoint: Only one explicit token projection exists

kubectl -n {{workloadNamespace}} get pod release-observer -o yaml && kubectl -n {{workloadNamespace}} wait --for=condition=Ready pod/release-observer --timeout=120s

Continue whenPod is Ready, uses {{serviceAccountName}}, has no kube-api-access default volume, and mounts one read-only projected token for {{tokenAudience}}.

Stop whenThe image is unreviewed, token is logged, default projection also exists, Pod runs privileged, or expected denials changed.

If this step fails

Pod remains ContainerCreating with projected volume errors.

Likely causeTokenRequest, kubelet, ServiceAccount existence, audience policy, or admission validation failed.

Safe checks
  • kubectl -n {{workloadNamespace}} describe pod release-observer
  • kubectl -n {{workloadNamespace}} get events --sort-by=.lastTimestamp | tail -n 60

ResolutionCorrect the specific projection or platform failure; do not create a long-lived token Secret.

Security notes

  • Never `cat` the token during validation. Inspect Pod spec, projected volume metadata, authorization results, and application behavior instead.

Alternatives

  • Use no token at all when the workload can receive required data through a ConfigMap mount or narrow service.

Stop conditions

  • Stop if the application cannot rotate token reads or sends the token to any endpoint other than the reviewed audience.
10

verification

Verify runtime identity, required API behavior, and denied operations

read-only

Observe the Pod identity and application calls, then correlate Kubernetes audit records without printing the projected token.

Why this step matters

Manifest review and impersonation must be connected to the actual running Pod identity. Runtime checks catch wrong serviceAccountName, mutation, and deployment drift without extracting credentials.

What to understand

Inspect the final admitted Pod, not only the template. Mutating webhooks can add volumes, sidecars, or security context changes.

Correlate application request failures with audit events by username, namespace, verb, resource, and response code. Exclude request headers and tokens.

Repeat high-risk negative tests after deployment to detect inherited group or cluster binding changes that occurred between review and rollout.

Observe token refresh behavior over longer than one token lifetime in staging. The application should continue without 401 spikes and without restarting solely to refresh credentials.

System changes

  • No intentional state change; reads Pod identity and sends authorization review requests.

Syntax explained

custom-columns
Shows the admitted ServiceAccount, node, and readiness without exposing environment or token content.
update deployment
Tests a deliberately forbidden adjacent operation to prove read-only scope remains effective.
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

kubectl -n {{workloadNamespace}} get pod release-observer -o custom-columns=NAME:.metadata.name,SA:.spec.serviceAccountName,NODE:.spec.nodeName,READY:.status.containerStatuses[0].ready && kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get deployment -n {{workloadNamespace}} && kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} update deployment -n {{workloadNamespace}}
Example output / evidence
NAME               SA                 NODE       READY
release-observer   release-observer   worker-2   true
yes
no

Checkpoint: Runtime subject matches reviewed policy

kubectl -n {{workloadNamespace}} get pod release-observer -o jsonpath='{.spec.serviceAccountName}{" "}{.spec.automountServiceAccountToken}{"\n"}'

Continue whenShows {{serviceAccountName}} and false; required reads succeed and writes remain forbidden.

Stop whenAdmitted Pod differs from reviewed spec, audit username differs, or any negative operation succeeds.

If this step fails

Audit events show requests from the default ServiceAccount.

Likely causeThe deployed workload omitted serviceAccountName or another Pod performs the calls.

Safe checks
  • kubectl -n {{workloadNamespace}} get pod -o custom-columns=NAME:.metadata.name,SA:.spec.serviceAccountName
  • kubectl -n {{workloadNamespace}} get deployment -o yaml

ResolutionCorrect the workload template, stop the unintended caller, and rerun the complete matrix.

Security notes

  • Avoid debug commands that print environment variables, mounts, or token files into shared logs. Use identity metadata and authorization APIs.

Alternatives

  • Use a purpose-built conformance test container in staging, then remove it before production if the application exposes sufficient metrics.

Stop conditions

  • Stop on any admitted mutation that broadens token or Linux privilege.
11

config

Grant one optional destination namespace without cluster-wide binding

caution

When evidence requires cross-namespace reads, create a Role and RoleBinding in that destination that reference the source ServiceAccount explicitly.

Why this step matters

Cross-namespace access is still a set of destination-scoped grants. A RoleBinding in each approved destination preserves boundaries and avoids turning a few namespaces into all-cluster access.

What to understand

The subject namespace remains {{workloadNamespace}}, while metadata.namespace and Role namespace are {{destinationNamespace}}. These fields have different purposes.

The destination owner should review the grant because its object metadata becomes visible to an external namespace's workload.

Do not include Secrets, Pods, logs, or writes unless separately justified. Reusing the source Role blindly can leak destination-specific resources.

Test an approved destination as yes and an unapproved namespace as no. A ClusterRoleBinding elsewhere can invalidate this boundary.

System changes

  • Creates a Role and binding in {{destinationNamespace}} granting selected Deployment reads to a ServiceAccount from {{workloadNamespace}}.

Syntax explained

metadata.namespace: {{destinationNamespace}}
Defines where the Role rules apply and where the binding grants them.
subjects[].namespace: {{workloadNamespace}}
Identifies the source namespace of the ServiceAccount receiving access.
File release-observer-cross-namespace.yaml
Configuration
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: release-observer-target-read
  namespace: {{destinationNamespace}}
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: release-observer-target-read
  namespace: {{destinationNamespace}}
subjects:
  - kind: ServiceAccount
    name: {{serviceAccountName}}
    namespace: {{workloadNamespace}}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: release-observer-target-read
Example output / evidence
role.rbac.authorization.k8s.io/release-observer-target-read created
rolebinding.rbac.authorization.k8s.io/release-observer-target-read created
yes
no

Checkpoint: Only approved destination is readable

kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} list deployments.apps -n {{destinationNamespace}}

Continue whenReturns yes only for {{destinationNamespace}}; the same test in any unbound namespace returns no.

Stop whenRequirement actually spans all namespaces, destination owner has not approved, or unrelated access becomes effective.

If this step fails

The source ServiceAccount still cannot read the destination.

Likely causeBinding subject namespace, destination namespace, roleRef, API group, or resource differs from the request.

Safe checks
  • kubectl -n {{destinationNamespace}} describe rolebinding release-observer-target-read
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} list deployments.apps -n {{destinationNamespace}}

ResolutionCorrect the exact field mismatch while preserving destination scope; do not escalate to ClusterRoleBinding.

Security notes

  • Destination namespace owners must understand that a compromised source workload can use every granted read until the binding is removed.

Alternatives

  • Expose a narrow API or copied summary instead of cross-namespace Kubernetes access when data ownership requires stronger separation.

Stop conditions

  • Do not deploy this optional step unless cross-namespace access is evidenced and approved.
12

verification

Audit wildcard, Secret, workload, token, bind, escalate, and impersonation paths

read-only

Search effective role rules and bindings for capabilities that can indirectly exceed the intended read-only workload function.

Why this step matters

Direct read rules are only one part of effective power. Creating workloads, reading credentials, changing authorization, or minting and impersonating identities can bypass the intended boundary.

What to understand

Search is a triage aid, not a parser. Review structured YAML, subjects, namespaces, aggregation labels, and exact rules for every candidate.

Secrets and service-account token subresources are credential paths. Pod create can mount credentials. Exec and ephemeral-container rights can enter another workload's trust boundary.

`bind` permits attaching roles the caller may not otherwise grant; `escalate` permits creating or updating roles beyond the caller's own permissions; `impersonate` changes request identity.

Aggregated ClusterRoles can gain new rules when labeled roles appear. Review aggregation selectors and monitor effective rules over time.

CertificateSigningRequest approval, webhook configuration, namespace mutation, and admission policy can create less obvious cluster escalation and require separate platform review.

System changes

  • No state changes; produces a candidate list and explicit authorization results for manual privilege-path review.

Syntax explained

serviceaccounts/token
The TokenRequest subresource can mint credentials for ServiceAccounts and must be reviewed separately from ServiceAccount reads.
bind / escalate / impersonate
Authorization-control verbs capable of granting or assuming permissions beyond ordinary resource CRUD.
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

kubectl get clusterrole,role -A -o yaml | grep -nE '(^| )(\*|secrets|serviceaccounts/token|pods/exec|pods/attach|impersonate|escalate|bind)( |$)' || true && kubectl get clusterrolebinding,rolebinding -A -o yaml | grep -n '{{serviceAccountName}}' || true && kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} create serviceaccounts/token -n {{workloadNamespace}}
Example output / evidence
842:    resources:
843:    - secrets
1193:    - bind
1194:    - escalate
no

Checkpoint: No indirect privilege path for release-observer

kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} create pods -n {{workloadNamespace}} && kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get secrets -n {{workloadNamespace}}

Continue whenBoth return no, as do token, exec, RBAC mutation, bind, escalate, impersonate, and cluster-scoped operations.

Stop whenAny indirect capability is effective or an applicable aggregation path is unexplained.

If this step fails

The grep output is huge and cannot identify applicable rules.

Likely causeCluster-wide triage lacks structured subject-to-binding-to-role correlation.

Safe checks
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} --list -n {{workloadNamespace}}
  • kubectl get rolebinding,clusterrolebinding -A -o yaml

ResolutionStart from every binding matching the exact subject and its groups, resolve each roleRef, then test risky verbs individually.

Security notes

  • Do not assume a role named view or read-only is safe. Inspect resolved rules and aggregation.

Alternatives

  • Use a maintained RBAC analysis tool in CI, but preserve direct API authorization tests as authoritative runtime evidence.

Stop conditions

  • Deployment cannot proceed with an unexplained effective escalation path.
13

instruction

Rehearse revocation, token expiry, review, and desired-state cleanup

read-only

Remove the smallest binding in staging, prove fresh requests fail, restore only through reviewed desired state, and establish recurring ownership and expiry review.

Why this step matters

Access is operationally safe only when responders can revoke it predictably and the deployment owner does not silently recreate it. Rehearsal also exposes how short-lived tokens behave after authorization changes.

What to understand

Deleting a RoleBinding removes its grant immediately for new authorization decisions; an already authorized API request or watch may have connection-lifetime behavior that needs application testing.

Deleting the ServiceAccount also invalidates bound tokens through object binding, but it is a broader identity action. Remove the smallest binding first when the incident concerns one permission set.

Short token expiry does not delay RBAC revocation: the API server evaluates current authorization on each request. Expiry limits credential authentication duration after identity removal or theft.

Change desired state before or with emergency deletion so GitOps does not restore access. Record the incident, owner, reason, and follow-up review.

Periodic review must detect nonexistent subjects, wildcard drift, aggregation changes, unused permissions, shared identities, and exceptional grants without owners.

System changes

  • The demonstrated command is server-side dry run only; a real incident procedure would remove the selected RoleBinding and alter desired state.

Syntax explained

delete rolebinding
Withdraws one attachment while leaving the Role and ServiceAccount available for forensic review and controlled restoration.
--dry-run=server -o yaml
Validates deletion target and admission without performing revocation during rehearsal.
Command
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

kubectl -n {{workloadNamespace}} delete rolebinding {{roleBindingName}} --dry-run=server -o yaml && kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get deployments.apps -n {{workloadNamespace}} && kubectl -n {{workloadNamespace}} get role,rolebinding,serviceaccount --show-labels
Example output / evidence
rolebinding.rbac.authorization.k8s.io/release-observer-read deleted (server dry run)
yes
NAME                                                        LABELS
serviceaccount/release-observer                             app.kubernetes.io/name=release-observer
role.rbac.authorization.k8s.io/release-observer-read        <none>
rolebinding.rbac.authorization.k8s.io/release-observer-read <none>

Checkpoint: Revocation path and owner are documented

Continue whenA real staging rehearsal makes required can-i checks return no after binding removal and desired state does not recreate it unexpectedly.

Stop whenAnother binding preserves the permission, GitOps restores the grant, or the application cannot fail safely when API access disappears.

If this step fails

Permission remains after the intended binding is removed.

Likely causeAnother binding, group, or authorizer grants the same tuple.

Safe checks
  • kubectl get rolebinding,clusterrolebinding -A -o yaml
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get deployments.apps -n {{workloadNamespace}}

ResolutionTrace and remove the remaining grant through its owner. Do not delete the whole namespace as an authorization shortcut.

Security notes

  • Keep emergency revocation permission tightly controlled and audited; the ability to delete bindings can also cause widespread denial of service.

Alternatives

  • Scale the workload to zero in addition to binding removal when runtime exfiltration or active misuse is suspected.

Stop conditions

  • Do not claim revocation readiness until fresh allow tests actually become denied in staging.

Finish line

Verification checklist

Dedicated identitykubectl -n {{workloadNamespace}} get serviceaccount {{serviceAccountName}} -o jsonpath='{.metadata.name}{" "}{.automountServiceAccountToken}{"\n"}'Shows {{serviceAccountName}} and false with no long-lived token Secret.
Exact role and bindingkubectl -n {{workloadNamespace}} get role/{{roleName}} rolebinding/{{roleBindingName}} -o yamlRules and subject match the reviewed manifest with no wildcard, write, Secret, Pod, token, or escalation capability.
Required readskubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} watch deployments.apps -n {{workloadNamespace}}Returns yes for every approved Deployment and ReplicaSet read plus exact ConfigMap get.
Dangerous denialskubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get secrets -n {{workloadNamespace}}Returns no, as do writes, Pod creation, exec, token requests, RBAC mutation, impersonation, cluster scope, and unapproved namespaces.
Explicit projected tokenkubectl -n {{workloadNamespace}} get pod release-observer -o yamlPod uses {{serviceAccountName}}, automount=false, one read-only projected token for {{tokenAudience}}, bounded expiry, and a restricted security context.
Revocationkubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get deployments.apps -n {{workloadNamespace}}After a real staging binding-removal rehearsal, a fresh request returns no and desired state does not recreate the grant until approved.

Recovery guidance

Common problems and safe checks

The application receives 403 Forbidden for one required API call.

Likely causeThe Role omits the exact API group, resource, subresource, namespace, name, or verb, or the request uses a different ServiceAccount than expected.

Safe checks
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get deployments.apps -n {{workloadNamespace}}
  • kubectl -n {{workloadNamespace}} get pod -o custom-columns=NAME:.metadata.name,SA:.spec.serviceAccountName
  • kubectl -n {{workloadNamespace}} describe rolebinding {{roleBindingName}}

ResolutionExtract the exact denied request from audit or application logs, add only that justified tuple to staging, and rerun both positive and adjacent negative tests.

A negative `can-i` check unexpectedly returns yes.

Likely causeAnother RoleBinding, ClusterRoleBinding, group, built-in discovery grant, or external authorizer supplies the permission.

Safe checks
  • kubectl get rolebinding -A -o yaml
  • kubectl get clusterrolebinding -o yaml
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} --list -n {{workloadNamespace}}

ResolutionTrace every applicable subject and binding, remove or narrow the unintended grant through its owner, and repeat the test with a fresh request.

The Pod has no token at the expected projected path.

Likely causeThe projected volume or mount is absent, serviceAccountName is wrong, TokenRequest is denied, audience is invalid, or kubelet failed volume setup.

Safe checks
  • kubectl -n {{workloadNamespace}} describe pod release-observer
  • kubectl -n {{workloadNamespace}} get pod release-observer -o yaml
  • kubectl -n {{workloadNamespace}} get events --sort-by=.lastTimestamp | tail -n 60

ResolutionCorrect the explicit Pod projection and identity. Do not turn global automount back on to mask a malformed workload.

The token authenticates to Kubernetes but an external service rejects it.

Likely causeThe projected audience identifies the Kubernetes API rather than the external recipient, or the service validates audience and issuer correctly.

Safe checks
  • kubectl -n {{workloadNamespace}} describe pod release-observer
  • kubectl auth can-i create serviceaccounts/token -n {{workloadNamespace}}

ResolutionUse a separate projected token with the exact external audience only if that service has an intentional trust relationship; never disable audience checking.

A Role with resourceNames still allows more list results than expected.

Likely causeList and watch authorize a collection and cannot be constrained to one object by resourceNames in the same way as get.

Safe checks
  • kubectl -n {{workloadNamespace}} get role {{roleName}} -o yaml
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} list configmaps -n {{workloadNamespace}}

ResolutionRemove list or watch for that resource, use get on an exact resourceName, or redesign the application to consume mounted configuration.

Cross-namespace access works in one destination but not another.

Likely causeRoleBindings are namespaced; only the destination containing the binding grants the referenced rules.

Safe checks
  • kubectl -n {{destinationNamespace}} get role,rolebinding -o yaml
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} get deployments.apps -n {{destinationNamespace}}

ResolutionCreate the reviewed RoleBinding only in each approved destination. Do not replace it with an all-cluster binding.

RoleBinding subject looks correct but access is denied.

Likely causeThe subject namespace is omitted or wrong, roleRef points to another kind or name, or the application uses the default ServiceAccount.

Safe checks
  • kubectl -n {{workloadNamespace}} get rolebinding {{roleBindingName}} -o yaml
  • kubectl -n {{workloadNamespace}} get pod release-observer -o jsonpath='{.spec.serviceAccountName}'

ResolutionCorrect the immutable roleRef by replacing the binding through desired state and set the exact subject namespace and Pod serviceAccountName.

A removed binding reappears minutes later.

Likely causeGitOps, Helm, an operator, or automation still declares it.

Safe checks
  • kubectl -n {{workloadNamespace}} get rolebinding {{roleBindingName}} -o yaml --show-managed-fields
  • kubectl -n {{workloadNamespace}} get events --field-selector involvedObject.name={{roleBindingName}}

ResolutionRemove or suspend the binding in the authoritative desired-state system, record emergency action, and verify it remains absent.

A supposedly read-only principal can change workloads indirectly.

Likely causeIt can patch a controller, create Pods, bind a powerful role, mint a token, edit an admission object, or read a credential used by a privileged controller.

Safe checks
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} --list -n {{workloadNamespace}}
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} create pods -n {{workloadNamespace}}
  • kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}} create serviceaccounts/token -n {{workloadNamespace}}

ResolutionRemove the indirect capability and assess actions already taken. Role names and read-looking business functions do not override effective privilege paths.

After token rotation, the application returns intermittent 401 errors.

Likely causeThe application reads the token once and does not reopen the projected file after kubelet atomically rotates it.

Safe checks
  • kubectl -n {{workloadNamespace}} describe pod release-observer
  • kubectl -n {{workloadNamespace}} logs release-observer --tail=120

ResolutionUpdate the client library or token reader to reopen the projected file and refresh authentication before expiry; do not extend tokens indefinitely.

Audit finds a binding to a ServiceAccount that no longer exists.

Likely causeWorkload deletion did not remove separately managed RBAC, leaving stale authorization state that could affect a recreated identity with the same name.

Safe checks
  • kubectl get rolebinding,clusterrolebinding -A -o yaml
  • kubectl -n {{workloadNamespace}} get serviceaccount {{serviceAccountName}}

ResolutionConfirm ownership and delete the stale binding through desired state. Treat recreation of the same ServiceAccount name as inheriting any surviving bindings.

The default ServiceAccount token appears in Pods despite namespace policy.

Likely causePod-level automount overrides the ServiceAccount setting, a mutating webhook changes the spec, or another ServiceAccount is selected.

Safe checks
  • kubectl -n {{workloadNamespace}} get pod -o yaml
  • kubectl -n {{workloadNamespace}} get serviceaccount -o yaml
  • kubectl get mutatingwebhookconfiguration

ResolutionFix the workload template and mutation source, then enforce the intended rule with admission policy for namespaces where authors are not trusted.

A server-side dry run succeeds but production apply is denied.

Likely causeThe applying identity, field manager, object state, admission context, or immutable roleRef differs from the dry run.

Safe checks
  • kubectl -n {{workloadNamespace}} get role,rolebinding -o yaml --show-managed-fields
  • kubectl auth can-i update rolebindings.rbac.authorization.k8s.io -n {{workloadNamespace}}

ResolutionCompare the exact actor and live object, replace immutable bindings through the owner when necessary, and preserve the admission denial reason.

After the procedure

Alternatives and next steps

Consider these alternatives

  • If the workload does not call Kubernetes, set automountServiceAccountToken false and create no Role or binding.
  • Use workload identity federation for cloud APIs instead of storing cloud keys in Kubernetes Secrets or reusing Kubernetes API tokens as cloud credentials.
  • Expose a narrow internal service rather than granting many workloads direct API access when one audited controller can perform the operation safely.
  • Use a destination-namespace RoleBinding to a reusable ClusterRole when identical namespaced read rules are needed in selected namespaces; avoid a ClusterRoleBinding.
  • Use an admission policy or policy engine to reject wildcard grants, default-ServiceAccount use, long-lived token Secrets, or disallowed ClusterRoleBindings before they enter the cluster.

Operate it safely

  • Generate a periodic authorization review from live bindings, identity-provider groups, and desired state; require owners and expiry or review dates for exceptional grants.
  • Deploy admission checks that reject wildcard privileges, cluster-admin bindings, long-lived service-account token Secrets, default-ServiceAccount use, and unapproved privilege-escalation verbs.
  • Enable and protect Kubernetes audit logging with rules that capture RBAC, ServiceAccount, token, impersonation, exec, Secret, and workload-controller activity while excluding credential contents.
  • Use namespace and NetworkPolicy boundaries alongside RBAC. API authorization does not control Pod-to-Pod traffic or protect a token after a compromised workload exfiltrates it.
  • Integrate workload identity federation for external services and remove static cloud credentials from Secrets where supported.

Reference

Frequently asked questions

Is the built-in view ClusterRole always safe?

It is useful for human read-only access but may be broader than one workload needs and can evolve through aggregation. Define a purpose-specific Role and test its exact resources and subresources.

Does automountServiceAccountToken false remove all identity?

It prevents the default API credential mount. The ServiceAccount still identifies the Pod for policy and image pulls, and the Pod can explicitly request a projected token if authorized by its specification.

Why not grant list when the application only gets one object?

List exposes collection metadata and cannot generally be constrained to one resourceName. Grant get on the named object or mount the object as configuration instead.

Can RBAC express a deny rule for Secrets?

No. Kubernetes RBAC is additive. Prevent Secret reads by ensuring no applicable role grants them and use admission, namespace, and organizational controls for broader policy.

Recovery

Rollback

Withdraw the smallest binding first, stop the workload if compromise is suspected, reconcile desired state so access stays removed, and preserve role and audit evidence until the effective-permission matrix proves revocation.

  1. Remove {{roleBindingName}} from the authoritative repository and sync that change; in an active incident, delete the live binding immediately as a recorded emergency action while the desired-state fix proceeds.
  2. Repeat the exact `kubectl auth can-i --as=system:serviceaccount:{{workloadNamespace}}:{{serviceAccountName}}` allow checks and prove they now return no. Trace any surviving grant before continuing.
  3. Scale or delete the release-observer Pod if credential theft or active misuse is suspected. Bound Pod tokens become invalid when their object binding is removed, but preserve audit evidence and monitor failed use.
  4. Remove the optional destination RoleBinding from {{destinationNamespace}} separately; verify no other destination binding or ClusterRoleBinding remains.
  5. Keep the Role temporarily for forensic comparison if safe, then remove unused Role and ServiceAccount objects through desired state after confirming no workload shares the identity.
  6. Restore the prior workload revision or no-API design if the application cannot operate under the minimum role. Do not restore a broad legacy grant merely to recover quickly.
  7. Review API audit events, Secret access, Pod changes, token requests, impersonation, exec, and RBAC modifications for the affected identity and response window; rotate downstream credentials if exposure is possible.

Evidence

Sources and review

Verified 2026-07-24Review due 2026-10-22
Using RBAC authorizationofficialKubernetes RBAC good practicesofficialKubernetes ServiceAccountsofficialProjected service-account token volumesofficialControlling access to the Kubernetes APIofficial