Back to Blog

How the Kubernetes API Server Authenticates a Request for the CKS Exam: The Authenticator Chain, X.509 Certificates, Static & Bootstrap Tokens, ServiceAccount JWT Internals, Structured AuthenticationConfiguration & OIDC Claim Mapping, Webhook Token Authentication, Authenticating Proxies, Anonymous Endpoints, Impersonation Headers and kubectl auth whoami

A practitioner's guide to the authentication layer of kube-apiserver for the Certified Kubernetes Security Specialist (CKS) exam: how the authenticator chain short-circuits, what a Kubernetes identity actually contains, every reserved system: name, X.509 CN/O mapping and the no-revocation rule, the static token file, bootstrap token Secrets and groups, the ServiceAccount JWT issuer, keys, bound-object claims and discovery endpoints, every --oidc-* flag and its replacement in the structured AuthenticationConfiguration file (CEL claim validation, claim mappings, user validation, 64-authenticator limit, automatic reload), webhook TokenReview, the --requestheader-* authenticating-proxy flags and their CA warning, configuring anonymous access per endpoint, impersonation headers and constrained impersonation, client-go exec credential plugins, kubectl auth whoami, a failure-diagnosis table, a kubeadm lab, scenario drills, common mistakes and an FAQ.

By Sailor Team , November 9, 2026

Every security control in Kubernetes assumes the API server already knows who is calling. RBAC decides what alice may do, but something earlier in the request path has to decide that this TLS connection, or this bearer token, is alice, and which groups she belongs to. That earlier step is authentication, and it is the layer that CKS candidates most often treat as a black box. The exam does not. The Cluster Hardening domain (15% of the exam) lists “Restrict access to Kubernetes API” as a competency alongside RBAC and service account hygiene, the Cluster Setup domain tests the CIS Benchmark checks that govern anonymous access and the static token file, and the whole exam runs on Kubernetes v1.37, where the structured AuthenticationConfiguration file has been the primary way to configure external identity for several releases.

This article is the mechanics layer. The KCSA authentication and RBAC overview introduces the request pipeline and the idea that users are not Kubernetes objects. The kubeconfig and client certificate guide covers the client side and the operational problems with long-lived certificates. The CKA cluster access guide shows how to mint a user certificate with the CertificateSigningRequest API, and the service account token security guide covers automount, token theft and legacy token clean-up. None of that is repeated here. Instead, this guide explains what happens inside kube-apiserver between the TCP connection and the authorizer: the chain of authenticators, the exact data each produces, every flag and configuration field that controls them, and the failure modes those flags create. Everything below was checked against the Kubernetes v1.37 reference documentation in November 2026.

Where authentication sits and what it produces

A request to the API server passes through three gates in a fixed order: authentication, authorization, admission control. Authentication answers who; it never answers whether. The output of a successful authentication is a small user-info record with four attributes, and the whole of the rest of the request path works from that record rather than from the original credential.

AttributeTypeTypical valuesWho consumes it
Usernamestring[email protected], system:serviceaccount:dev:ci, system:node:worker-1RBAC User subjects, audit logs, admission
UIDstringA UUID, an employee number, or emptyImpersonation, audit, some webhooks
Groupslist of stringsdevelopers, system:masters, system:authenticatedRBAC Group subjects
Extramap of string to list of stringsexample.com/tenant: [acme], authentication.kubernetes.io/pod-name: [web-0]Authorization webhooks, admission policies, audit

The documentation makes a point that trips up many candidates: “All values are opaque to the authentication system and only hold significance when interpreted by an authorizer.” The authenticator does not know or care that system:masters is special. RBAC does. That is why a certificate with O=system:masters is dangerous: the authenticator faithfully reports the group, and the RBAC authorizer’s built-in cluster-admin binding does the rest.

Three rules govern how the authenticators are combined.

  1. Every successful authentication adds the group system:authenticated. No authenticator has to do this; the API server does it automatically.
  2. The first authenticator that succeeds wins. In the documentation’s words, “the first module to successfully authenticate the request short-circuits evaluation.”
  3. The order is not guaranteed. The API server “does not guarantee the order authenticators run in.” In practice the credential type decides which authenticators are even relevant (a client certificate is never examined by a token authenticator), but you should never write a design that depends on one bearer-token authenticator running before another.

A failure is also worth defining precisely. An authenticator can return three outcomes for a credential: authenticated, not mine (the token format does not match, so try the next one), or rejected (the token was mine and it is invalid). A rejected credential produces 401 Unauthorized immediately. A credential that no authenticator claims is treated as no credential at all, and the request falls through to the anonymous authenticator described later. That is why the same misconfiguration can show up either as a 401 or as a mysterious system:anonymous cannot get resource message depending on whether the token was malformed or merely unknown.

Seeing the result: kubectl auth whoami

Before touching any authenticator, learn the tool that shows you what the chain produced. The SelfSubjectReview API (stable since v1.28) returns the user-info record for the caller, and kubectl auth whoami wraps it:

kubectl auth whoami
# ATTRIBUTE   VALUE
# Username    kubernetes-admin
# Groups      [kubeadm:cluster-admins system:authenticated]

kubectl auth whoami -o yaml
# status:
#   userInfo:
#     username: kubernetes-admin
#     groups: [kubeadm:cluster-admins, system:authenticated]

Properties that the exam can turn into a question:

  • Every authenticated subject can call it, because the built-in system:basic-user ClusterRole grants create on selfsubjectreviews (alongside selfsubjectaccessreviews and selfsubjectrulesreviews).
  • Nothing is persisted. The object is filled in on the POST and discarded; you cannot kubectl get selfsubjectreviews.
  • It reports the identity after impersonation. If you run it with --as, you see the impersonated user, which is the whole point when debugging an RBAC policy.
  • The fields that appear depend on the authenticator. A client certificate yields a username and groups. An OIDC token with claim mappings can also yield a UID and extra attributes.

When a scenario says “the user authenticates successfully but every request is forbidden”, this is the first command to run, because nine times out of ten the groups are not what the administrator assumed (a missing prefix, an O= field that was dropped, a group claim that is a string instead of an array).

Reserved names: the system: prefix

Kubernetes reserves usernames and groups that start with system: for its own components. No external authenticator should produce them, and the structured configuration file lets you enforce that with a validation rule. Knowing the built-in ones lets you read an audit log or an RBAC binding at a glance.

