Skip to content

EMK - OIDC Authentication

Estimated time to read: 10 minutes

This page describes how to configure your Kubernetes cluster with OIDC authentication. This is only available through the YAML configurator and requires EMK access through a service account (SA).


OIDC authentication allows you to integrate your Kubernetes cluster with external identity providers such as Azure AD, Google, Keycloak, Zitadel and Dex. The kube-apiserver validates JWT tokens issued by your identity provider to authenticate users. More information about Kubernetes OIDC authentication can be found on the Kubernetes website, topic Authenticating.

Kubernetes Version Support

  • Kubernetes >= 1.30: Use Structured Authentication configuration

Structured Authentication

For clusters with Kubernetes version >= 1.30, kube-apiserver can be provided with Structured Authentication configuration. This method offers more flexibility and supports multiple JWT authenticators.

Example Structured Authentication Configuration

apiVersion: v1
kind: ConfigMap
metadata:
  name: oidc-authentication-config
data:
  config.yaml: |
    apiVersion: apiserver.config.k8s.io/v1beta1
    kind: AuthenticationConfiguration
    jwt:
    - issuer:
        url: https://identity.example.com
        audiences:
        - 7192d6e6-2b9f-454e-96c3-6863aa0f4050
        - 234ff210-2b29-4324-88e2-2fe5974a4c8e
        audienceMatchPolicy: MatchAny
      claimMappings:
        username:
          claims: email
          prefix: 'oidc:'
        groups:
          claims: groups
          prefix: 'oidc:'
apiVersion: v1
kind: ConfigMap
metadata:
  name: multi-oidc-authentication-config
data:
  config.yaml: |
    apiVersion: apiserver.config.k8s.io/v1beta1
    kind: AuthenticationConfiguration
    jwt:
    - issuer:
        url: https://internal.example.com
        audiences:
        - 7192d6e6-2b9f-454e-96c3-6863aa0f4050
        audienceMatchPolicy: MatchAny
      claimMappings:
        username:
          claims: email
          prefix: 'internal:'
        groups:
          claims: groups
          prefix: 'internal:'
    - issuer:
        url: https://external.example.com
        audiences:
        - 234ff210-2b29-4324-88e2-2fe5974a4c8e
        audienceMatchPolicy: MatchAny
      claimMappings:
        username:
          claims: email
          prefix: 'external:'
        groups:
          claims: groups
          prefix: 'external:'
apiVersion: v1
kind: ConfigMap
metadata:
  name: advanced-oidc-authentication-config
data:
  config.yaml: |
    apiVersion: apiserver.config.k8s.io/v1beta1
    kind: AuthenticationConfiguration
    jwt:
    - issuer:
        url: https://identity.example.com
        audiences:
        - 7192d6e6-2b9f-454e-96c3-6863aa0f4050
        - 234ff210-2b29-4324-88e2-2fe5974a4c8e
        audienceMatchPolicy: MatchAny
      claimMappings:
        username:
          claims: email
          prefix: 'oidc:'
        groups:
          claims: groups
          prefix: 'oidc:'
        extra:
        - key: 'department'
          expression: 'claims.department'
        - key: 'employee_id'
          expression: 'claims.employee_id'
      claimValidationRules:
      - expression: 'claims.email_verified == true'
        message: "email must be verified"
      - expression: 'has(claims.groups) && size(claims.groups) > 0'
        message: "user must belong to at least one group"
      - expression: 'claims.exp - claims.iat <= 3600'
        message: "token validity cannot exceed 1 hour"
      userValidationRules:
      - expression: "!user.username.startsWith('system:')"
        message: "username cannot use system: prefix"
      - expression: "size(user.username) >= 3"
        message: "username must be at least 3 characters"
Structured Authentication Parameters

Issuer Configuration

