A Kubernetes cluster is full of non-human identities, and almost none of them are people. Every Pod runs as a service account, every node authenticates as a kubelet, controllers and operators call the API server, and deployment pipelines hold cluster credentials. Kubernetes NHI security means knowing which of these identities exist, what they can do, how their credentials are issued and how quickly a stolen one stops working. This guide covers service account tokens, RBAC, Secrets, cloud federation, SPIFFE, admission control, audit logging and the failure patterns seen in real clusters.
Key takeaways
- Every Pod gets an identity whether you plan for it or not: without a named service account, Kubernetes assigns the namespace’s
defaultservice account and, unless told otherwise, mounts a token for it. - Modern clusters issue bound, projected, time-limited tokens through the TokenRequest API. Long-lived Secret-based tokens are legacy and should be found and removed.
- Some RBAC permissions are effectively admin: reading or listing Secrets, creating Pods,
escalate,bind,impersonate,nodes/proxyand wildcards. Treat any service account holding them as privileged. - Kubernetes Secrets are base64-encoded, not encrypted, and stored unencrypted in etcd by default. Configure encryption at rest and keep high-value credentials in an external store.
- Pods should reach cloud APIs through workload identity federation, not static cloud keys stored in Secrets.
The non-human identities in a Kubernetes cluster
The Kubernetes service accounts documentation describes a service account as “a type of non-human account” that provides a distinct identity in the cluster, while human user accounts do not exist in the API server by default: they come from an external authenticator. Most identities the API server actually manages are therefore non-human identities. A useful inventory covers six groups.
- Service accounts. Namespaced, lightweight objects that teams create freely, referenced in RBAC as
system:serviceaccount:<namespace>:<name>. Default RBAC policy grants no permissions to service accounts outsidekube-systembeyond API discovery, so every permission a workload holds was granted by someone and should trace to an owner. - The default service account. Every namespace gets one called
default, recreated by the control plane if deleted. A Pod that does not setspec.serviceAccountNameruns as it, so anything granted todefaultis shared by every such Pod in the namespace. - Node and kubelet identities. Each kubelet authenticates as
system:node:<nodeName>in thesystem:nodesgroup. The Node authoriser limits it to Secrets, ConfigMaps and volumes of Pods on its own node, and theNodeRestrictionadmission plugin limits what it can modify. A compromised node still exposes every credential its Pods use. - Controllers and operators. Third-party operators often ship ClusterRoles that can read every Secret or create Pods in every namespace. An operator’s service account is often the most privileged identity in the cluster and the least reviewed.
- CI/CD and deployment identities. Pipelines hold write access to production through a kubeconfig, a service account token or a cloud identity, and sit outside the cluster where Kubernetes controls cannot protect them.
- External identities used by Pods. Cloud IAM roles, database credentials, API keys, registry pull secrets and SPIFFE IDs. These are often the most valuable credentials in a Pod, and follow the principles in the NHI Authentication Guide.
Service account tokens: legacy versus bound
Service accounts authenticate with signed JSON Web Tokens presented as bearer tokens. How a token was issued determines how dangerous it is when stolen.
Legacy Secret-based tokens
Before Kubernetes v1.24, the control plane automatically created a Secret of type kubernetes.io/service-account-token for each service account, holding a token that never expired or rotated. Automatic generation has stopped, but you can still create such a Secret manually with the kubernetes.io/service-account.name annotation, and the control plane will populate a non-expiring token. The Kubernetes project recommends against this: once disclosed, the token works until someone deletes the Secret.
Bound, projected tokens
By default, the ServiceAccount admission controller adds a projected volume (kube-api-access-<suffix>) to each Pod, and the kubelet fills it using the TokenRequest API. According to Managing Service Accounts, that token is bound to the specific Pod, has the API server as its audience, and expires when the Pod is deleted or after its lifetime (one hour by default). The kubelet refreshes it as it approaches expiry, so applications must reload the file rather than read it once.
You can project extra tokens for other audiences, such as a secrets store or internal service, with a serviceAccountToken projected volume that sets audience and expirationSeconds (at least ten minutes). The API server flag --service-account-max-token-expiration caps lifetimes. From outside a Pod, kubectl create token <serviceaccount> mints a time-limited token through the same API.
Two details matter for defenders. Kubernetes has no specific mechanism to revoke a TokenRequest token: deleting the bound Pod is how you invalidate it. And a service that validates tokens offline through OIDC discovery keeps accepting a token until it expires, even after its Pod is gone. The TokenReview API checks that the bound object still exists, which is why Kubernetes recommends it; every receiving service should also check the audience claim.
Turning automounting off
Most application Pods never call the Kubernetes API, yet by default they carry a token for it. Set automountServiceAccountToken: false on the ServiceAccount, the Pod spec, or both; the Pod spec takes precedence, so a workload that genuinely needs API access can opt back in. This removes the first thing an attacker looks for after compromising a container.
Finding and removing legacy token Secrets
Secrets support a field selector on type, so you can list every long-lived service account token:
kubectl get secrets --all-namespaces --field-selector type=kubernetes.io/service-account-token
From v1.29, a legacy token cleaner in kube-controller-manager handles auto-generated tokens that are not mounted by any Pod: after a period of non-use (one year by default) it adds a kubernetes.io/legacy-token-invalid-since label, and after a further unused period it deletes the Secret. Manually created token Secrets are not cleaned up this way. Review each one, identify its consumer (often an external system or pipeline), move that consumer to short-lived tokens or federation, then delete the Secret. This is offboarding work and belongs in your NHI lifecycle management process.
| Token type | How it is issued | Lifetime | Revocation | Recommendation |
|---|---|---|---|---|
| Legacy Secret-based token | Secret of type kubernetes.io/service-account-token, auto-generated before v1.24 or created manually | No expiry | Delete the Secret | Find and remove; do not create new ones |
| Default projected token | Kubelet via TokenRequest, audience is the API server | One hour by default, refreshed by the kubelet | Expires, or delete the Pod | Disable automounting where the API is not needed |
| Custom projected token | Projected volume with an explicit audience | Set by expirationSeconds, minimum ten minutes | Expires, or delete the Pod | Preferred for external systems that trust the cluster issuer |
| Client TokenRequest | kubectl create token or the API | Requested duration, within API server limits | Expires, or delete the bound object | Use instead of static tokens for access from outside the cluster |
RBAC for non-human identities
A Role holds permissions within one namespace; a ClusterRole can cover cluster-scoped resources or any namespace. A RoleBinding grants a Role or ClusterRole within one namespace; a ClusterRoleBinding grants a ClusterRole across the whole cluster. Permissions are purely additive, with no deny rules, so the only way to restrict an identity is not to grant it.
The Kubernetes RBAC good practices set the core rules:
- Prefer RoleBindings to ClusterRoleBindings.
- Avoid wildcards, especially for resources: because Kubernetes is extensible, a wildcard also covers resource types created in future.
- Use
cluster-adminonly where specifically needed, and never add identities tosystem:masters, whose members bypass RBAC and cannot be restricted by removing bindings. - Give each application its own service account rather than granting roles to
default. - Review bindings periodically. If a deleted identity’s name is recreated, it inherits any bindings left behind, so removing an identity must include removing its bindings.
Permissions that amount to privilege escalation
Flag any service account that holds:
- Secrets
get,listorwatch.listandwatchreturn full Secret contents, not just names. - Workload creation. Creating Pods, or Deployments, Jobs and other objects that create Pods, implicitly grants access to every Secret, ConfigMap and volume in the namespace and to the permissions of every service account there, since a Pod can run as any of them. The built-in
editrole allows this. escalate,bindandimpersonate. These bypass the protections that normally stop an identity granting itself more than it has.nodes/proxy. Evengetreaches the kubelet API, which can execute commands in any Pod on the node and bypasses audit logging and admission control.createonserviceaccounts/token, which mints tokens for existing service accounts, plus CSR creation and approval, PersistentVolume creation (hostPathaccess to the node), control of admission webhooks, and patching Namespaces (which can loosen Pod Security labels).
Treat pods/exec as sensitive too: exec into a container gives access to its mounted tokens and Secrets. To see what a service account can do, run kubectl auth can-i --list --as system:serviceaccount:<namespace>:<name>. Service accounts that need these powers should be few, owned and reviewed like any privileged account, as the Privileged Access Management Guide describes. Excessive privileges remain one of the most common NHI issues.
Kubernetes Secrets: what they do and do not protect
A Secret delivers a credential to a Pod without baking it into the image. It is not, by itself, a secrets management system.
- Base64 is not encryption. The Kubernetes Secrets good practices state that it “provides no additional confidentiality over plain text”. A Secret manifest committed to Git is a leaked secret.
- Secrets are stored unencrypted in etcd by default, readable by anyone with API read access or access to etcd or its backups.
- Creating a Pod is reading a Secret. Anyone who can create a Pod in a namespace can mount any Secret in it, even without direct
getrights.
Encryption at rest
Configure an EncryptionConfiguration through the API server’s --encryption-provider-config flag; the default identity provider provides no confidentiality. As Encrypting Confidential Data at Rest explains, locally managed keys protect against an etcd compromise but not a control plane host compromise, because the key sits on that host. The KMS v2 provider uses envelope encryption with a key encryption key held in an external key management service, so an attacker needs both etcd and the KMS. After enabling encryption, rewrite existing Secrets (for example kubectl get secrets --all-namespaces -o json | kubectl replace -f -) so older data is encrypted too.
External secret stores and file mounts
Keep high-value credentials outside the cluster. Two generic patterns are common: a Container Storage Interface (CSI) secrets store driver, which has the kubelet fetch secrets from an external store and mount them into authorised Pods, keeping them out of etcd; and a sync controller that copies external secrets into Kubernetes Secrets, which is simpler but stores copies in the cluster. Either way, the Pod should authenticate to the store with an audience-scoped projected token or workload identity, not another static token. External stores also make dynamic, short-lived secrets practical, easing the problems in the guide to NHI rotation challenges.
Prefer mounting Secrets as files over environment variables. Environment variables are inherited by child processes and often surface in logs, crash dumps and debugging output. Kubernetes also advises exposing a Secret only to the containers that need it and never logging the value after reading it.
Workload identity federation to the cloud
A cloud access key in a Kubernetes Secret is long-lived, readable by anyone who can create Pods in the namespace, and tends to spread into manifests, Helm values and CI variables. The compromise of AWS environments through exposed .env files shows where static cloud keys in files lead. All three major providers let Pods exchange a Kubernetes service account identity for short-lived cloud credentials instead; the Cloud Workload Identity Guide covers the wider pattern.
| Option | Trust mechanism | Pod to cloud identity mapping | Points to watch |
|---|---|---|---|
| IAM roles for service accounts (Amazon EKS) | The cluster’s OIDC issuer is an IAM identity provider; the projected token is exchanged through STS AssumeRoleWithWebIdentity | Service account annotated with an IAM role | Scope each role’s trust policy to the intended namespace and service account; restrict Pod access to instance metadata so Pods cannot use the node role |
| EKS Pod Identity | A node agent obtains credentials through the EKS Auth service; roles trust the pods.eks.amazonaws.com principal, with no OIDC provider | A Pod Identity association links an IAM role to a service account in a namespace | Associations live in EKS and permissions in IAM, so review both; one role can serve many clusters |
| Workload Identity Federation for GKE | A fixed workload identity pool, PROJECT_ID.svc.id.goog; the GKE metadata server exchanges the token for a federated access token | IAM roles granted to a principal naming the namespace and Kubernetes service account, or optional IAM service account impersonation | Identity sameness: the same namespace and service account name in different clusters of one project are treated as one identity |
| Microsoft Entra Workload ID on AKS | The cluster’s OIDC issuer is trusted through a federated identity credential on a managed identity or app registration | Service account annotated with azure.workload.identity/client-id; Pods labelled azure.workload.identity/use: "true" get a projected token via a mutating webhook | Replaces the deprecated pod-managed identity; scope each federated credential to one service account |
In every option the Kubernetes service account becomes the handle for a cloud identity, so Kubernetes RBAC now guards cloud access: anyone who can create Pods running as a federated service account gets its cloud permissions. Keep one service account per workload, restrict Pod creation in namespaces that hold federated service accounts, and grant cloud roles as narrowly as cluster roles.
SPIFFE and SPIRE for workload identity
Service account tokens identify a workload to the Kubernetes API and, through federation, to a cloud. They are less suited to service-to-service authentication across clusters and platforms. SPIFFE defines a platform-neutral identity (spiffe://trust-domain/path) and short-lived identity documents called SVIDs, as X.509 certificates or JWTs, delivered through the Workload API. SPIRE is its reference implementation.
In Kubernetes, SPIRE agents typically attest their node with the k8s_psat attestor, which validates a projected service account token through the TokenReview API, and attest workloads with the k8s attestor, which asks the local kubelet about the calling Pod and produces selectors such as namespace and service account. Workloads receive automatically rotated X.509 SVIDs for mutual TLS, with no secret stored in the cluster, and separate trust domains can federate by exchanging trust bundles. See the guide to SPIFFE and SPIRE; on the Machine-to-Machine Identity Maturity Model, SPIFFE-based identity sits at the highest level.
Admission control and audit as identity guardrails
RBAC decides who can create a Pod; admission control decides what that Pod may look like. That matters because a privileged Pod can reach the node and every credential on it.
Pod Security Admission. Pod Security Admission enforces the Pod Security Standards (privileged, baseline, restricted) per namespace through labels, in enforce, audit or warn mode. Enforce Baseline or Restricted wherever you do not fully trust whoever creates Pods, and restrict who can patch Namespaces, since the level is a label.
Validating admission policies. Validating admission policies are a declarative, in-process alternative to validating webhooks, written in the Common Expression Language (CEL). Use them for identity rules RBAC cannot express: reject application Pods running as default, require automountServiceAccountToken: false unless a Pod is approved, reject new token Secrets outside an allow-list, and block bindings that give cluster-admin to service accounts.
Audit logging. Kubernetes auditing is off until you pass --audit-policy-file to the API server. The reference example logs Secret access at Metadata level, recording who read what without writing values into the log. For NHIs, alert on:
- Secret
listorwatchby service accounts that do not normally make them, and one identity reading many Secrets quickly; createonserviceaccounts/token, new token Secrets and new ClusterRoleBindings referencing service accounts;pods/execcalls, and service account tokens used from unexpected source addresses;- use of invalidated legacy tokens, flagged with the
authentication.k8s.io/legacy-token-invalidatedaudit annotation.
Auditors increasingly expect this evidence for machine access; see the regulatory and audit perspectives in the Ultimate Guide.
Kubernetes-specific failure patterns
Most Kubernetes identity incidents are local versions of the wider NHI risks.
A mounted token stolen from a compromised Pod. An attacker with code execution in a container reads /var/run/secrets/kubernetes.io/serviceaccount/token and calls the API server. If that service account can list Secrets or create Pods, the attack spreads. Defences: disable automounting where the API is not needed, keep permissions minimal, and rely on bound tokens that expire and die with their Pod.
Overbroad ClusterRoleBindings. An agent, operator or Helm chart binds a wildcard ClusterRole, or cluster-admin, to a service account “to make it work”, turning every Pod that uses it into a cluster administrator. Defences: review ClusterRoleBindings that reference service accounts, replace wildcards with explicit rules, and block such bindings at admission.
Secrets in environment variables and images. Credentials in environment variables surface in logs and crash dumps; credentials baked into images travel with every pull. Research into authentication secrets hidden inside container images found keys in image layers, including access to Kubernetes infrastructure. Both are forms of secret sprawl. Defences: mount Secrets as files, scan images before they reach a registry, and move high-value credentials to external stores or federation.
CI/CD kubeconfigs with cluster-admin. A pipeline stores a kubeconfig with a long-lived token or client certificate bound to cluster-admin, so any pipeline compromise becomes a cluster compromise. The tj-actions GitHub Action supply chain attack, which exposed secrets from workflow runs, and the CI/CD pipeline exploitation case study, where plaintext pipeline credentials led to production access, show how exposed these credentials are. Defences: a dedicated, namespace-scoped service account per pipeline, OIDC federation or short-lived tokens instead of stored kubeconfigs, and pull-based GitOps agents inside the cluster so no external system needs write access.
Forgotten legacy tokens and orphaned identities. Clusters upgraded over many versions carry old token Secrets, service accounts for retired applications and bindings nobody owns. Each is a working credential with no owner. These map to NHI1 (Improper Offboarding), NHI5 (Overprivileged NHI) and NHI7 (Long-Lived Secrets) in the OWASP Non-Human Identities Top 10 (2025).
Practitioner checklist
- Inventory service accounts, bindings, token Secrets, operators and pipeline credentials in every cluster, and give each an owner.
- Create a dedicated service account per workload; grant nothing to
default. - Set
automountServiceAccountToken: falseby default and opt in only where a Pod calls the API. - List Secrets of type
kubernetes.io/service-account-token, migrate their consumers and delete them. - Use audience-scoped, short-lived projected tokens for external systems, and validate audience on the receiving side.
- Prefer RoleBindings, remove wildcards, and keep workload identities out of
system:mastersandcluster-admin. - Justify every service account with Secret read, Pod creation,
escalate,bind,impersonate,nodes/proxyorserviceaccounts/tokenrights. - Enable encryption at rest, preferably with KMS v2, and rewrite existing Secrets afterwards.
- Keep high-value credentials in an external store and mount secrets as files.
- Replace cloud keys in Secrets with IRSA or EKS Pod Identity, Workload Identity Federation for GKE, or Microsoft Entra Workload ID.
- Enforce Baseline or Restricted Pod Security Standards and add validating admission policies for identity rules.
- Enable audit logging and alert on token creation, Secret listing and new privileged bindings.
- Move CI/CD to federated or short-lived, namespace-scoped credentials.
Standards and references
- Kubernetes: Service Accounts
- Kubernetes: Managing Service Accounts
- Kubernetes: Role Based Access Control Good Practices
- Kubernetes: Good practices for Kubernetes Secrets
- Kubernetes: Encrypting Confidential Data at Rest
- Kubernetes: Pod Security Admission and Validating Admission Policy
- Kubernetes: Auditing
- CISA: Updated Kubernetes Hardening Guide (NSA and CISA)
- OWASP Non-Human Identities Top 10 (2025)
- Amazon EKS: IAM roles for service accounts and EKS Pod Identity
- Google Cloud: Workload Identity Federation for GKE
- Microsoft Learn: Microsoft Entra Workload ID on AKS
- SPIFFE Concepts
Related NHI Mgmt Group resources: The Ultimate Guide to Non-Human Identities · NHI Authentication Guide · Guide to SPIFFE and SPIRE · Cloud Workload Identity Guide