Last modified July 23, 2026
Configure OIDC authentication for workload clusters with structured authentication
Kubernetes structured authentication configuration replaces the legacy --oidc-* API server flags with a declarative, file-based configuration. Giant Swarm workload clusters now expose this mechanism through the cluster app values, which lets you wire one or more identity providers into the API server without rolling your own kubeconfig distribution.
This tutorial walks you through enabling structured authentication on a workload cluster, connecting multiple OIDC providers, mapping claims to Kubernetes identities, and migrating from the legacy configuration.
If you are looking for information on how to authorize authenticated users once they reach the API server, read our cluster access control guide. For platform API authentication (the management cluster), see platform API authentication.
Before you begin
Structured authentication is available from the following workload cluster releases onward:
aws-34.0.0(CAPA)cloud-director-34.0.0(CAPVCD)vsphere-34.0.0(CAPV)azure-34.0.0(CAPZ)
You configure it through the global.controlPlane.oidc.structuredAuthentication field of your cluster app values. Changes trigger a rolling update of the control plane nodes, so test the configuration on a non-production cluster first.
You will also need:
- Cluster-admin access to the workload cluster (to create
ClusterRoleBindingresources for the groups you authenticate). - At least one OIDC provider with a registered client/application for your cluster. The provider must expose a discovery document at
<issuer>/.well-known/openid-configuration.
Why use structured authentication
The legacy --oidc-* flags only allowed a single issuer per API server and offered limited control over how claims become Kubernetes identities. Structured authentication lifts both restrictions:
- Multiple issuers on the same API server, so you can serve internal and external users from different Identity Providers (IdPs) at once.
- Common Expression Language (CEL) expressions for validating tokens and mapping claims, including conditional logic on arbitrary claims.
- Per-issuer CA bundles and discovery URLs, useful for private providers whose issuer URL doesn’t match the discovery endpoint.
- Declarative configuration using a specific file
AuthenticationConfigurationtemplated via cluster app.
Note: Before Kubernetes v1.34, clusters could use Dex (Auth Bundle) to plug more than one IdPs to authenticate your users. The solution will still work with newer versions but it is encouraged to switch to standard method and avoid maintain multiple on-top components. In case of migration feel free to contact your account engineer to assists on the steps.
Enable structured authentication with a single issuer
The minimal configuration enables the feature and declares one issuer. This is the recommended starting point when migrating to or adopting the feature.
global:
controlPlane:
oidc:
structuredAuthentication:
enabled: true
issuers:
- issuerUrl: https://your-idp.example.com
clientId: kubernetes
Apply the values to the cluster app and wait for the control plane rolling update to finish. Use kubectl gs login to get access to the cluster API.
Grant users permission to log in
Once structured authentication is enabled, users authenticate directly against the identity provider. kubectl gs login runs the OAuth flow locally, exchanges the token, and writes a kubeconfig for the workload cluster. To do that it needs four connection parameters: the OIDC issuer, client ID, API server CA certificate, and API server endpoint. There are two ways to supply them, and they require different permissions on the management cluster.
Option 1: No management cluster access (parameters via flags)
If you provide all four values as flags, kubectl gs login never contacts the management cluster. Users then need no management cluster permissions at all (no RBAC, and not even a management cluster context):
kubectl gs login example \
--workload-cluster mywc \
--api-endpoint https://api.mywc.example.com:6443 \
--oidc-issuer https://your-idp.example.com \
--oidc-client-id kubernetes \
--api-ca-file /path/to/ca.crt
This is the most restrictive option and is ideal for giving access to users who shouldn’t have any management cluster permissions. The four values are stable for the lifetime of the cluster, so you can distribute them once (for example in your internal onboarding docs). See the direct OIDC flags for details.
Option 2: Automatic parameter injection (requires management cluster read access)
For convenience, users can omit those flags and let kubectl gs login discover the four parameters from the management cluster, passing only the cluster and organization:
kubectl gs login example \
--workload-cluster mywc \
--organization acme
In this mode kubectl gs login reads a small set of resources from the management cluster, all read-only (get). It assumes the organization’s namespace is org-<name> (derived from --organization), so all reads happen inside that namespace and no cluster-scoped permissions are required:
| Resource | Scope | Used for |
|---|---|---|
clusters.cluster.x-k8s.io | organization namespace | API server endpoint (spec.controlPlaneEndpoint) |
kubeadmcontrolplanes.controlplane.cluster.x-k8s.io | organization namespace | OIDC issuer and client ID from the structured authentication config |
configmaps (<cluster>-cluster-values) | organization namespace | API server CA certificate |
organizations.security.giantswarm.io | cluster-scoped | Fallback only. Read to resolve the organization’s namespace when the workload cluster isn’t found under org-<name>, that is, for organizations whose namespace doesn’t follow the convention. Not read in the normal case. |
Define a minimum role instead of read-all
To use option 2, users must be allowed to read the resources listed earlier on the management cluster. The built-in read-all role covers this, but it grants read access to essentially every resource on the management cluster, far more than logging in requires. We recommend granting a dedicated, minimal role instead, so that logging in to workload clusters doesn’t imply broad read access to the management cluster. Use read-all only if your users already have (and you have an RBAC plan for) general management cluster access.
The following Role grants exactly what kubectl gs login needs inside an organization namespace. Bind it to your users or their OIDC group, and create one Role/RoleBinding per organization namespace they should be able to reach:
# RBAC for `kubectl gs login <mc> --workload-cluster <wc> --organization <org>`
# using structured authentication with automatic parameter injection.
#
# Read-only. All access is `get` on single objects, inside the
# organization namespace (org-<org>). No cluster-scoped access needed.
---
# Everything read inside the organization namespace.
# Create one Role+RoleBinding per org namespace the user should reach
# (e.g. org-acme).
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: kubectl-gs-login-wc-oidc
namespace: org-acme
rules:
- apiGroups: ["cluster.x-k8s.io"]
resources: ["clusters"]
verbs: ["get"]
- apiGroups: ["controlplane.cluster.x-k8s.io"]
resources: ["kubeadmcontrolplanes"]
verbs: ["get"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"]
# Optional: silences a harmless read the client attempts; safe to omit.
- apiGroups: ["application.giantswarm.io"]
resources: ["apps"]
verbs: ["get"]
# EKS/CAPA workload clusters only — omit otherwise:
# - apiGroups: ["controlplane.cluster.x-k8s.io"]
# resources: ["awsmanagedcontrolplanes"]
# verbs: ["get"]
# - apiGroups: [""]
# resources: ["secrets"]
# verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kubectl-gs-login-wc-oidc
namespace: org-acme
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kubectl-gs-login-wc-oidc
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: "<your-oidc-user>" # or kind: Group
Store the binding in Git and reconcile it onto the management cluster the same way as the rest of your configuration.
Fallback: Cluster-scoped Organization read (non-standard namespaces only)
kubectl gs login derives the organization namespace as org-<name> by convention, so the Role shown earlier is all that’s needed in the normal case. It reads the cluster-scoped Organization resource only as a fallback. This happens when the workload cluster isn’t found in the assumed org-<name> namespace, for example when the organization’s namespace doesn’t follow the convention. If that applies to your organization, additionally grant a cluster-scoped read of Organization:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kubectl-gs-login-org-read
rules:
- apiGroups: ["security.giantswarm.io"]
resources: ["organizations"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubectl-gs-login-org-read
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kubectl-gs-login-org-read
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: "<your-oidc-user>" # or kind: Group
Configure multiple OIDC providers
To trust more than one identity provider, add additional entries to the issuers list. Each entry is a self-contained issuer with its own client, claim mappings, and prefixes.
The following example trusts both an internal Keycloak instance for engineers and an external Okta tenant for contractors. Prefixes keep the two namespaces of identities separate so that RBAC bindings are unambiguous.
global:
controlPlane:
oidc:
structuredAuthentication:
enabled: true
issuers:
- issuerUrl: https://keycloak.internal.example.com/realms/platform
clientId: workload-cluster
usernameClaim: email
usernamePrefix: "keycloak:"
groupsClaim: groups
groupsPrefix: "keycloak:"
- issuerUrl: https://example.okta.com
clientId: 0oa1b2c3d4EXAMPLE
usernameClaim: sub
usernamePrefix: "okta:"
groupsClaim: roles
groupsPrefix: "okta:"
With this configuration, a user authenticated via Keycloak with email=jane@example.com and group platform-admins appears to the API server as user keycloak:jane@example.com in group keycloak:platform-admins. You can then bind that group to a role as usual:
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: keycloak-platform-admins
subjects:
- kind: Group
name: keycloak:platform-admins
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
See binding roles for more examples of how to grant permissions to OIDC groups and users.
Note__**: Issuer URLs must be unique across the list. The API server rejects configurations with duplicate issuers.
Claim mapping options
Each issuer entry supports the following basic fields:
| Field | Purpose |
|---|---|
issuerUrl | The URL of the OIDC provider. Must match the iss claim in issued tokens. |
clientId | The audience the API server expects to see in the token. |
usernameClaim | The token claim used as the Kubernetes username. Defaults to sub. |
usernamePrefix | Prefix added in front of the username. Set an explicit value to avoid collisions across issuers. |
groupsClaim | The token claim used as the list of Kubernetes groups. |
groupsPrefix | Prefix added in front of each group. |
Pick email as usernameClaim only if your IdP guarantees the claim is verified and immutable. Otherwise stick with sub, which is always stable.
Advanced configuration
The issuers entries support the full set of options from the upstream AuthenticationConfiguration resource. The most common advanced fields are:
audiencesandaudienceMatchPolicy: accept tokens issued for more than one audience, for example when a single client ID is shared across several components.discoveryUrl: point to a discovery endpoint whose host differs from the issuer URL. Typical when the issuer lives behind a reverse proxy.caPem: inline Privacy Enhanced Mail (PEM) encoded CA bundle for providers that use a private certificate authority.claimValidationRules: CEL expressions that must evaluate totruefor a token to be accepted. Use this to require specific claims, enforce tenant IDs, or reject tokens without multi-factor authentication.userValidationRules: CEL expressions that run after the user object has been constructed. Useful to enforce invariants likeusername must start with okta:.claimMappings: advanced mappings that build usernames, groups, unique identifiers, and extra attributes from CEL expressions instead of plain claim names.
The following example requires an mfa claim with value true and extracts the username from the preferred_username claim:
global:
controlPlane:
oidc:
structuredAuthentication:
enabled: true
issuers:
- issuerUrl: https://your-idp.example.com
clientId: kubernetes
audiences:
- kubernetes
- kubernetes-api
audienceMatchPolicy: MatchAny
claimValidationRules:
- expression: 'claims.mfa == true'
message: "Multi-factor authentication is required to access this cluster."
claimMappings:
username:
expression: '"sso:" + claims.preferred_username'
groups:
expression: 'claims.roles'
For the full schema and semantics of these fields, refer to the Kubernetes structured authentication documentation.
Migrate from legacy OIDC configuration
If your cluster already uses the legacy global.controlPlane.oidc fields such as issuerUrl and clientId, you don’t need to rewrite them. Add structuredAuthentication.enabled: true alongside the existing keys and the cluster app converts the legacy configuration to the new structured format automatically:
global:
controlPlane:
oidc:
issuerUrl: https://your-idp.example.com
clientId: kubernetes
structuredAuthentication:
enabled: true
Once the rolling update completes and you have confirmed that existing users can still authenticate, you can move to the richer issuers list at your own pace, for example to add a second provider or CEL rules. The legacy top-level fields are ignored when issuers is set, so remove them at that point to keep the configuration unambiguous.
Verify the configuration
After the control plane rolling update, sign in with a token from each configured issuer and check that the API server recognizes you with the expected identity:
$ kubectl auth whoami
ATTRIBUTE VALUE
Username keycloak:jane@example.com
Groups [keycloak:platform-admins system:authenticated]
You can impersonate the expected username to validate your RBAC bindings before handing the cluster to users:
$ kubectl auth can-i get pods \
--all-namespaces \
--as "keycloak:jane@example.com" \
--as-group "keycloak:platform-admins"
yes
If authentication fails, check the API server logs for entries from the authentication component. Typical failure modes are a mismatched issuerUrl (it must match the iss claim byte-for-byte), an audience mismatch, or a CEL rule that rejects the token.
Further reading
- Kubernetes structured authentication configuration
- Cluster access control with RBAC
- Platform API authentication
Need help, got feedback?
We listen to your Slack support channel. You can also reach us at support@giantswarm.io. And of course, we welcome your pull requests!