Kubernetes Authentication

Authentication answers the first question every Kubernetes API request must answer: who are you? It is the stage immediately after TLS termination in the API Server Request Flow and immediately before authorization. The decisive design fact is that Kubernetes has no user objects — there is no kind: User resource and no built-in user database. Human users are external entities; the kube-apiserver simply runs a configured chain of authenticator modules against each request, stopping at the first one that successfully recognizes a caller (Kubernetes — Authenticating). Whichever authenticator wins produces a four-field identity — Username, UID, Groups, Extra — which is opaque at this stage and acquires meaning only when the authorization stage maps it against RBAC bindings. ServiceAccounts (covered in ServiceAccount) are the one exception: they are API objects, because they identify in-cluster workloads rather than humans.

Mental Model

The authenticator chain is an ordered short-circuit: each module inspects the request and either claims it (returning an identity, ending the chain) or declines (passing to the next). The apiserver does not guarantee a deterministic order across modules — so the modules are designed to be mutually exclusive in practice (a request carries either a client certificate or a bearer token or request-header identity, not several at once). If no module claims the request, it falls through to the anonymous authenticator.

flowchart TD
    REQ["HTTPS request reaches kube-apiserver<br/>(TLS already terminated)"]
    X509{"X.509 client cert<br/>present & signed by --client-ca-file?"}
    BEARER{"Authorization: Bearer token?"}
    SAT["ServiceAccount token validator<br/>(JWT signed by apiserver)"]
    OIDC["OIDC JWT validator<br/>(token from external IdP)"]
    WH["Webhook token authenticator<br/>(POST TokenReview to remote svc)"]
    PROXY{"X-Remote-User header<br/>from trusted front proxy?"}
    ANON["Anonymous:<br/>system:anonymous / system:unauthenticated"]
    IDENT["Identity:<br/>Username · UID · Groups · Extra"]
    AUTHZ["→ Authorization stage"]

    REQ --> X509
    X509 -->|yes| IDENT
    X509 -->|no| BEARER
    BEARER -->|"token type?"| SAT
    BEARER --> OIDC
    BEARER --> WH
    SAT -->|valid| IDENT
    OIDC -->|valid| IDENT
    WH -->|valid| IDENT
    BEARER -->|no token| PROXY
    PROXY -->|yes| IDENT
    PROXY -->|no| ANON
    ANON --> IDENT
    IDENT --> AUTHZ

The diagram shows the authenticator fall-through. The insight to extract: authentication is purely about recognition — every box that yields Identity produces the same four-field tuple, and none of them grant any permission. A request that authenticates as system:anonymous is fully “authenticated”; whether it can do anything is entirely the authorization stage’s problem.

Mechanical Walk-through

The authenticators

X.509 client certificates. Enabled with --client-ca-file=<ca-bundle>. The apiserver verifies the presented certificate chains to that CA, then maps it to an identity by convention: the certificate’s Common Name (CN) becomes the Username, and each Organization (O) field becomes a Group. This is how control-plane components and the original kubeadm admin kubeconfig authenticate — the kubeadm admin cert has CN=kubernetes-admin, O=system:masters, which is why it is a cluster superuser. Certificates cannot be revoked individually (Kubernetes has no CRL/OCSP support) — the only revocation is rotating the entire CA, which makes long-lived client certs a real operational hazard.

Bearer tokens are sent as Authorization: Bearer <token>. Several token sub-types share this header:

  • Static token file (--token-auth-file) — a CSV of token,user,uid,groups. Legacy and discouraged: tokens are long-lived, never expire, and changing the file requires an apiserver restart. Present only for compatibility.
  • Bootstrap tokens — short-lived tokens of the form [a-z0-9]{6}.[a-z0-9]{16}, stored as Secrets in kube-system, used during kubeadm node joins. A bootstrap token authenticates as user system:bootstrap:<id> in group system:bootstrappers, just long enough for the joining node’s kubelet to submit a CSR and obtain its real client certificate (see kubelet TLS bootstrap).
  • ServiceAccount tokens — JWTs signed by the apiserver itself with the key from --service-account-key-file. The modern variant is the bound projected token: short-lived, audience-scoped, bound to a specific Pod. Covered fully in ServiceAccount. Because they are signed locally, ServiceAccount tokens keep working even when an external identity provider is down — the load-bearing reason control loops survive an IdP outage.