Parameter
Required Description
url Yes URL of the OpenID Connect provider. Must use HTTPS scheme.
audiences Yes List of acceptable audiences for the JWT token.
audienceMatchPolicy No How to match audiences. Options: MatchAny (default), MatchAll.
certificateAuthority No PEM-encoded certificate bundle for verifying the provider's certificate.
discoveryURL No Override URL for OIDC discovery (default: issuer URL + /.well-known/openid-configuration).

Claim Mappings

Parameter
Required Description
username.expression No CEL expression to extract username from claims. Default: claims.sub.
username.prefix No Prefix for username.
groups.expression No CEL expression to extract groups from claims.
groups.prefix No Prefix for groups.
uid.expression No CEL expression to extract UID from claims.
extra No Additional attributes to extract from claims.

Rolling Out Configuration Changes

EMK will not automatically roll out changes to the authentication ConfigMap to minimize the number of Shoot reconciliations. EMK will pick up changes on the next reconciliation of Shoots referencing the configuration.

If you want to immediately roll out configuration changes, you can manually trigger a Shoot reconciliation as described in triggering an immediate reconciliation.

Configuration Validation

The user is responsible for the validity of the configured JWT authenticators. Invalid configurations may prevent users from accessing the cluster. Always test configurations in a non-production environment first.

User Permissions with RBAC

After configuring OIDC authentication, users can authenticate to your cluster, but they won't have any permissions yet. You need to configure Role-Based Access Control (RBAC) to grant permissions to users and groups extracted from the OIDC token.

Authentication vs Authorization

  • Authentication (OIDC configuration): Verifies who the user is
  • Authorization (RBAC): Determines what the user can do

Understanding the Flow

  1. User authenticates with their identity provider (Azure AD, Google, etc.)
  2. Identity provider issues a JWT token containing user information and groups
  3. Kubernetes extracts username and groups from the token based on your OIDC configuration
  4. RBAC checks if the username or any of the user's groups have permissions via RoleBindings/ClusterRoleBindings

Example RoleBindings

The most common approach is to grant permissions to groups rather than individual users. This makes permission management easier and more scalable.

See the following examples:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: oidc-cluster-admins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: Group
    name: 'oidc:operations'  # match claimMappings.groups.prefix + group name
    apiGroup: rbac.authorization.k8s.io
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: oidc-developers
  namespace: development
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: edit
subjects:
  - kind: Group
    name: oidc:developers  # match claimMappings.groups.prefix + group name
    apiGroup: rbac.authorization.k8s.io

You can also grant permissions directly to individual users:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: oidc-user-viewer
  namespace: production
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
subjects:
  - kind: User
    name: oidc:john@example.com  # match claimMappings.username.prefix + email
    apiGroup: rbac.authorization.k8s.io
Common Built-in ClusterRoles

Kubernetes provides several built-in ClusterRoles that you can use:

ClusterRole Permissions
cluster-admin Full access to all resources in the cluster
admin Full access within a namespace, can manage roles and rolebindings
edit Read/write access to most resources in a namespace
view Read-only access to most resources in a namespace

Matching Group Names

Make sure the group names in your RoleBindings match what comes from your OIDC provider.

See the following example:

If your Zitadel project is called mycorp and the role is called operations, your AuthenticationConfiguration can have:

claimMappings:
  groups:
    claim: "roles:mycorp"
    prefix: "oidc:"

And your ClusterRoleBinding subjects should contain:

- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: oidc:operations

Group names and other claims from your OIDC provider can be viewd in the OIDC token after a succesful authentication.

Complete Setup Example

In the following example an EMK cluster is configured to use a Zitadel instance for OIDC authentication. Allowing user John, a member of the Operations team at MyCorp, to access and manage the EMK cluster with cluster admin privileges.

Prerequisites:

Step summary:

  1. Create AuthenticationConfiguration
  2. Update Shoot configuration to use AuthenticationConfiguration
  3. Create RoleBindings to set permissions
  4. Create kubeconfig with OIDC authentication
  5. Access cluster using OIDC

Create AuthenticationConfiguration