NameKindProduced byMeaning
system:authenticatedgroupAPI server, every successful authenticationAnyone who proved an identity
system:unauthenticatedgroupAnonymous authenticatorRequests with no accepted credential
system:anonymoususerAnonymous authenticatorThe username of such requests
system:mastersgroupWhatever certificate or token carries itBound to cluster-admin by a built-in binding; cannot be restricted by RBAC
system:serviceaccount:<ns>:<name>userServiceAccount token authenticatorA workload identity
system:serviceaccounts, system:serviceaccounts:<ns>groupsServiceAccount token authenticatorAll service accounts, and those in one namespace
system:node:<nodeName>userThe kubelet’s client certificateA node identity, consumed by the Node authorizer
system:nodesgroupThe kubelet’s client certificate (O=system:nodes)All kubelets
system:bootstrap:<tokenID>userBootstrap token authenticatorA joining node before it has a certificate
system:bootstrappers, system:bootstrappers:<suffix>groupsBootstrap token authenticatorUsed by kubeadm’s CSR auto-approval RBAC
system:kube-controller-manager, system:kube-scheduler, system:kube-proxyusersControl-plane client certificatesBound to matching built-in ClusterRoles

The system:masters row deserves a sentence. The group is honoured by a hard-coded binding to cluster-admin, and the kubeadm super-admin.conf credential uses it as a break-glass identity. Since v1.29 the day-to-day admin.conf instead authenticates as kubernetes-admin in the group kubeadm:cluster-admins, which is bound to cluster-admin by an ordinary, editable ClusterRoleBinding. If an exam task asks you to find and remove over-privileged access, a certificate carrying O=system:masters is the thing that no RBAC edit can neutralise. The kubeconfig and client certificate guide covers that trap from the client side.

X.509 client certificates

Client certificate authentication is enabled with --client-ca-file=<file>, which points at one or more PEM-encoded CA certificates. Any certificate that chains to one of those CAs, is within its notBefore and notAfter window, and has an Extended Key Usage that includes ClientAuth is accepted. The mapping to a Kubernetes identity is fixed:

Certificate attributeOIDBecomes
commonName (CN)2.5.4.3Username
organization (O), repeated2.5.6.4One group per O value, order irrelevant
Kubernetes user ID attribute1.3.6.1.4.1.57683.2UID (requires the AllowParsingUserUIDFromCertAuth gate, beta and on by default since v1.34)

So a certificate issued to CN=Ada Lovelace,O=Users,O=Staff,O=Programmers authenticates as the user Ada Lovelace in the groups Users, Staff, Programmers and system:authenticated. A standard uid attribute (OID 0.9.2342.19200300.100.1.1) in the subject is ignored; only the Kubernetes-specific OID is read as a UID.

Two consequences matter for the exam.

There is no revocation. The documentation states it plainly: “Kubernetes 1.37 does not support certificate revocation. Any certificate that is issued remains valid until it expires.” The only ways to shut out a leaked certificate are to let it expire, to remove every RBAC binding that names its CN or O values, or to rotate the client CA, which invalidates every certificate at once. This is the reason the project steers human authentication towards short-lived tokens, and the reason an exam answer that says “revoke the certificate” is wrong.

The node identity is the same mechanism. A kubelet presents a certificate for CN=system:node:<name>,O=system:nodes. The authenticator produces exactly that username and group; it is the Node authorizer and the NodeRestriction admission plugin, covered in the authorization chain guide, that then limit the kubelet to its own node’s objects. A certificate with O=system:nodes but a CN that does not match the kubelet’s node is a textbook escalation path, which is why the CSR signer for kubelet certificates checks the CN against the requester.

On a kubeadm cluster the client CA is /etc/kubernetes/pki/ca.crt, and the same CA also issues the serving certificates, so a candidate who needs to inspect who a kubeconfig authenticates as can decode it without any cluster access:

kubectl config view --raw -o jsonpath='{.users[0].user.client-certificate-data}' \
  | base64 -d | openssl x509 -noout -subject -enddate -ext extendedKeyUsage
# subject=O=kubeadm:cluster-admins, CN=kubernetes-admin
# notAfter=Nov  8 10:12:31 2027 GMT
# X509v3 Extended Key Usage: TLS Web Client Authentication

Issuing new user certificates through the CertificateSigningRequest API, including the kubernetes.io/kube-apiserver-client signer and the expirationSeconds field, is covered in the CKA cluster access guide and is not repeated here.

The static token file

The oldest bearer-token authenticator reads a CSV file given by --token-auth-file=<file>. Each line has at least three columns, token, username and user UID, followed by an optional comma-separated list of groups, which must be double-quoted when there is more than one:

31ada4fd-adec-460c-809a-9e56ceb75269,monitoring-agent,m-001,"system:monitoring,readers"

The client sends Authorization: Bearer 31ada4fd-adec-460c-809a-9e56ceb75269. Three properties make this authenticator a CIS Benchmark finding (check 1.2.2 in the current benchmark asks that --token-auth-file is not set on the API server):

  • Tokens never expire.
  • The file is not re-read. Adding, changing or removing a token requires restarting every API server, which the documentation notes is also what you must do to deactivate a bootstrapping token after switching to another method.
  • Tokens are plain text on the control-plane disk, with no binding to a client, namespace or object.

The documentation’s own guidance is that the static token file suits “tokens that by their nature are long-lived, static, and perhaps may never be rotated”, such as a monitoring agent local to the control plane, and that “especially where very prompt token rotation is important” you should use a webhook token authenticator instead. On the exam, if you are asked to harden an API server and you find --token-auth-file in /etc/kubernetes/manifests/kube-apiserver.yaml, removing it (after confirming nothing depends on it) is the expected move; remember that editing the static Pod manifest restarts the API server on its own.

Bootstrap tokens

Bootstrap tokens exist for one job: letting a brand-new node or cluster component authenticate before it has a certificate. They are stable since v1.18 and are what kubeadm join uses.

PropertyValue
Format[a-z0-9]{6}.[a-z0-9]{16}, for example 07401b.f395accd246ae52d. The first part is the public Token ID; the second is the Token Secret
StorageA Secret of type bootstrap.kubernetes.io/token named bootstrap-token-<tokenID> in kube-system
Enable the authenticator--enable-bootstrap-token-auth on kube-apiserver
Expiry cleanupThe tokencleaner controller, enabled with --controllers=*,tokencleaner on kube-controller-manager
Authenticates asUser system:bootstrap:<tokenID>, group system:bootstrappers, plus any auth-extra-groups
Extra groups ruleEvery extra group must start with system:bootstrappers:
Usage switchesusage-bootstrap-authentication: "true" allows bearer-token use; usage-bootstrap-signing: "true" allows signing the cluster-info ConfigMap
ExpirationOptional expiration field, RFC 3339 UTC. Expired tokens are rejected for authentication and ignored for signing

The Secret that backs a kubeadm default token looks like this:

