The legacy --oidc-* flags let a Kubernetes API server trust exactly one JWT issuer with one client ID and one claim for the username, so most clusters either push every human through a single IdP or hand CI pipelines long-lived kubeconfig credentials. Structured authentication configuration replaces those flags with a versioned AuthenticationConfiguration file that can trust up to 64 JWT issuers, validate claims with CEL expressions, map identities precisely and reload without restarting the API server. This guide explains the file field by field, shows how to use it to let GitHub Actions and GitLab CI jobs authenticate with short-lived OIDC tokens, and covers rollout, detection and the mistakes that turn a flexible authenticator into a cluster-wide bypass. It is written for platform engineers and cluster operators who run their own control plane or a distribution that exposes API server flags.
Key takeaways
- It is stable and the flags are now the legacy path. The
StructuredAuthenticationConfigurationfeature was alpha in v1.29, beta in v1.30 and stable in v1.34, with the gate locked on; the file is passed with--authentication-config. - You cannot mix the two models. Setting
--authentication-configtogether with any--oidc-*flag makes the API server report an error and exit, so migration is a cut-over, not a blend. - CEL gives you policy at the front door.
claimValidationRulesreject tokens before mapping,claimMappingsbuild username, groups, UID and extra attributes, anduserValidationRulesreject the final identity, for example any username starting withsystem:. - CI identity becomes keyless. A pipeline's OIDC token can be trusted directly by the API server and mapped to a narrow Kubernetes identity, replacing static service account tokens stored as CI secrets.
- Most risk sits in the mapping, not the crypto. A missing audience check, a username claim that a tenant controls, or a CI rule that accepts any branch gives an attacker an authenticated identity with whatever RBAC you bound to it.
What changed from the --oidc flags
KEP-3331, which introduced the feature, describes the flag-based configuration as growing without bounds and lists what operators kept asking for: multiple client IDs, identifying users by more than one claim, mapping arbitrary claims to extra attributes, requiring group membership and running more than one provider without an external multiplexing proxy. The structured file answers each of these.
| Need | Legacy flag behaviour | Structured configuration |
|---|---|---|
| Number of issuers | One (--oidc-issuer-url) | Up to 64 entries under jwt, each with a unique issuer.url |
| Audiences | One exact match (--oidc-client-id) | audiences list with audienceMatchPolicy: MatchAny |
| Required claims | --oidc-required-claim key=value | claimValidationRules with claim/requiredValue or a boolean CEL expression |
| Username | One claim plus prefix | Claim plus prefix, or a CEL expression combining claims |
| Groups | One claim plus prefix | Claim plus prefix, or CEL returning a string or list |
| UID and extra attributes | Not available | uid and extra[] with domain-prefixed keys |
| Final identity checks | Not available | userValidationRules evaluated against the user object |
| Change without restart | No | File is re-read automatically when it changes |
| Anonymous access | --anonymous-auth on or off for all paths | anonymous.conditions limits anonymous requests to listed paths such as /livez, /readyz, /healthz |
The file uses apiVersion: apiserver.config.k8s.io/v1 and kind: AuthenticationConfiguration. If you set anonymous in the file you cannot also set --anonymous-auth. Configurable anonymous endpoints (AnonymousAuthConfigurableEndpoints) also reached stable in v1.34. Managed services generally do not expose API server flags, so check your provider's own documentation for what it supports before planning around this file.
The issuer block, field by field
- url: Required. Must match the token's
issclaim, must be unique across all authenticators and must not conflict with the issuer set in--service-account-issuer. - discoveryURL: Optional override for
{url}/.well-known/openid-configuration. The exact value is used, so include the well-known path. It must differ fromurland be unique, and theissuerfield returned by discovery must still matchurl. - certificateAuthority: PEM CA bundle for fetching discovery and keys, the same content as
--oidc-ca-file. Leave it unset only when the IdP uses a publicly trusted certificate. - audiences and audienceMatchPolicy: At least one listed audience must appear in the token's
aud. With more than one audience the policy must beMatchAny. Use a dedicated audience per cluster, never a generic value shared with other relying parties. - egressSelectorType: Optional;
controlplaneorcluster, matching your--egress-selector-config-file, for routing discovery, JWKS and distributed-claim traffic. It depends on theStructuredAuthenticationConfigurationEgressSelectorgate, beta in v1.34.
The minimum token the API server will accept carries iss, aud, exp and the claim used for the username. When several authenticators are configured, the first one to succeed wins and the documentation states that order is not guaranteed, so never rely on ordering to resolve overlapping issuers.
CEL claim rules and identity mapping
CEL expressions in claimValidationRules, claimMappings and extra[].valueExpression see a claims map; expressions in userValidationRules see a user object (the resulting UserInfo). Distributed claims are not available to CEL.
| Stage | Field | Must return | Typical use |
|---|---|---|---|
| 1. Token checks | claimValidationRules[].expression | Boolean; all must be true | Tenant or hosted-domain check, maximum token lifetime (claims.exp - claims.nbf <= 86400 in the upstream example), CI branch or environment restrictions |
| 2. Username | claimMappings.username | String; empty fails authentication | Prefixed subject, for example "gha:" + claims.sub |
| 2. Groups | claimMappings.groups | String or list of strings | Prefixed group names for RBAC bindings |
| 2. UID | claimMappings.uid | String | Immutable subject identifier for audit correlation |
| 2. Extra | claimMappings.extra[] | String or list | Carry repository ID, environment or tenant to authorisation webhooks and audit logs |
| 3. Identity checks | userValidationRules[].expression | Boolean; all must be true | Block system: usernames and groups |
Three rules in the reference documentation catch people out:
- Prefixes are mandatory with claim mapping: when
username.claimorgroups.claimis set, aprefixis required; set it to an empty string deliberately if you mean none. Use a distinct prefix per issuer (corp:,gha:,gitlab:) so two issuers can never produce the same username. - Email needs verification: if the username expression uses
claims.email, the configuration must also referenceclaims.email_verifiedsomewhere (username expression, an extra value or a validation rule). - Extra keys are namespaced: keys must be lowercase, unique and domain-prefixed (
example.com/tenant);k8s.io,kubernetes.ioand their subdomains are reserved.
Treat userValidationRules as a mandatory backstop. The upstream example uses !user.username.startsWith('system:') and user.groups.all(group, !group.startsWith('system:')); without them, a mapping mistake or an IdP group an administrator can rename could yield membership of system:masters, which bypasses RBAC entirely.
Keyless CI identity: GitHub Actions and GitLab CI
CI platforms already issue OIDC tokens to jobs. Trusting them directly removes the most common cluster credential leak: a service account token or kubeconfig stored as a pipeline secret. The key design decision is which claims prove "this exact pipeline, on this protected ref, deploying to this environment".
| Property | GitHub Actions | GitLab CI |
|---|---|---|
Issuer (issuer.url) | https://token.actions.githubusercontent.com | Your GitLab instance URL, for example https://gitlab.example.com |
| Audience | Default is the owner URL; set a cluster-specific value with core.getIDToken(audience) | Set per token in the job's id_tokens keyword (aud) |
| Subject format | repo:ORG/REPO:environment:NAME, ...:ref:refs/heads/BRANCH, ...:pull_request | project_path:GROUP/PROJECT:ref_type:branch:ref:main |
| Immutable identifiers | repository_id, repository_owner_id; repositories created after 15 July 2026 default to a subject containing owner and repository IDs (not on GitHub Enterprise Server) | project_id (stable for the project's lifetime), namespace_id |
| Protection signals | environment, ref, job_workflow_ref, runner_environment | ref_protected, environment, environment_protected, runner_environment |
| Token lifetime | Short-lived; not stated on the claims reference page | Job timeout if set, otherwise 5 minutes after issue |
A defensible pattern for a production deploy job from GitHub Actions is a dedicated jwt entry for the GitHub issuer with a cluster-specific audience, and claim rules such as:
- Pin the repository by ID, not name:
claims.repository_id == "123456789", so a renamed or recreated repository cannot inherit trust. - Require a protected environment:
claims.environment == "production", with required reviewers configured on that environment in GitHub. - Pin the workflow: compare
claims.job_workflow_refto the approved reusable deploy workflow at a protected ref, so a new workflow file in the same repository cannot mint a usable token. - Exclude pull requests: reject subjects ending in
:pull_request.
Map the username to something like "gha:" + claims.repository + ":" + claims.environment, the UID to the repository ID, and add an extra attribute carrying the workflow ref for audit. Then bind that username to a namespaced Role with only the verbs the deploy needs. For GitLab, the equivalent rules are claims.project_id, claims.ref_protected == "true" and claims.environment_protected == "true"; check the claim types your instance actually emits before writing the comparison. See the GitHub Actions OIDC Federation Hardening Guide for the same claim choices applied to cloud trust policies, and the Kubernetes RBAC Guide for binding design.
Where identity fits
Every external issuer you add is a new population of non-human identities that can reach the API server. Record each jwt entry as an identity source in your NHI inventory with an owner, the RBAC it can reach and a review date, exactly as you would a cloud workload identity federation trust. The Workload Identity Federation Trust Review Checklist applies almost unchanged, and the Kubernetes NHI Security Guide covers the service account side.
Rollout and rollback
- Inventory current flags: capture every
--oidc-*value,--anonymous-authand--service-account-issuerfrom each control plane node, and list the RBAC bindings that reference the current username and group prefixes. - Translate one to one first: write a file with a single
jwtentry that reproduces the existing flags exactly (same issuer, audience, claim and prefixes), so existing bindings keep matching. AdduserValidationRulesblockingsystem:identities. - Test in a non-production cluster: remove all
--oidc-*flags and add--authentication-config. Confirm the server starts, then have test users runkubectl auth whoamito see the username, groups and extra values actually produced. - Write negative tests: tokens with the wrong audience, an expired
exp, a missing claim, a CI token from a pull request and from an unprotected branch must all return 401 Unauthorized. - Cut over production node by node: on a multi-node control plane, update one API server at a time and watch authentication errors before continuing.
- Add new issuers one at a time: append a CI issuer, deploy the file atomically (write a temporary file, then rename it over the original) and let automatic reload apply it. Check the reload metrics before granting any RBAC.
- Roll back: keep the previous file under version control. For a bad rule, restore the previous file and let reload apply it; to return to flags, remove
--authentication-configand restore the--oidc-*flags with a restart, since the two cannot coexist.
Detection and measurement
KEP-3331 defines metrics for the reload controller and each JWT authenticator. Alert on these and on API server audit events.
| Signal | Source | What it tells you |
|---|---|---|
apiserver_authentication_config_controller_automatic_reload_failures_total | API server metrics | A changed file failed to load; the server keeps the last good config, so a failed change can go unnoticed |
apiserver_authentication_config_controller_automatic_reload_last_config_hash | API server metrics | Compare across control plane nodes; differing hashes mean nodes enforce different rules |
apiserver_authentication_jwt_authenticator_latency_seconds | API server metrics | CEL or JWKS latency per issuer |
apiserver_authentication_jwks_fetch_last_timestamp_seconds and ..._last_keyset_hash | API server metrics | Stale key fetches or unexpected key rotation at an issuer |
| Audit events with external usernames | Kubernetes audit log (user.username, user.extra) | Which CI identity did what; unexpected verbs or namespaces |
| Validation failure messages | API server logs (the rule's message) | Repeated failures from one issuer or subject, a sign of probing or misconfigured pipelines |
Useful KPIs: number of static service account tokens or kubeconfigs stored in CI systems (target zero for clusters with structured authentication); percentage of jwt entries with a userValidationRules backstop (target 100%); percentage of CI issuer entries pinned by immutable repository or project ID; reload failures in the last 30 days; and time from an issuer being retired to its removal from the file. Audit log detection patterns are in the Kubernetes Audit Log Detection Guide.
Decision guidance
- Structured file versus an authentication proxy: if you run your own control plane on v1.30 or later, the file removes the need for a proxy that impersonates users. Keep a proxy only when you need features outside JWT authentication, such as session recording.
- Direct CI trust versus a broker: trusting CI issuers directly is simplest for a few clusters. With dozens of clusters and many teams, a central token broker that exchanges CI tokens for cluster-scoped tokens may be easier to govern, at the cost of a new critical component.
- Claim rules versus RBAC: use CEL to decide who is allowed to authenticate at all (tenant, repository, protected ref), and RBAC to decide what they can do. Do not encode permissions in CEL.
- Small versus large estates: a small team can keep one file in Git per cluster, with peer review and sample-token tests. Large estates should template the file from a central list of approved issuers so a retired issuer is removed everywhere at once.
Common failure patterns
- Trusting the repository, not the workflow: a rule that only checks the repository name lets any workflow, branch or compromised third-party action in that repository mint a usable token. The tj-actions/changed-files compromise showed how a poisoned action runs inside trusted workflows; pin environment and
job_workflow_refso such code cannot reach production. - Shared or generic audience: accepting the GitHub default audience or a value used by other relying parties means a token minted for one service can be replayed against the cluster.
- No prefix separation: two issuers mapped with empty prefixes can produce the same username, and RBAC cannot tell them apart.
- Missing
system:backstop: withoutuserValidationRules, a group claim an IdP administrator controls can be mapped intosystem:masters. - Silent reload failure: an edited file that fails validation leaves the old rules in force while the change ticket says otherwise. Alert on the reload failure counter.
- Leaving the old credentials in place: teams add keyless CI but never delete the stored kubeconfig, so the static secret remains the weakest path.
Framework mapping
| Framework | Reference | How this guide applies |
|---|---|---|
| NIST CSF 2.0 | PR.AA-01, PR.AA-03 | Managing identities for services and authenticating them to the API server |
| NIST SP 800-53 Rev. 5 | IA-5, IA-8, AC-2, CM-6, AU-2 | Short-lived authenticators, external identities, account management, configuration settings and audit |
| CIS Controls v8.1 | Control 5 (Account Management), Control 6 (Access Control Management) | Inventory of CI identities and least-privilege bindings |
| ISO/IEC 27001:2022 | A.5.16, A.5.17, A.8.5, A.8.9 | Identity management, authentication information, secure authentication, configuration management |
| MITRE ATT&CK | T1078.004 Cloud Accounts, T1550.001 Application Access Token | Abuse of valid cloud identities and replay of stolen tokens that strict audience and claim rules limit |
Practitioner checklist
- Confirm the cluster version (v1.30 or later; stable from v1.34) and whether your platform exposes
--authentication-config. - Translate existing
--oidc-*flags one to one before adding anything new, and remove the flags entirely at cut-over. - Give every issuer a distinct username and group prefix and a cluster-specific audience.
- Add
userValidationRulesrejectingsystem:usernames and groups to every configuration. - Pin CI identities by immutable repository or project ID, protected environment or ref, and workflow ref; reject pull request subjects.
- Restrict anonymous access to health endpoints with
anonymous.conditions. - Deploy the file atomically, keep it in version control with negative test tokens, and alert on reload failures and config hash drift between nodes.
- Delete stored kubeconfigs and service account tokens from CI once keyless authentication is proven.
- Review every
jwtentry and its RBAC bindings at least quarterly.
Standards and references
- Kubernetes: Authenticating (structured authentication configuration)
- Kubernetes: KEP-3331 Structured Authentication Config
- GitHub Docs: OpenID Connect reference for GitHub Actions
- GitLab Docs: OpenID Connect (OIDC) authentication using ID tokens
- NIST: Cybersecurity Framework 2.0
- NIST: SP 800-53 Rev. 5
- MITRE ATT&CK: Application Access Token
Related NHI Mgmt Group resources: Kubernetes Security Guide · Kubernetes Service Account Token Guide · Self-Hosted OIDC Issuer Security Guide · Workload Identity Federation Guide