The following kubectl commands must be executed using your EMK Service Account.

  1. Create file oidc-authentication-config.yaml containing a ConfigMap with the AuthenticationConfiguration:

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: oidc-authentication-config
      namespace: garden-myproject
    data:
      config.yaml: |
        apiVersion: apiserver.config.k8s.io/v1beta1
        kind: AuthenticationConfiguration
        jwt:
        - issuer:
            url: https://identity.example.com
            audiences:
            - "7192d6e6-2b9f-454e-96c3-6863aa0f4050"
            audienceMatchPolicy: MatchAny
          claimMappings:
            username:
              claim: "preferred_username"
              prefix: "oidc:"
            groups:
              claim: "roles:mycorp"
              prefix: "oidc:"
    

    Configuration Validation

    The user is responsible for the validity of the configured JWT authenticators. Invalid configurations may prevent users from accessing the cluster. Always test configurations in a non-production environment first.

  2. Add the ConfigMap to your EMK project namespace:

    kubectl apply -f oidc-authentication-config.yaml
    
  3. Verify the ConfigMap was created:

    kubectl get configmap oidc-authentication-config
    

    Expected output:

    NAME                          DATA   AGE
    oidc-authentication-config    1      15s
    

  4. Update your preferred cluster, mycluster in our example, to use the structured authentication:

    kubectl patch shoot mycluster \
      --type 'merge' \
      --patch '{
                 "spec": {
                   "kubernetes": {
                     "kubeAPIServer": {
                       "structuredAuthentication": {
                         "configMapName": "oidc-authentication-config"
                       }
                     }
                   }
                 }
               }'
    

    Expected additional configuration to your shoot config:

    spec:
      kubernetes:
        kubeAPIServer:
          structuredAuthentication:
            configMapName: oidc-authentication-config
    

    Note

    EMK validates the Shoot resource to refer only to existing ConfigMaps containing a valid AuthenticationConfiguration, and rejects the change on failure.

  5. Reconcile your cluster for changes to take affect:

    kubectl annotate shoot mycluster gardener.cloud/operation=reconcile
    

    Note

    EMK will not automatically roll out changes to the authentication ConfigMap to minimize the number of Shoot reconciliations. EMK will pick up changes on the next reconciliation of Shoots referencing the configuration.

Create RoleBindings

The following kubectl commands must be applied in your cluster.

  1. Create file clusterrolebindings.yaml containing permissions:

    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: oidc-cluster-admins
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: cluster-admin
    subjects:
    - apiGroup: rbac.authorization.k8s.io
      kind: Group
      name: oidc:operations # match claimMappings.groups.prefix + group name
    EOF
    
  2. Apply the create file to create the ClusterRoleBinding in your cluster:

    kubectl apply -f clusterrolebindings.yaml
    

Verifying RBAC Configuration

After creating RoleBindings, verify that users have the expected permissions:

# Check what a specific user can do
kubectl auth can-i --list --as oidc:john@example.com

# Check if a user can perform a specific action
kubectl auth can-i get nodes --as oidc:john@example.com

# Check permissions for a group in a namespace
kubectl auth can-i get pods --as-group oidc:operations -n development

OIDC Authentication with kubectl