apiVersion: v1
kind: Secret
metadata:
  name: bootstrap-token-07401b
  namespace: kube-system
type: bootstrap.kubernetes.io/token
stringData:
  description: "The default bootstrap token generated by 'kubeadm init'."
  token-id: 07401b
  token-secret: f395accd246ae52d
  expiration: 2026-11-10T03:22:11Z
  usage-bootstrap-authentication: "true"
  usage-bootstrap-signing: "true"
  auth-extra-groups: system:bootstrappers:kubeadm:default-node-token

The security-relevant mechanics:

  • The identity is deliberately weak. The documentation says the “naming and groups are intentionally limited to discourage users from using these tokens past bootstrapping.” What a bootstrap token can do is entirely decided by the RBAC bound to system:bootstrappers and the extra groups. On a kubeadm cluster that is the right to create CertificateSigningRequests and to have node client CSRs auto-approved, which is exactly enough to join a node and nothing more.
  • A leaked token is a node-join credential. Anyone holding it, before expiry, can obtain a kubelet client certificate for a node name of their choosing and then operate within the Node authorizer’s limits for that name. kubeadm init tokens default to a 24-hour TTL; a token created with kubeadm token create --ttl 0 never expires and is a finding.
  • The signing half is separate. With --controllers=*,bootstrapsigner, the controller manager uses the token secret as an HMAC-SHA256 key to sign the cluster-info ConfigMap in kube-public, which joining nodes read anonymously to learn the cluster CA. The documentation warns that “any party with a bootstrapping token can create a valid signature for that token”, so a single token shared with many clients lets a compromised client man-in-the-middle the others.

The practical commands:

kubeadm token list                                   # ID, TTL, expiry, usages, extra groups
kubeadm token create --ttl 2h --print-join-command   # short-lived token for one join
kubeadm token delete 07401b                          # removes the Secret immediately
kubectl -n kube-system get secrets --field-selector type=bootstrap.kubernetes.io/token

ServiceAccount tokens at the authenticator level

The ServiceAccount authenticator is always on. It verifies JSON Web Tokens (JWTs) issued by the API server itself and produces the username system:serviceaccount:<namespace>:<name> plus the groups system:serviceaccounts and system:serviceaccounts:<namespace>. The service account token security guide covers how Pods receive these tokens, automount, and the hunt for legacy Secret-based tokens. This section covers the issuer and verifier inside the API server, which is where the exam’s harder questions live.

The flags

FlagPurposeDetail that becomes a question
--service-account-signing-key-filePrivate key used to sign new tokensChanges to the file are not re-read; a restart is required. kubeadm uses /etc/kubernetes/pki/sa.key
--service-account-key-filePEM public or private keys (RSA or ECDSA) used to verify tokensThe file can contain multiple keys and the flag can be repeated, which is how key rotation works. If unspecified, the TLS private key is used. kubeadm uses sa.pub
--service-account-issuerThe iss claim stamped into every tokenShould be an https:// URL. Can be given more than once: the first value is used for issuing, every value is accepted for verification, which allows an issuer migration. kubeadm uses https://kubernetes.default.svc.cluster.local
--api-audiencesAudiences accepted for the API server itselfDefaults to the issuer value when unset
--service-account-max-token-expirationCeiling on the lifetime a TokenRequest may ask forA Kubernetes duration string
--service-account-extend-token-expirationStretches projected-volume tokens to one year while still rotating them hourlyA migration aid for clients that never re-read the token file; the extended value is min(1 year, signer maximum)
--service-account-lookupValidate that the backing Secret still exists in etcd for legacy tokensDeleting the Secret then revokes the token
--service-account-jwks-uriOverrides the JWKS URI advertised in the discovery documentNeeded when the API server is reached through a different public address
--service-account-signing-endpointSocket of an external JWT signerMutually exclusive with the two key-file flags; the ExternalServiceAccountTokenSigner gate is stable since v1.36

What is inside a token

A bound token minted for a Pod carries, besides the standard registered claims, a private kubernetes.io claim that ties it to API objects:

{
  "aud": ["https://kubernetes.default.svc.cluster.local"],
  "exp": 1762682640, "iat": 1762679040, "nbf": 1762679040,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "jti": "aed34954-b33a-4142-b1ec-389d6bbb4936",
  "kubernetes.io": {
    "namespace": "payments",
    "node": { "name": "worker-2", "uid": "646e7c5e-..." },
    "pod":  { "name": "api-7c9d8-x2k1p", "uid": "5e0bd49b-..." },
    "serviceaccount": { "name": "api", "uid": "14ee3fa4-..." }
  },
  "sub": "system:serviceaccount:payments:api"
}

Rules the authenticator applies to those claims:

  • A token can be bound to a Pod, a Secret, a Node (creating node-bound tokens is GA since v1.33), or, as an alpha feature in v1.37 behind APIServerWebhookAuthenticationToken, a webhook configuration object. The bound object’s name and UID are checked on every use. If the object is gone, or its UID differs because it was deleted and recreated, authentication fails.
  • If the bound object or the ServiceAccount has a deletionTimestamp, the token stops working 60 seconds after that timestamp, even while finalizers keep the object around.
  • The node block inside a Pod-bound token is informational. The API server embeds it so that integrators do not need to look up the Pod, but it does not verify that the Node still exists.
  • Since v1.32 (the ServiceAccountTokenPodNodeInfo and ServiceAccountTokenJTI gates are GA), the authenticator also surfaces these values as extra attributes: authentication.kubernetes.io/pod-name, pod-uid, node-name, node-uid and credential-id (the jti). Those extras appear in audit logs and in kubectl auth whoami, which means a stolen Pod token can be traced to the Pod and node it was issued for.

Discovery, offline verification and the TokenReview escape hatch

The API server publishes OpenID discovery documents at /.well-known/openid-configuration and /openid/v1/jwks, and the built-in ClusterRole system:service-account-issuer-discovery controls who may read them (kubeadm binds it to system:serviceaccounts; granting it to system:unauthenticated is a common choice when an external system must fetch the keys). An external verifier that uses those keys can check the signature, iss, aud and exp of a token, but the documentation is explicit that such verifiers “do not verify the claims embedded in the JWT token to be current and still valid”: a Pod-bound token whose Pod has been deleted still verifies offline until exp. Anything that needs the binding checked must submit the token in a TokenReview, which the API server answers with the expanded user info, including the extra attributes above.

Revocation and key rotation

There is “no central record of tokens issued, so there is no way to revoke individual tokens” created through TokenRequest. The levers are indirect:

