Join our Newsletter — 33% off our NHI Course

Kubernetes NHI Security Guide

Written by: Lalit Choda, NHI Mgmt Group

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 default service 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/proxy and 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 outside kube-system beyond 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 set spec.serviceAccountName runs as it, so anything granted to default is shared by every such Pod in the namespace.
  • Node and kubelet identities. Each kubelet authenticates as system:node:<nodeName> in the system:nodes group. The Node authoriser limits it to Secrets, ConfigMaps and volumes of Pods on its own node, and the NodeRestriction admission 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 typeHow it is issuedLifetimeRevocationRecommendation
Legacy Secret-based tokenSecret of type kubernetes.io/service-account-token, auto-generated before v1.24 or created manuallyNo expiryDelete the SecretFind and remove; do not create new ones
Default projected tokenKubelet via TokenRequest, audience is the API serverOne hour by default, refreshed by the kubeletExpires, or delete the PodDisable automounting where the API is not needed
Custom projected tokenProjected volume with an explicit audienceSet by expirationSeconds, minimum ten minutesExpires, or delete the PodPreferred for external systems that trust the cluster issuer
Client TokenRequestkubectl create token or the APIRequested duration, within API server limitsExpires, or delete the bound objectUse 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-admin only where specifically needed, and never add identities to system: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, list or watch. list and watch return 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 edit role allows this.
  • escalate, bind and impersonate. These bypass the protections that normally stop an identity granting itself more than it has.
  • nodes/proxy. Even get reaches the kubelet API, which can execute commands in any Pod on the node and bypasses audit logging and admission control.
  • create on serviceaccounts/token, which mints tokens for existing service accounts, plus CSR creation and approval, PersistentVolume creation (hostPath access 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 get rights.

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.

OptionTrust mechanismPod to cloud identity mappingPoints 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 AssumeRoleWithWebIdentityService account annotated with an IAM roleScope 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 IdentityA node agent obtains credentials through the EKS Auth service; roles trust the pods.eks.amazonaws.com principal, with no OIDC providerA Pod Identity association links an IAM role to a service account in a namespaceAssociations live in EKS and permissions in IAM, so review both; one role can serve many clusters
Workload Identity Federation for GKEA fixed workload identity pool, PROJECT_ID.svc.id.goog; the GKE metadata server exchanges the token for a federated access tokenIAM roles granted to a principal naming the namespace and Kubernetes service account, or optional IAM service account impersonationIdentity sameness: the same namespace and service account name in different clusters of one project are treated as one identity
Microsoft Entra Workload ID on AKSThe cluster’s OIDC issuer is trusted through a federated identity credential on a managed identity or app registrationService account annotated with azure.workload.identity/client-id; Pods labelled azure.workload.identity/use: "true" get a projected token via a mutating webhookReplaces 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 list or watch by service accounts that do not normally make them, and one identity reading many Secrets quickly;
  • create on serviceaccounts/token, new token Secrets and new ClusterRoleBindings referencing service accounts;
  • pods/exec calls, and service account tokens used from unexpected source addresses;
  • use of invalidated legacy tokens, flagged with the authentication.k8s.io/legacy-token-invalidated audit 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: false by 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:masters and cluster-admin.
  • Justify every service account with Secret read, Pod creation, escalate, bind, impersonate, nodes/proxy or serviceaccounts/token rights.
  • 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

Related NHI Mgmt Group resources: The Ultimate Guide to Non-Human Identities · NHI Authentication Guide · Guide to SPIFFE and SPIRE · Cloud Workload Identity Guide