To setup OIDC authentication with kubectl, we'll modify a current kubeconfig as it already contains the server address and required certificate information.

  1. Download a time limited kubeconfig and save it to ~/.kube/config-oidc.yaml, see time limited access

  2. List and remove the current user from the kubeconfig:

    % kubectl config get-users \
      --kubeconfig ~/.kube/config-oidc.yaml 
    NAME
    garden-mycorp--mycluster-external
    
    % kubectl config delete-user garden-mycorp--mycluster-external \
      --kubeconfig ~/.kube/config-oidc.yaml
    
  3. Add the OIDC credential, change the issuer URL and client ID accordingly:

    kubectl config set-credentials oidc \
      --kubeconfig ~/.kube/config-oidc.yaml \
      --exec-api-version=client.authentication.k8s.io/v1 \
      --exec-interactive-mode=Never \
      --exec-command=kubectl \
      --exec-arg=oidc-login \
      --exec-arg=get-token \
      --exec-arg="--oidc-issuer-url=https://identity.example.com" \
      --exec-arg="--oidc-client-id=7192d6e6-2b9f-454e-96c3-6863aa0f4050" \
      --exec-arg="--oidc-extra-scope=groups" \
      --exec-arg="--oidc-extra-scope=email" \
      --exec-arg="--oidc-extra-scope=name" \
      --exec-arg="--oidc-extra-scope=sub" \
      --exec-arg="--oidc-extra-scope=email_verified"
    
  4. Update the current context with the new credential:

    kubectl config set-context \
      --kubeconfig ~/.kube/config-oidc.yaml \
      --user=oidc \
      --current
    
  5. Now list a common resource, e.g. nodes:

    kubectl get nodes \
      --kubeconfig ~/.kube/config-oidc.yaml
    

    Note

    kubectl will open your default web browser to initiate the OIDC login process. On a succesful authentication, kubectl will list the nodes

Using kubectl OIDC Plugins

For a better user experience, use kubectl plugins for automatic token refresh and interactive login flows.

See How to setup kubelogin for setup instructions.

Troubleshooting

OIDC Authentication

Clean the token cache:

kubectl oidc-login clean

View the ID token claims, crucial when you experince claim mismatches, change the issuer URL and client ID accordingly:

kubectl oidc-login setup \
  --oidc-issuer-url=https://identity.example.com \
  --oidc-client-id=7192d6e6-2b9f-454e-96c3-6863aa0f4050

Tokens are cached at ~/.kube/cache/oidc-login/ and can be inspected after decoding:

jq -r '.id_token | split(".") | .[0],.[1] | @base64d | fromjson' \
  ~/.kube/cache/oidc-login/942d484c0518e5fee6cb9ca192421790a9f8c59215e89cd87a37a8d44fc0b956

RBAC

User authenticated but has no permissions:

  • Verify the group names in your RoleBindings match exactly with what's in the JWT token (including prefix)
  • Check if the user is actually a member of the groups in your identity provider
  • Decode the JWT token at jwt.io to see what claims are present
  • Use kubectl auth can-i --list --as <username> to see what permissions the user has

Groups not appearing correctly:

  • Ensure groupsClaim points to the correct claim in your JWT token
  • Verify the groupsPrefix is included in your RoleBinding subject names
  • Check that your identity provider is configured to include group information in tokens

Common Issues

Authentication fails with "Unauthorized"

  • Verify the issuer url is correct and accessible from the cluster
  • Check that the audiences list matches your identity provider configuration
  • Ensure the JWT token contains the required claims specified in your claimMappings
  • Validate your claimValidationRules are not blocking valid tokens

Username or groups not mapped correctly

  • Verify the CEL expressions in claimMappings are correct
  • Check the claims exist in your JWT token (decode it at jwt.io)
  • Test your CEL expressions using the CEL Playground
  • Ensure the prefix values match what you use in your RoleBindings

Certificate verification errors

  • Provide the CA certificate in certificateAuthority if your issuer uses a custom CA
  • Ensure the certificate is in PEM format
  • Verify the certificate chain is complete

Claim validation failures

  • Review the claimValidationRules expressions for syntax errors
  • Test CEL expressions using the CEL Playground
  • Check that the message field provides clear guidance for users
  • Verify the claims referenced in validation rules exist in your tokens

Security Best Practices

Security Recommendations

  • Always use HTTPS for issuer.url
  • Implement short token expiration times (< 1 hour recommended)
  • Use claim validation rules to restrict access
  • Regularly rotate client credentials
  • Monitor authentication logs for suspicious activity
  • Use RBAC to limit permissions after authentication
  • For Structured Authentication, validate user input with userValidationRules

Additional Resources