To invalidateDo this
Every token of one ServiceAccountDelete and recreate the ServiceAccount; the UID changes and every token fails the UID check
One bound tokenDelete the object it is bound to (the Pod, or the Secret you bound it to on purpose)
A legacy Secret-based tokenDelete the Secret, with --service-account-lookup on (the default)
Everything, including tokens whose binding cannot be removedRotate the signing key and remove the old public key from --service-account-key-file

Rotating the signing key safely is a three-step procedure that the flag table makes possible: add the new public key to the verification key file on every API server and restart them (tokens signed by either key now verify), switch --service-account-signing-key-file to the new private key and restart again, and only remove the old public key once every token signed with it has expired. With --service-account-extend-token-expiration on, that last wait can be a year, which is the hidden cost of that flag.

External JWT authenticators: OIDC flags and the structured configuration file

Kubernetes has no identity provider of its own and no login page. For humans, the standard pattern is OpenID Connect: the user authenticates with an external provider, receives an id_token (a signed JWT), and presents it as a bearer token. The API server verifies the signature using keys fetched from the provider’s discovery endpoint, so it never calls the provider per request. The documentation lists the challenges honestly: there is no web interface to trigger the login, the id_token “can’t be revoked, it’s like a certificate so it should be short-lived”, and the provider must support OIDC discovery, run TLS with non-obsolete ciphers and present a certificate whose issuing CA has the CA flag set to TRUE, even if self-signed, because Go’s TLS client is strict.

There are two ways to configure the authenticator, and mixing them is a hard error.

The command-line flags

FlagRequiredMeaning and default
--oidc-issuer-urlYesThe provider’s issuer URL. Must be https://. If discovery lives at https://idp.example.com/.well-known/openid-configuration, the value is https://idp.example.com
--oidc-client-idYesThe aud every token must be issued for
--oidc-username-claimNoClaim used as the username. Default sub
--oidc-username-prefixNoPrefix added to the username. If unset and the claim is anything other than email, the default is <issuer URL>#. The value - disables prefixing
--oidc-groups-claimNoClaim used for groups. Must be an array of strings in the token. No default
--oidc-groups-prefixNoPrefix added to every group, for example oidc:
--oidc-required-claimNokey=value that must be present. Repeatable
--oidc-ca-fileNoCA for the provider’s TLS certificate. Defaults to the host’s root CAs
--oidc-signing-algsNoAccepted algorithms from RS256/384/512, ES256/384/512, PS256/384/512. Default RS256

The prefix defaults are where most real-world misconfiguration happens. With --oidc-username-claim=sub and no prefix flag, the username becomes https://idp.example.com#8f1c..., and the RBAC binding that names 8f1c... never matches. With --oidc-username-claim=email the prefix defaults to nothing, so an attacker who controls any trusted provider that can emit the claim email: system:kube-controller-manager could, without the prefix, impersonate a control-plane identity. That is exactly why prefixes exist and why the structured file forces you to decide.

The AuthenticationConfiguration file

Passing --authentication-config=<file> makes the API server read a AuthenticationConfiguration object instead. The feature went alpha in v1.29, beta in v1.30 and stable in v1.34, where the gate is locked on. The documentation is blunt about mixing: “If you specify --authentication-config along with any of the --oidc-* command line arguments, this is a misconfiguration. In this situation, the API server reports an error and then immediately exits.” On an exam, a kube-apiserver static Pod that crash-loops right after you added the file is almost always this.

An annotated example:

apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
jwt:                                    # up to 64 authenticators
- issuer:
    url: https://idp.example.com        # https only; unique across authenticators;
                                        # must not equal --service-account-issuer
    discoveryURL: https://idp.internal.example.com/.well-known/openid-configuration
                                        # optional; the fetched "issuer" must still equal url
    certificateAuthority: |             # PEM; defaults to the system trust store
      -----BEGIN CERTIFICATE-----
      ...
    audiences:
    - kubernetes
    - kubernetes-cli
    audienceMatchPolicy: MatchAny       # required whenever more than one audience is listed
  claimValidationRules:
  - claim: hd                           # legacy form, same as --oidc-required-claim
    requiredValue: example.com
  - expression: 'claims.exp - claims.nbf <= 86400'
    message: total token lifetime must not exceed 24 hours
  claimMappings:
    username:
      claim: email                      # if claim is set, prefix is REQUIRED ...
      prefix: "oidc:"                   # ... use "" explicitly for no prefix
    groups:
      expression: 'claims.roles.split(",")'   # CEL; string or list of strings
    uid:
      claim: sub
    extra:
    - key: example.com/tenant           # domain-prefixed; k8s.io and kubernetes.io reserved
      valueExpression: 'claims.tenant'
  userValidationRules:
  - expression: "!user.username.startsWith('system:')"
    message: username cannot use the reserved system: prefix
  - expression: "user.groups.all(g, !g.startsWith('system:'))"
    message: groups cannot use the reserved system: prefix

The rules the file enforces, each of which is a plausible exam distractor:

RuleDetail
Authenticator countAt most 64 entries in jwt
issuer.urlhttps:// only, unique across all authenticators, and must not conflict with the ServiceAccount issuer
issuer.discoveryURLMust differ from url, must be unique, and must include /.well-known/openid-configuration yourself; the issuer value inside the fetched document must still equal issuer.url
AudiencesAt least one aud entry must match; audienceMatchPolicy: MatchAny is required when more than one audience is listed
claimValidationRulesEither claim/requiredValue or a CEL expression returning a boolean. All rules must pass. message customises the API server log line
claimMappings.usernameRequired. Either claim + prefix (prefix mandatory, "" for none) or a CEL expression returning a string. An empty result fails authentication
The email ruleIf a username expression uses claims.email, then claims.email_verified must be referenced somewhere in the expressions; the documented equivalent of the automatic check is claims.?email_verified.orValue(true) == true
claimMappings.groupsclaim + prefix or a CEL expression returning a string or list of strings
claimMappings.uidclaim or expression
claimMappings.extraKeys must be lower-case domain-prefixed paths such as example.org/foo, unique, and never under k8s.io or kubernetes.io
userValidationRulesCEL over the final user object; all must pass
CEL variablesclaims (the token payload as a map) in claim rules and mappings; user (the UserInfo) in user validation rules
Distributed claimsNot available to CEL expressions

Two operational behaviours matter as much as the schema. The API server re-reads the file automatically when it changes, exposing apiserver_authentication_config_controller_automatic_reload_last_timestamp_seconds so you can confirm the reload, and the documentation asks that edits be made atomically (write a temporary file, then rename). And a second feature gate, StructuredAuthenticationConfigurationEgressSelector (beta and on by default since v1.34), adds an egressSelectorType field of controlplane or cluster so discovery and JWKS traffic to the issuer goes through the configured egress selector, which is how you reach a provider that lives inside the cluster network.

