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.
| Attribute | Type | Typical values | Who consumes it |
|---|---|---|---|
| Username | string | [email protected], system:serviceaccount:dev:ci, system:node:worker-1 | RBAC User subjects, audit logs, admission |
| UID | string | A UUID, an employee number, or empty | Impersonation, audit, some webhooks |
| Groups | list of strings | developers, system:masters, system:authenticated | RBAC Group subjects |
| Extra | map of string to list of strings | example.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.
- Every successful authentication adds the group
system:authenticated. No authenticator has to do this; the API server does it automatically. - The first authenticator that succeeds wins. In the documentation’s words, “the first module to successfully authenticate the request short-circuits evaluation.”
- 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-userClusterRole grantscreateonselfsubjectreviews(alongsideselfsubjectaccessreviewsandselfsubjectrulesreviews). - Nothing is persisted. The object is filled in on the
POSTand discarded; you cannotkubectl 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.
| Name | Kind | Produced by | Meaning |
|---|---|---|---|
system:authenticated | group | API server, every successful authentication | Anyone who proved an identity |
system:unauthenticated | group | Anonymous authenticator | Requests with no accepted credential |
system:anonymous | user | Anonymous authenticator | The username of such requests |
system:masters | group | Whatever certificate or token carries it | Bound to cluster-admin by a built-in binding; cannot be restricted by RBAC |
system:serviceaccount:<ns>:<name> | user | ServiceAccount token authenticator | A workload identity |
system:serviceaccounts, system:serviceaccounts:<ns> | groups | ServiceAccount token authenticator | All service accounts, and those in one namespace |
system:node:<nodeName> | user | The kubelet’s client certificate | A node identity, consumed by the Node authorizer |
system:nodes | group | The kubelet’s client certificate (O=system:nodes) | All kubelets |
system:bootstrap:<tokenID> | user | Bootstrap token authenticator | A joining node before it has a certificate |
system:bootstrappers, system:bootstrappers:<suffix> | groups | Bootstrap token authenticator | Used by kubeadm’s CSR auto-approval RBAC |
system:kube-controller-manager, system:kube-scheduler, system:kube-proxy | users | Control-plane client certificates | Bound 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 attribute | OID | Becomes |
|---|---|---|
commonName (CN) | 2.5.4.3 | Username |
organization (O), repeated | 2.5.6.4 | One group per O value, order irrelevant |
| Kubernetes user ID attribute | 1.3.6.1.4.1.57683.2 | UID (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.
| Property | Value |
|---|---|
| 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 |
| Storage | A 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 cleanup | The tokencleaner controller, enabled with --controllers=*,tokencleaner on kube-controller-manager |
| Authenticates as | User system:bootstrap:<tokenID>, group system:bootstrappers, plus any auth-extra-groups |
| Extra groups rule | Every extra group must start with system:bootstrappers: |
| Usage switches | usage-bootstrap-authentication: "true" allows bearer-token use; usage-bootstrap-signing: "true" allows signing the cluster-info ConfigMap |
| Expiration | Optional 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:bootstrappersand 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 inittokens default to a 24-hour TTL; a token created withkubeadm token create --ttl 0never 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 thecluster-infoConfigMap inkube-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
| Flag | Purpose | Detail that becomes a question |
|---|---|---|
--service-account-signing-key-file | Private key used to sign new tokens | Changes to the file are not re-read; a restart is required. kubeadm uses /etc/kubernetes/pki/sa.key |
--service-account-key-file | PEM public or private keys (RSA or ECDSA) used to verify tokens | The 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-issuer | The iss claim stamped into every token | Should 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-audiences | Audiences accepted for the API server itself | Defaults to the issuer value when unset |
--service-account-max-token-expiration | Ceiling on the lifetime a TokenRequest may ask for | A Kubernetes duration string |
--service-account-extend-token-expiration | Stretches projected-volume tokens to one year while still rotating them hourly | A migration aid for clients that never re-read the token file; the extended value is min(1 year, signer maximum) |
--service-account-lookup | Validate that the backing Secret still exists in etcd for legacy tokens | Deleting the Secret then revokes the token |
--service-account-jwks-uri | Overrides the JWKS URI advertised in the discovery document | Needed when the API server is reached through a different public address |
--service-account-signing-endpoint | Socket of an external JWT signer | Mutually 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
nodeblock 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
ServiceAccountTokenPodNodeInfoandServiceAccountTokenJTIgates are GA), the authenticator also surfaces these values as extra attributes:authentication.kubernetes.io/pod-name,pod-uid,node-name,node-uidandcredential-id(thejti). Those extras appear in audit logs and inkubectl 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 invalidate | Do this |
|---|---|
| Every token of one ServiceAccount | Delete and recreate the ServiceAccount; the UID changes and every token fails the UID check |
| One bound token | Delete the object it is bound to (the Pod, or the Secret you bound it to on purpose) |
| A legacy Secret-based token | Delete the Secret, with --service-account-lookup on (the default) |
| Everything, including tokens whose binding cannot be removed | Rotate 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
| Flag | Required | Meaning and default |
|---|---|---|
--oidc-issuer-url | Yes | The 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-id | Yes | The aud every token must be issued for |
--oidc-username-claim | No | Claim used as the username. Default sub |
--oidc-username-prefix | No | Prefix 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-claim | No | Claim used for groups. Must be an array of strings in the token. No default |
--oidc-groups-prefix | No | Prefix added to every group, for example oidc: |
--oidc-required-claim | No | key=value that must be present. Repeatable |
--oidc-ca-file | No | CA for the provider’s TLS certificate. Defaults to the host’s root CAs |
--oidc-signing-algs | No | Accepted 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:
| Rule | Detail |
|---|---|
| Authenticator count | At most 64 entries in jwt |
issuer.url | https:// only, unique across all authenticators, and must not conflict with the ServiceAccount issuer |
issuer.discoveryURL | Must 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 |
| Audiences | At least one aud entry must match; audienceMatchPolicy: MatchAny is required when more than one audience is listed |
claimValidationRules | Either claim/requiredValue or a CEL expression returning a boolean. All rules must pass. message customises the API server log line |
claimMappings.username | Required. Either claim + prefix (prefix mandatory, "" for none) or a CEL expression returning a string. An empty result fails authentication |
The email rule | If 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.groups | claim + prefix or a CEL expression returning a string or list of strings |
claimMappings.uid | claim or expression |
claimMappings.extra | Keys must be lower-case domain-prefixed paths such as example.org/foo, unique, and never under k8s.io or kubernetes.io |
userValidationRules | CEL over the final user object; all must pass |
| CEL variables | claims (the token payload as a map) in claim rules and mappings; user (the UserInfo) in user validation rules |
| Distributed claims | Not 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:
| Flag | Meaning | Default |
|---|---|---|
--authentication-token-webhook-config-file | A kubeconfig-format file: clusters describes the remote service (URL and CA); users describes the API server’s own client certificate for the call | none |
--authentication-token-webhook-cache-ttl | How long a decision is cached | 2 minutes |
--authentication-token-webhook-version | authentication.k8s.io/v1beta1 or authentication.k8s.io/v1 TokenReview objects | v1beta1 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.
| Flag | Required | Meaning |
|---|---|---|
--requestheader-client-ca-file | Yes | PEM bundle of CAs allowed to sign the proxy’s client certificate. A valid certificate must be presented before headers are consulted |
--requestheader-allowed-names | No | Comma-separated list of CNs. If set, only proxy certificates with those CNs are trusted; if empty, any CN under the CA is |
--requestheader-username-headers | Yes, case-insensitive | Headers checked in order; the first one with a value is the username |
--requestheader-group-headers | No, case-insensitive | Headers whose every value becomes a group. X-Remote-Group is suggested |
--requestheader-extra-headers-prefix | No, case-insensitive | Headers 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:anonymousorsystem:unauthenticated. Out of the box the only such grant is thesystem:public-info-viewerClusterRole, bound to bothsystem:authenticatedandsystem:unauthenticated, which permits/healthz,/livez,/readyzand/version. Legacy rules that grant to*“do not automatically allow access to anonymous users.” - CIS check 1.2.1 asks that
--anonymous-authis set tofalseon 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’slivenessProbe,readinessProbeandstartupProbecall/livez,/readyzand/livezover HTTPS with no credential. Turn anonymous off with the flag and those probes start returning401; 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.
| Header | kubectl flag | Requires | RBAC to set it |
|---|---|---|---|
Impersonate-User | --as | nothing | impersonate on users (core group), or on serviceaccounts for system:serviceaccount:... names |
Impersonate-Group (repeatable) | --as-group | Impersonate-User | impersonate 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-encoded | impersonate 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:
| Mode | Applies when Impersonate-User is | Identity verb | Action verb pattern |
|---|---|---|---|
| user-info | A 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 |
| ServiceAccount | system:serviceaccount:<ns>:<name> | impersonate:serviceaccount on serviceaccounts | impersonate-on:serviceaccount:<verb> |
| Node | system: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:
- Record the baseline.
kubectl auth whoamias the admin, thencurl -sk https://127.0.0.1:6443/versionandcurl -sk https://127.0.0.1:6443/apifrom the control-plane node to see the200and the403forsystem:anonymous. - Write the configuration file at
/etc/kubernetes/authn/config.yamlwith theanonymoussection restricted to/livez,/readyzand/healthz. Keep it a plain file, not a ConfigMap; the API server reads it from disk. - Mount it into the static Pod. Add
--authentication-config=/etc/kubernetes/authn/config.yamlto the command, ahostPathvolume for/etc/kubernetes/authn, and a matching read-onlyvolumeMount. Do not add--anonymous-auth. Save, and watchcrictl pson the node until the new kube-apiserver container is running. - Verify the behaviour change.
/livezstill returnsok;/apinow returns401instead of403. The probes keep passing. - Add a JWT authenticator to the same file with a
username.prefixofoidc:and the twosystem:user-validation rules, then confirm the reload without a restart by checkingkubectl get --raw /metrics | grep apiserver_authentication_config_controller_automatic_reload. - 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 aTokenReviewand compare thestatus.user.extrakeys with the raw claims. - Test impersonation end to end. Create a ClusterRole that allows
impersonateonuserswithresourceNames: ["auditor"], bind it to a test ServiceAccount, and runkubectl auth whoami --as=auditorwith that ServiceAccount’s token. Then try--as=adminand read the403. - Clean up by removing the flag and mount, or keep the file and extend it; either way check that
/livezis 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
| Symptom | Most likely cause | How to confirm |
|---|---|---|
Unauthorized (401) for a client certificate that looks valid | The certificate was signed by a CA that is not in --client-ca-file, has expired, or lacks the ClientAuth EKU | openssl x509 -noout -issuer -enddate -ext extendedKeyUsage; compare the issuer with /etc/kubernetes/pki/ca.crt |
Forbidden (403) for a certificate user with correct-looking RBAC | The O= groups in the certificate are not what the bindings name | kubectl auth whoami with that kubeconfig; check the group list |
User https://idp.example.com#abc123 appears in audit logs | OIDC username prefix defaulted to the issuer URL | Set --oidc-username-prefix or claimMappings.username.prefix and update bindings |
| OIDC token rejected, API server log says the groups claim is invalid | The groups claim is a string, not an array of strings | Decode 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 present | Remove every --oidc-* argument; the two are mutually exclusive |
kube-apiserver restarts every few seconds after --anonymous-auth=false | Health probes now receive 401 | Use the anonymous.conditions section of the configuration file instead of the flag |
| Edited static token file, old token still works or new token fails | The file is read only at start-up | Restart the API server (touch the static Pod manifest) |
kubeadm join fails with an unauthorized error | Bootstrap token expired or deleted, or --enable-bootstrap-token-auth is missing | kubeadm token list; check the expiration field of the Secret |
| ServiceAccount token stops working after a Pod is recreated | The token was bound to the old Pod’s UID | Expected behaviour; the new Pod has a new projected token |
| Legacy Secret token still works after the Secret was deleted | --service-account-lookup=false | Re-enable lookup (the default is true) |
Requests through a proxy arrive as system:anonymous | The proxy’s client certificate is not signed by --requestheader-client-ca-file, or its CN is not in --requestheader-allowed-names | Check the proxy certificate and the allowed-names list |
Every client can set X-Remote-User and be believed | The request-header CA is the same CA as the client CA | Issue 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 token | Decision cached | Wait for --authentication-token-webhook-cache-ttl (default 2m) or lower it |
kubectl says the exec plugin is not found | Relative command path resolves against the kubeconfig directory, not the shell | Use an absolute path or fix the relative one |
Scenario drills
| Scenario phrase | Discriminator | Answer |
|---|---|---|
| ”Users authenticate but every request is forbidden; the bindings name the correct usernames.” | Mapping, not authentication | kubectl auth whoami; fix group or username prefix |
| ”Two identity providers must both be trusted for the same cluster.” | More than one issuer | Structured AuthenticationConfiguration with two jwt entries (the flags allow one) |
“No external token may ever map to a system: user or group.” | Validate the final identity | userValidationRules with !user.username.startsWith('system:') and the groups equivalent |
| ”Reject tokens whose lifetime exceeds 24 hours.” | Claim-level rule | claimValidationRules expression claims.exp - claims.nbf <= 86400 |
| ”Disable anonymous API access without breaking the API server’s liveness probe.” | Per-endpoint anonymous | anonymous.enabled: true with conditions for /livez, /readyz, /healthz; no --anonymous-auth flag |
| ”The API server crash-loops after an authentication change.” | Mutually exclusive settings | Remove --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 exists | Remove or rename the RBAC subjects, wait for expiry, or rotate the client CA |
| ”A ServiceAccount token leaked. Revoke only that account’s tokens.” | UID binding | Delete and recreate the ServiceAccount |
| ”A token leaked from a Pod. Identify which Pod and node.” | Extra attributes | authentication.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 bindings | Submit the token in a TokenReview instead of trusting JWKS verification alone |
”A bootstrap token created with --ttl 0 is in the cluster.” | Never expires | Delete 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 live | Webhook 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.” | Impersonation | kubectl 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 impersonation | Constrained 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 tokens | claimMappings.extra with a domain-prefixed key (not under k8s.io) |
| “Discovery for the IdP is only reachable inside the cluster network.” | Egress | issuer.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-one | Add 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 configuration | Read 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 mismatch | Compare 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:mastersas 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
subclaim becomes<issuer>#<sub>, and anemailclaim becomes a bare string that could collide with reserved names. - Mixing
--oidc-*flags with--authentication-config, or--anonymous-authwith ananonymoussection. 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
TokenReviewAPI checks the bindings. - Reusing the cluster CA as the request-header CA. Every client could then spoof identity headers.
- Granting the
impersonateverb when constrained impersonation would do. The plain verb is unlimited; theimpersonate:/impersonate-on:pair is scoped. - Confusing 401 with 403. A
401means no authenticator accepted the credential; a403namingsystem:anonymousmeans anonymous access is on and RBAC said no. - Running a kubeconfig without reading its
execblock. 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.