OpenID Connect (OIDC). The standard mechanism for human users. The user authenticates to an external Identity Provider (Okta, Microsoft Entra ID, Google, Auth0, Keycloak, Dex), obtains an OIDC ID Token (a JWT), and presents it as a bearer token. The apiserver validates the JWT’s signature against the IdP’s published JWKS, checks the issuer and audience, and extracts the Username and Groups from configured claims. The classic configuration uses per-flag settings: --oidc-issuer-url, --oidc-client-id, --oidc-username-claim (e.g. email), --oidc-groups-claim (e.g. groups), --oidc-username-prefix. The apiserver only validates tokens — it never performs the OIDC login flow itself; kubectl (via a plugin like kubelogin) handles the browser redirect and token refresh. See Authentication MOC for the underlying JWT, JWKS, and OIDC primitives.

Webhook token authentication (--authentication-token-webhook-config-file) — the apiserver POSTs a TokenReview object containing the bearer token to an external HTTPS service, which returns whether the token is valid and, if so, the identity. This is how managed Kubernetes plugs cloud IAM into authentication: EKS’s aws-iam-authenticator is the canonical example — it lets AWS IAM principals authenticate to the cluster.

Authenticating proxy. The apiserver can be fronted by a reverse proxy that performs authentication itself and then sets identity headers — X-Remote-User, X-Remote-Group, X-Remote-Uid, X-Remote-Extra-*. The apiserver trusts these headers only when the request arrives over a connection presenting a client certificate signed by --requestheader-client-ca-file. This same mechanism is how the API Aggregation Layer forwards a caller’s identity to an extension API server.

Anonymous requests. Any request that no authenticator claims is mapped to Username: system:anonymous in group system:unauthenticated. Anonymous access is enabled by default (except under the AlwaysAllow authorization mode) and is disabled with --anonymous-auth=false. Anonymous is not inherently dangerous — health endpoints (/healthz, /livez, /readyz) are intentionally anonymous — but an RBAC binding to system:unauthenticated exposes whatever it grants to the entire internet, which is one of the most common Kubernetes misconfigurations.

The identity tuple

Every authenticator, on success, produces the same four fields (Authenticating):

Username: "jane@example.com"
UID:      "73fbcf2c-1a2b-4c3d-..."           # stable unique id; distinguishes
                                              # two users who reused a username
Groups:   ["system:authenticated",            # every authenticated request gets this
           "developers", "team-payments"]
Extra:    {"scopes.authentication.k8s.io": ["openid","email"]}

system:authenticated is added automatically to every successfully-authenticated request — it is the universal “you got past authn” group, useful as an RBAC subject for cluster-wide read-only roles. Extra is a free-form map[string][]string that authenticators can populate and that some authorizers (especially webhook authorizers) consume.

Structured Authentication Configuration

The historical OIDC configuration is per-flag and supports exactly one issuer. Structured Authentication Configuration replaces it with a YAML file pointed to by --authentication-config. It supports multiple JWT issuers, CEL (Common Expression Language) expressions to extract and transform claims into username/group/UID, CEL validation rules on incoming tokens, and — critically — hot reload: editing the file reconfigures authentication without an apiserver restart.

The feature followed the standard Kubernetes promotion path under KEP-3331: alpha in v1.29, beta in v1.30 (Kubernetes 1.30 blog), and stable / GA in v1.34 — the current docs mark AuthenticationConfiguration under the apiserver.config.k8s.io/v1 group version as Kubernetes v1.34 [stable], enabled by default (Kubernetes — Authenticating, as of v1.36, May 2026). The earlier v1alpha1 and v1beta1 group versions differ in field shape, so a config file authored against a beta cluster may need adjustment when the apiserver is upgraded to the v1 form.

# --authentication-config=/etc/kubernetes/authn-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
jwt:
- issuer:
    url: https://login.example.com           # the OIDC issuer; must match the JWT 'iss' claim
    audiences: [kubernetes]                   # acceptable 'aud' values
  claimMappings:
    username:
      claim: email                           # which JWT claim becomes Username
      prefix: "oidc:"                         # namespacing prefix to avoid collisions
    groups:
      claim: groups
      prefix: "oidc:"
  claimValidationRules:
  - claim: hd                                 # CEL-validate a custom claim
    requiredValue: example.com                # reject tokens not from this hosted domain