Why the exam cares: the file is the only way to have more than one issuer, more than one audience per issuer, a UID or extra attributes from a token, or a validation rule such as “reject any token that would map to a system: name”. The flags cannot do any of that, and they are now the legacy path.

The client side of OIDC

The API server only sees a bearer token, so the client is responsible for obtaining and refreshing it. The three options are passing --token=<id_token> on the command line, the built-in oidc auth-provider in the kubeconfig (which refreshes the id_token using the refresh_token and client_secret, and does not support providers that omit the id_token from refresh responses), and the exec credential plugin mechanism covered below. Whatever the client does, kubectl auth whoami shows the mapped result.

Webhook token authentication

For token formats Kubernetes does not understand natively, the API server can POST the token to an external service and let it decide. Three flags configure it:

FlagMeaningDefault
--authentication-token-webhook-config-fileA kubeconfig-format file: clusters describes the remote service (URL and CA); users describes the API server’s own client certificate for the callnone
--authentication-token-webhook-cache-ttlHow long a decision is cached2 minutes
--authentication-token-webhook-versionauthentication.k8s.io/v1beta1 or authentication.k8s.io/v1 TokenReview objectsv1beta1 for backwards compatibility

The protocol is the TokenReview object. The API server sends spec.token and, optionally, spec.audiences (the identifiers of the server the token was presented to, so an audience-aware authenticator can confirm the token was meant for this cluster). The webhook returns the same object with status filled in:

{
  "apiVersion": "authentication.k8s.io/v1",
  "kind": "TokenReview",
  "status": {
    "authenticated": true,
    "user": {
      "username": "[email protected]",
      "uid": "42",
      "groups": ["developers", "qa"],
      "extra": { "scopes": ["openid", "profile"] }
    },
    "audiences": ["https://kube.example.com"]
  }
}

Behaviours to remember: the response must use the same API version as the request; the spec in the response is ignored; a rejection is "authenticated": false with an optional error string (otherwise the client gets a generic Unauthorized); and the extra values “should not contain confidential data, as it can be recorded in logs or API objects, and is made available to admission webhooks.” The cache TTL is the trade-off knob: a revoked token keeps working for up to the TTL, and shortening it increases load on the webhook.

The same TokenReview API is what you can call, as a client with permission to create tokenreviews, to validate any bearer token the API server would accept. That is how you expand the bound-object claims of a ServiceAccount token, and it is how a sidecar or gateway can verify a token presented to it by another workload.

Authenticating reverse proxies

The API server can also trust a front-end proxy to have done the authentication and to tell it the result in HTTP headers. Because header spoofing would be trivial, the proxy must present a client certificate from a dedicated CA before any header is read.

FlagRequiredMeaning
--requestheader-client-ca-fileYesPEM bundle of CAs allowed to sign the proxy’s client certificate. A valid certificate must be presented before headers are consulted
--requestheader-allowed-namesNoComma-separated list of CNs. If set, only proxy certificates with those CNs are trusted; if empty, any CN under the CA is
--requestheader-username-headersYes, case-insensitiveHeaders checked in order; the first one with a value is the username
--requestheader-group-headersNo, case-insensitiveHeaders whose every value becomes a group. X-Remote-Group is suggested
--requestheader-extra-headers-prefixNo, case-insensitiveHeaders starting with a prefix become extras: prefix removed, remainder lower-cased and percent-decoded as the key. X-Remote-Extra- is suggested

With X-Remote-User, X-Remote-Group and X-Remote-Extra- configured, this request:

GET /api/v1/namespaces HTTP/1.1
X-Remote-User: fido
X-Remote-Group: dogs
X-Remote-Group: dachshunds
X-Remote-Extra-Acme.com%2Fproject: some-project
X-Remote-Extra-Scopes: openid

authenticates as user fido in groups dogs, dachshunds and system:authenticated, with extras acme.com/project: [some-project] and scopes: [openid].

The documentation repeats one warning twice, and the exam likes it: “If you have a certificate authority (CA) that is also used in a different context, do not trust that certificate authority to identify authenticating proxy clients.” The reason is that these same --requestheader-* flags are what the API aggregation layer uses: kubeadm configures --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt so that the API server trusts itself, as front-proxy-client, when it forwards requests to an aggregated API server. If front-proxy-ca.crt were the same CA that issues ordinary client certificates, every ordinary client could set X-Remote-User: kubernetes-admin and be believed. Keep the front-proxy CA separate and, in a hardened cluster, set --requestheader-allowed-names=front-proxy-client.

The documentation also draws the line between this and impersonation: with a proxy, “the API server trusts its direct client to provide information about the identity of the principal making the original request”; with impersonation, an authenticated user asks to be treated as someone else and must hold RBAC permission to do so.

Anonymous requests and per-endpoint anonymous access

When every configured authenticator declines a request, the anonymous authenticator assigns the username system:anonymous and the group system:unauthenticated. It is enabled by default whenever the authorization mode is anything other than AlwaysAllow, and the legacy switch is --anonymous-auth=false.

Three facts to keep straight:

  • An invalid bearer token is a 401, not an anonymous request. Only the absence of a recognised credential is anonymous.
  • Anonymous does not mean allowed. RBAC must explicitly grant something to system:anonymous or system:unauthenticated. Out of the box the only such grant is the system:public-info-viewer ClusterRole, bound to both system:authenticated and system:unauthenticated, which permits /healthz, /livez, /readyz and /version. Legacy rules that grant to * “do not automatically allow access to anonymous users.”
  • CIS check 1.2.1 asks that --anonymous-auth is set to false on the API server, and the CKS has historically tested exactly that edit. The trap is that on a kubeadm cluster the kube-apiserver static Pod’s livenessProbe, readinessProbe and startupProbe call /livez, /readyz and /livez over HTTPS with no credential. Turn anonymous off with the flag and those probes start returning 401; the kubelet then restarts a perfectly healthy API server on a loop.

The structured file solves that cleanly. The anonymous section, stable since v1.34 (the AnonymousAuthConfigurableEndpoints gate is locked on; it was alpha in v1.31 and beta in v1.32), lets you keep anonymous access only on named paths:

apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
anonymous:
  enabled: true
  conditions:
  - path: /livez
  - path: /readyz
  - path: /healthz

With this file, “any other endpoints will not be reachable anonymously, even if your authorization configuration would allow it”, and the probes keep working. One constraint: if the anonymous field is present in the file, you cannot also pass --anonymous-auth; the API server treats that as a misconfiguration. The documentation’s generic example of a structured file is simply anonymous: { enabled: false }, which is the equivalent of the flag.

Testing it from a control-plane node, where the API server listens on 6443:

curl -sk https://127.0.0.1:6443/livez          # ok (allowed path)
curl -sk https://127.0.0.1:6443/api/v1/pods    # 401 with the file above; 403 for system:anonymous without it

A 403 whose message names system:anonymous tells you anonymous authentication is on and RBAC refused; a 401 tells you authentication itself refused, which is the state you want on everything except the health endpoints.

Impersonation at the header level

Impersonation lets an authenticated subject ask the API server to evaluate the request as another identity. The request authenticates normally first, the API server checks that the caller may impersonate each requested attribute, and only then replaces the user info. Authorization then runs against the impersonated identity, and the audit log records both (user is the impersonator, impersonatedUser is the target). The RBAC privilege escalation guide covers why the impersonate verb is a cluster-admin-equivalent grant; this section covers the wire-level mechanics.

Headerkubectl flagRequiresRBAC to set it
Impersonate-User--asnothingimpersonate on users (core group), or on serviceaccounts for system:serviceaccount:... names
Impersonate-Group (repeatable)--as-groupImpersonate-Userimpersonate on groups (core group)
Impersonate-Uid--as-uid (kubectl 1.23+)Impersonate-User; server 1.22+impersonate on uids in authentication.k8s.io
Impersonate-Extra-<name> (repeatable)--as-user-extra (kubectl 1.35+)Impersonate-User; name lower-case and percent-encodedimpersonate on userextras/<name> in authentication.k8s.io

Scope rules: resourceNames restricts which users, groups, UIDs or extra values may be impersonated; because impersonating a user grants everything that user can do anywhere, the documentation notes that impersonation “is not namespace scoped” and must be granted with a ClusterRole and ClusterRoleBinding, except that impersonating a ServiceAccount can be granted in its namespace (while the impersonated ServiceAccount may still act outside it).

The classic debugging use is exactly what the exam expects you to reach for:

kubectl auth can-i delete pods -n payments --as=system:serviceaccount:payments:api
kubectl get pods -n payments --as=jane --as-group=developers
kubectl auth whoami --as=jane --as-group=developers    # shows the impersonated identity

Because the plain impersonate verb is all-or-nothing, v1.35 added constrained impersonation (ConstrainedImpersonation gate, beta and on by default since v1.36). It splits the grant in two: permission to impersonate a specific identity, and permission to perform specific verbs while impersonating. The new verbs live in the authentication.k8s.io group and come in three modes:

ModeApplies when Impersonate-User isIdentity verbAction verb pattern
user-infoA normal user (not system:serviceaccount: or system:node:)impersonate:user-info on users, groups, uids, userextras/<name>impersonate-on:user-info:<verb> on the target resource
ServiceAccountsystem:serviceaccount:<ns>:<name>impersonate:serviceaccount on serviceaccountsimpersonate-on:serviceaccount:<verb>
Nodesystem:node:<name>impersonate:arbitrary-node on nodes, or impersonate:associated-node (only the node named in the caller’s authentication.kubernetes.io/node-name extra)impersonate-on:arbitrary-node:<verb> or impersonate-on:associated-node:<verb>

The documented pattern is a node agent that may impersonate only its own node to get and list Pods on it, without cluster-wide Pod access. For the exam, know that the old impersonate verb still grants unlimited impersonation and that constrained impersonation is the least-privilege alternative; do not confuse the two sets of verbs.

client-go exec credential plugins

The last authenticator-adjacent mechanism runs on the client. A kubeconfig user entry can specify an exec block, and kubectl (or anything built on client-go, including the kubelet) runs that command to obtain credentials. It is stable since v1.22 and is how almost every managed-cloud and enterprise SSO integration works today; the documentation notes that the old built-in cloud auth providers were removed from kubectl.

users:
- name: sso-user
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1   # plugin output MUST use this version
      command: ./bin/example-client-go-exec-plugin   # relative paths resolve against the kubeconfig's directory
      args: ["--cluster", "prod"]
      env:
      - name: IDP_URL
        value: https://idp.example.com
      interactiveMode: Never        # Never | IfAvailable | Always; required in v1, defaults to IfAvailable in v1beta1
      provideClusterInfo: true      # passes server, CA data and the cluster's exec extension to the plugin
      installHint: "Install the plugin with: brew install example-client-go-exec-plugin"

The contract: the plugin receives an ExecCredential object in the KUBERNETES_EXEC_INFO environment variable (including spec.interactive and, with provideClusterInfo, spec.cluster), and prints an ExecCredential to stdout whose status carries either token or both clientCertificateData and clientKeyData, optionally with an expirationTimestamp. If an expiry is present, the credential is cached until then, until a 401, or until the process exits; without one it is cached until a 401 or exit. If the plugin returns a different certificate on a later call, client-go closes its connections to force a new TLS handshake.

Two security points follow from the design. First, almost every exec plugin pairs with a webhook token authenticator on the server, because the token it mints is in a format only the organisation’s service understands. Second, a kubeconfig is now executable configuration: exec.command runs with your privileges the moment you run kubectl. A kubeconfig received from an untrusted source, or committed to a repository, must be read before it is used, and interactiveMode plus installHint are the only guard-rails client-go applies.

Putting it together on a kubeadm cluster

On a kubeadm control plane every authenticator is configured through the static Pod manifest at /etc/kubernetes/manifests/kube-apiserver.yaml. The default authentication-related arguments look like this:

- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --enable-bootstrap-token-auth=true
- --service-account-issuer=https://kubernetes.default.svc.cluster.local
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --requestheader-allowed-names=front-proxy-client
- --requestheader-username-headers=X-Remote-User
- --requestheader-group-headers=X-Remote-Group
- --requestheader-extra-headers-prefix=X-Remote-Extra-
- --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
- --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key