anonymous:
  enabled: true
  conditions:                                 # restrict anonymous to specific paths only
  - path: /healthz
  - path: /livez
  - path: /readyz

Line-by-line. jwt is a list — multiple issuers, each independently validated. claimMappings.username selects which claim is the identity and namespaces it with a prefix so an OIDC email of jane@example.com becomes oidc:jane@example.com, which cannot collide with a ServiceAccount or X.509 identity. claimValidationRules reject tokens that fail a CEL predicate — here, tokens not from the example.com Google Workspace domain. The anonymous.conditions block confines anonymous access to health endpoints only, closing the “RBAC-bound system:unauthenticated” foot-gun structurally.

Failure Modes

RBAC binding to system:unauthenticated. A ClusterRoleBinding granting any permission to the group system:unauthenticated (or system:anonymous) exposes that permission to every unauthenticated request — i.e. the public internet if the apiserver is reachable. Audit kubectl get clusterrolebindings -o wide | grep -i unauth. The structured-config anonymous.conditions block is the structural fix.

OIDC issuer outage. If the IdP becomes unreachable, the apiserver cannot fetch/refresh the JWKS to validate signatures and all OIDC kubectl access fails. ServiceAccount-token-based control loops are unaffected (tokens are signed locally by the apiserver) — so reconciliation continues, but humans are locked out. Keep a break-glass X.509 admin kubeconfig stored offline for exactly this scenario.

Client certificate that cannot be revoked. Kubernetes has no certificate revocation. A leaked client cert is valid until it expires or until the cluster CA is rotated (a disruptive operation). Prefer short-lived OIDC tokens for humans and bound ServiceAccount tokens for workloads; treat X.509 client certs as break-glass only.

Static token file in production. --token-auth-file tokens never expire and editing the file needs an apiserver restart. A leaked static token is permanent. Treat any cluster still using it as a finding.

Username collision across authenticators. Without prefixes, an X.509 cert with CN=jane, an OIDC token with email: jane, and a ServiceAccount could all map to the username jane, conflating three distinct principals in RBAC. Always use username-prefix / claimMappings.username.prefix so each identity source occupies its own namespace.

Alternatives and When to Choose Them

  • OIDC vs X.509 client certs for humans. OIDC: centralized, revocable (short-lived tokens just expire), MFA-capable, group membership from the corporate directory — the correct choice for human users at any real scale. X.509: no external dependency, but unrevocable and unwieldy — keep only as offline break-glass.
  • Webhook token auth vs OIDC. Webhook delegates the entire decision to an external service and is the right fit when identity lives in a system that is not an OIDC provider (cloud IAM) — at the cost of a network round-trip per token validation. OIDC validates JWTs locally (just a periodic JWKS fetch) and is lower-latency.
  • ServiceAccount tokens vs everything above — these are for workloads, not humans. A Pod should authenticate with its bound projected ServiceAccount token; a human should never share a ServiceAccount token. See ServiceAccount.
  • Structured config vs per-flag OIDC. Use structured authentication config for any new cluster: multiple issuers, CEL claim transforms, and hot reload are strict improvements over the single-issuer --oidc-* flags.

Production Notes

  • Managed services run their own authn front. EKS uses webhook auth (aws-iam-authenticator / EKS access entries) to map IAM principals; GKE and AKS integrate their cloud identity directories. You configure authorization (RBAC) yourself, but authentication is partly the provider’s.
  • system:masters is a bypass, not a role. Membership in system:masters (the kubeadm admin cert’s O field) makes the apiserver skip authorization entirely. Anyone holding a cert or kubeconfig in that group is an unconditional superuser. Treat that file like a root password — store it offline, never embed it in CI.
  • Audit authentication, not just authorization. The audit log records the authenticated user.username and user.groups on every request — the primary forensic trail for “who did this.” A user.username of system:anonymous on a mutating request is an incident.
  • Prefer short-lived everything. Bound ServiceAccount tokens (default ~1 h, auto-rotated) and OIDC ID tokens (minutes) limit the blast radius of a leak; long-lived static tokens and non-expiring client certs do not.

See Also