Note what is absent: no --anonymous-auth (so it is on), no --token-auth-file, no --oidc-*, no --authentication-config. A ten-minute lab that exercises the whole chain:

  1. Record the baseline. kubectl auth whoami as the admin, then curl -sk https://127.0.0.1:6443/version and curl -sk https://127.0.0.1:6443/api from the control-plane node to see the 200 and the 403 for system:anonymous.
  2. Write the configuration file at /etc/kubernetes/authn/config.yaml with the anonymous section restricted to /livez, /readyz and /healthz. Keep it a plain file, not a ConfigMap; the API server reads it from disk.
  3. Mount it into the static Pod. Add --authentication-config=/etc/kubernetes/authn/config.yaml to the command, a hostPath volume for /etc/kubernetes/authn, and a matching read-only volumeMount. Do not add --anonymous-auth. Save, and watch crictl ps on the node until the new kube-apiserver container is running.
  4. Verify the behaviour change. /livez still returns ok; /api now returns 401 instead of 403. The probes keep passing.
  5. Add a JWT authenticator to the same file with a username.prefix of oidc: and the two system: user-validation rules, then confirm the reload without a restart by checking kubectl get --raw /metrics | grep apiserver_authentication_config_controller_automatic_reload.
  6. Inspect a Pod token. kubectl create token default --bound-object-kind Pod --bound-object-name <pod>, split the JWT on dots and base64-decode the middle part, then submit it in a TokenReview and compare the status.user.extra keys with the raw claims.
  7. Test impersonation end to end. Create a ClusterRole that allows impersonate on users with resourceNames: ["auditor"], bind it to a test ServiceAccount, and run kubectl auth whoami --as=auditor with that ServiceAccount’s token. Then try --as=admin and read the 403.
  8. Clean up by removing the flag and mount, or keep the file and extend it; either way check that /livez is healthy before leaving the node.

The CKS practice environment guide explains how to build a disposable kubeadm cluster to run exercises like this safely, and the cluster setup and API server hardening guide covers the neighbouring kube-apiserver flags that the CIS Benchmark checks.

Diagnosing authentication failures

SymptomMost likely causeHow to confirm
Unauthorized (401) for a client certificate that looks validThe certificate was signed by a CA that is not in --client-ca-file, has expired, or lacks the ClientAuth EKUopenssl x509 -noout -issuer -enddate -ext extendedKeyUsage; compare the issuer with /etc/kubernetes/pki/ca.crt
Forbidden (403) for a certificate user with correct-looking RBACThe O= groups in the certificate are not what the bindings namekubectl auth whoami with that kubeconfig; check the group list
User https://idp.example.com#abc123 appears in audit logsOIDC username prefix defaulted to the issuer URLSet --oidc-username-prefix or claimMappings.username.prefix and update bindings
OIDC token rejected, API server log says the groups claim is invalidThe groups claim is a string, not an array of stringsDecode the token; fix the provider or use a CEL expression such as claims.roles.split(",")
kube-apiserver exits immediately after adding --authentication-config--oidc-* flags are still presentRemove every --oidc-* argument; the two are mutually exclusive
kube-apiserver restarts every few seconds after --anonymous-auth=falseHealth probes now receive 401Use the anonymous.conditions section of the configuration file instead of the flag
Edited static token file, old token still works or new token failsThe file is read only at start-upRestart the API server (touch the static Pod manifest)
kubeadm join fails with an unauthorized errorBootstrap token expired or deleted, or --enable-bootstrap-token-auth is missingkubeadm token list; check the expiration field of the Secret
ServiceAccount token stops working after a Pod is recreatedThe token was bound to the old Pod’s UIDExpected behaviour; the new Pod has a new projected token
Legacy Secret token still works after the Secret was deleted--service-account-lookup=falseRe-enable lookup (the default is true)
Requests through a proxy arrive as system:anonymousThe proxy’s client certificate is not signed by --requestheader-client-ca-file, or its CN is not in --requestheader-allowed-namesCheck the proxy certificate and the allowed-names list
Every client can set X-Remote-User and be believedThe request-header CA is the same CA as the client CAIssue a dedicated front-proxy CA
kubectl --as returns 403 ... cannot impersonate resource "users"Caller lacks the impersonate verb (or the constrained equivalent)kubectl auth can-i impersonate users
Webhook token authenticator keeps accepting a revoked tokenDecision cachedWait for --authentication-token-webhook-cache-ttl (default 2m) or lower it
kubectl says the exec plugin is not foundRelative command path resolves against the kubeconfig directory, not the shellUse an absolute path or fix the relative one

Scenario drills

Scenario phraseDiscriminatorAnswer
”Users authenticate but every request is forbidden; the bindings name the correct usernames.”Mapping, not authenticationkubectl auth whoami; fix group or username prefix
”Two identity providers must both be trusted for the same cluster.”More than one issuerStructured AuthenticationConfiguration with two jwt entries (the flags allow one)
“No external token may ever map to a system: user or group.”Validate the final identityuserValidationRules with !user.username.startsWith('system:') and the groups equivalent
”Reject tokens whose lifetime exceeds 24 hours.”Claim-level ruleclaimValidationRules expression claims.exp - claims.nbf <= 86400
”Disable anonymous API access without breaking the API server’s liveness probe.”Per-endpoint anonymousanonymous.enabled: true with conditions for /livez, /readyz, /healthz; no --anonymous-auth flag
”The API server crash-loops after an authentication change.”Mutually exclusive settingsRemove --oidc-* when using --authentication-config, or remove --anonymous-auth when the file has an anonymous section
”A developer’s client certificate was leaked. Revoke it.”No revocation existsRemove or rename the RBAC subjects, wait for expiry, or rotate the client CA
”A ServiceAccount token leaked. Revoke only that account’s tokens.”UID bindingDelete and recreate the ServiceAccount
”A token leaked from a Pod. Identify which Pod and node.”Extra attributesauthentication.kubernetes.io/pod-name, pod-uid, node-name in audit logs or a TokenReview
”An external service verifies our ServiceAccount JWTs offline and still accepts tokens of deleted Pods.”Offline verification does not check bindingsSubmit the token in a TokenReview instead of trusting JWKS verification alone
”A bootstrap token created with --ttl 0 is in the cluster.”Never expiresDelete it with kubeadm token delete; create short-TTL tokens per join
”Monitoring agent must authenticate with a long-lived static credential; rotation must not need a restart.”Static token file cannot rotate liveWebhook token authentication, or a ServiceAccount token bound to a Secret
”A proxy performs SSO and passes the username in a header.”Authenticating proxy--requestheader-* flags with a dedicated CA and --requestheader-allowed-names
”Admin wants to test what a ServiceAccount can do without extracting its token.”Impersonationkubectl auth can-i ... --as=system:serviceaccount:<ns>:<name>
”A controller may act as the user jane, but only to list and watch Pods in one namespace.”Scoped impersonationConstrained impersonation: impersonate:user-info on users/jane plus impersonate-on:user-info:list and :watch on Pods in that namespace
”An extra attribute from the IdP must be available to the authorization webhook.”Extras from tokensclaimMappings.extra with a domain-prefixed key (not under k8s.io)
“Discovery for the IdP is only reachable inside the cluster network.”Egressissuer.discoveryURL and egressSelectorType: cluster (gate beta, on by default since v1.34)
“Rotate the ServiceAccount signing key without invalidating running workloads.”Verify-with-many, sign-with-oneAdd the new public key to --service-account-key-file, restart, switch the signing key, restart, remove the old key after the longest token lifetime
”A kubeconfig from a vendor contains an exec block.”Executable configurationRead the command before using the file; it runs with your privileges
”A CI job presents a token and the API server returns 401, while a Pod token works.”Issuer or audience mismatchCompare iss and aud with --service-account-issuer and --api-audiences

Common mistakes

  • Assuming authenticators run in a fixed order. The first success wins, and the order is not guaranteed; design so that each credential type is unambiguous.
  • Treating system:masters as something RBAC can restrict. It is honoured by a built-in binding; the only fix for a certificate that carries it is expiry or CA rotation.
  • Forgetting the username prefix. Without one, an OIDC sub claim becomes <issuer>#<sub>, and an email claim becomes a bare string that could collide with reserved names.
  • Mixing --oidc-* flags with --authentication-config, or --anonymous-auth with an anonymous section. Both combinations make the API server exit.
  • Disabling anonymous access with the flag on a kubeadm cluster. The health probes break. Use anonymous.conditions.
  • Expecting a static token file edit to take effect. It is read at start-up only.
  • Believing a bound ServiceAccount token can be revoked individually. It cannot; delete the bound object, recreate the ServiceAccount, or rotate keys.
  • Trusting offline JWKS verification to enforce Pod bindings. It checks signature and expiry only; the TokenReview API checks the bindings.
  • Reusing the cluster CA as the request-header CA. Every client could then spoof identity headers.
  • Granting the impersonate verb when constrained impersonation would do. The plain verb is unlimited; the impersonate:/impersonate-on: pair is scoped.
  • Confusing 401 with 403. A 401 means no authenticator accepted the credential; a 403 naming system:anonymous means anonymous access is on and RBAC said no.
  • Running a kubeconfig without reading its exec block. The command runs locally with your privileges.

Practising this for the CKS

The CKS is a performance-based exam on live clusters, so these mechanics are tested as tasks: harden the API server flags, fix a kubeconfig, find and remove an over-privileged identity, make anonymous access safe without breaking probes. The way to get fast is to do the tasks repeatedly on a disposable cluster until the manifest edits and the verification commands are automatic. The free CKS practice guide shows how to set that up at no cost. When you want to measure readiness under exam conditions, Sailor.sh’s CKS Mock Exam Bundle includes five full-length performance mock exams with cluster-hardening tasks of this kind, scored against the real curriculum weights, with explanations that show the exact manifest changes and the verification steps. Use it after the lab above feels routine, not before.

Frequently Asked Questions

What is the order in which the Kubernetes API server tries its authenticators?

There is no guaranteed order. The documentation states that the API server does not guarantee the order authenticators run in, and that the first one to succeed short-circuits the rest. Each credential type is handled only by the authenticators that understand it, so in practice a client certificate and a bearer token never compete, but two bearer-token authenticators might, and your configuration should not rely on one being consulted first.

How do I revoke a Kubernetes client certificate?

You cannot. Kubernetes v1.37 does not support certificate revocation, so a certificate remains valid until its notAfter date. Your options are to remove every RBAC binding that references the certificate’s username and groups, to let it expire, or to rotate the client CA, which invalidates all certificates issued by the old CA at once. This is the main reason the project recommends short-lived tokens for human users.

Why does my OIDC user show up as https://issuer#1234 instead of their name?

Because --oidc-username-prefix was not set and the username claim is something other than email. In that case Kubernetes prefixes the claim with the issuer URL and a # to prevent collisions. Set an explicit prefix such as oidc:, or - to disable prefixing, or in the structured configuration file set claimMappings.username.prefix explicitly (it is required whenever claim is used).

What is the difference between the --oidc-* flags and the AuthenticationConfiguration file?

The flags configure exactly one JWT issuer with one audience and simple claim-to-attribute mapping. The file, stable since v1.34 and enabled with --authentication-config, supports up to 64 issuers, multiple audiences per issuer, CEL expressions for claim validation and claim mapping, UID and extra attributes, validation rules on the final user object, a separate discovery URL, and automatic reload when the file changes. Using both at the same time is an error that makes the API server exit.

How do I disable anonymous access to the API server without breaking its health probes?

Use the anonymous section of the AuthenticationConfiguration file rather than --anonymous-auth=false. Set enabled: true and list conditions with the paths /livez, /readyz and /healthz. Only those paths remain reachable anonymously, everything else returns 401, and the kube-apiserver static Pod’s probes keep succeeding. Do not pass --anonymous-auth alongside the file; the two are mutually exclusive.

Can I revoke a single ServiceAccount token issued by the TokenRequest API?

No. There is no central record of issued tokens. A token bound to a Pod or Secret stops working when that object is deleted, and you can invalidate every token of a ServiceAccount by deleting and recreating the ServiceAccount, which changes its UID. Rotating the signing key and removing the old verification key invalidates everything signed with it.

What does kubectl auth whoami show, and who can run it?

It submits a SelfSubjectReview and prints the username, UID, groups and extra attributes that the API server’s authenticators produced for your credential, after any impersonation headers are applied. Nothing is stored. Every authenticated subject can use it because the built-in system:basic-user ClusterRole allows creating selfsubjectreviews.

How is an authenticating proxy different from impersonation?

With an authenticating proxy the API server trusts its direct client, identified by a certificate from the --requestheader-client-ca-file CA, to state the original caller’s identity in headers such as X-Remote-User; the proxy did the authentication. With impersonation, a fully authenticated user adds Impersonate-User and related headers and must hold the impersonate verb (or the constrained equivalents) for each attribute; the API server authenticates the caller first and only then switches identity, recording both in the audit log.

Conclusion

Authentication is where every Kubernetes request stops being bytes on a socket and becomes a username, a UID, a list of groups and a map of extras. The authenticators that produce that record are few, and each has sharp edges: certificates that cannot be revoked, a static token file that cannot be reloaded, bootstrap tokens that are node-join credentials, ServiceAccount JWTs whose bindings only the TokenReview API checks, OIDC prefixes that silently rewrite usernames, a request-header CA that must never be shared, and an anonymous switch that takes the health probes down with it. The structured AuthenticationConfiguration file, now stable, is the modern answer to most of those edges: multiple issuers, CEL validation that forbids system: names, per-endpoint anonymous access, and reload without a restart.

For the CKS, the skill being measured is reading an API server manifest and knowing, flag by flag, who it will believe and why. Once that is instinct, the Cluster Hardening tasks become edits you can make in two minutes and verify in one, with kubectl auth whoami as the final word on whether the cluster sees the identity you intended.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now