Configuring OIDC Authentication for Browserless Clients

Follow these steps to configure OpenID Connect (OIDC) authentication for a shoot cluster. This guide covers creating the necessary API Server authentication configuration, installing the OIDC login plugin, and granting cluster access to OIDC users / clients.

Prerequisites

Before proceeding, ensure you have the following access and credentials:

  1. Project Kubeconfig: Download this from the PSKE dashboard under Members > Service Accounts.
  2. Shoot Kubeconfig: Download this from the shoot overview page in the PSKE dashboard.
  3. OIDC Credentials: Retrieve the issuer URL, client ID, and client secret from your OIDC identity provider.

1. Create the Authentication ConfigMap

Create a file named kube-apiserver-auth-config.yaml and paste the following content. Ensure you replace the placeholder issuer URL and client ID values. For more information, refer to the OIDC configuration documentation: OIDC / Structured Authentication.

# kube-apiserver-auth-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-apiserver-auth-config
data:
  config.yaml: |-
    apiVersion: apiserver.config.k8s.io/v1beta1
    kind: AuthenticationConfiguration
    jwt:
      - issuer:
          url: https://example.com
          audiences:
            - example-client-id
        claimMappings:
          username:
            claim: sub
            prefix: https://example.com#

Deploy the ConfigMap to your project using the project kubeconfig:

export KUBECONFIG=kubeconfig-project.yaml
kubectl apply --filename=kube-apiserver-auth-config.yaml

Note: If you update this ConfigMap later, remember to manually reconcile the Shoot (see Step 2 note).

2. Attach the ConfigMap to the Shoot

Edit the Shoot resource either in the PSKE dashboard (switch the cluster overview from Overview to YAML) or using kubectl edit (using the project kubeconfig). Add the following configuration under the spec.kubernetes.kubeAPIServer.structuredAuthentication section:

apiVersion: core.gardener.cloud/v1beta1
kind: Shoot
spec:
  kubernetes:
    kubeAPIServer:
      structuredAuthentication:
        configMapName: kube-apiserver-auth-config

Note: Gardener automatically rolls out the AuthenticationConfiguration when the structuredAuthentication in the Shoot spec changes. If you update the content of the ConfigMap without changing its name, the Shoot will not auto-reconcile. To apply the new configuration, manually trigger a reconciliation.

3. Install the OIDC Login Plugin

Install the kubectl oidc-login plugin. Follow the official installation guide here:

https://github.com/int128/kubelogin#setup

4. Obtain an OIDC Token with Client Credentials Grant

Verify that the OIDC issuer URL and credentials are correct by running the following command:

kubectl oidc-login setup --oidc-issuer-url=https://example.com --oidc-client-id=example-client-id --oidc-client-secret=example-client-secret --grant-type=client-credentials

If the command executes successfully, it prints the token claims. The output will resemble the following:

Authentication in progress...
## Authenticated with the OpenID Connect Provider

You got the token with the following claims:

```
{
  "iss": "https://example.com",
  "sub": "example-sub",
  "aud": "example-client-id",
  "exp": 1767312000,
  "iat": 1767225600,
  "jti": "3e9a0664-fa50-46de-85a2-d2b0c060b19c",
  "at_hash": "uav9UWBowQlxgbIzgsvCpE",
}
```

Take note of the value of the sub claim. You will need this value in the next step.

5. Grant Access to the Shoot

Use the sub claim value obtained in the previous step to create a ClusterRoleBinding that maps the OIDC subject to the view role in the shoot cluster:

# oidc-user.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: oidc-cluster-viewer
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: https://example.com#example-sub

Apply the binding using your shoot kubeconfig:

export KUBECONFIG=kubeconfig-shoot.yaml
kubectl apply --filename=oidc-user.yaml

6. Add the OIDC User to the Shoot Kubeconfig

Generate an OIDC user entry in your kubeconfig using the following command. This configures kubectl to retrieve tokens automatically for this user:

export KUBECONFIG=kubeconfig-shoot.yaml
kubectl config set-credentials oidc \
  --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://example.com" \
  --exec-arg="--oidc-client-id=example-client-id" \
  --exec-arg="--oidc-client-secret=example-client-secret" \
  --exec-arg="--grant-type=client-credentials"

7. Verify Access

With the shoot kubeconfig updated, you can now execute kubectl commands against the shoot using the new OIDC credentials. For example:

kubectl --kubeconfig=kubeconfig-shoot.yaml --user=oidc get nodes

Setup Dex as OIDC Identity Provider for Browserless Clients

This guide is designed for development and testing environments:

  • nip.io: The dynamic DNS service nip.io is used for auto-generated domains. Do not use it for production.
  • Secrets: Never hardcode client secrets or passwords in configuration files.

Prerequisites

  1. Tools: helm, kubectl, kubelogin, openssl and a shell (e.g. bash)
  2. Shoot Kubeconfig: Download this from the shoot overview page in the PSKE dashboard.

1. Install Traefik

helm repo add traefik https://traefik.github.io/charts
helm repo update traefik
helm upgrade traefik traefik/traefik --create-namespace --install --namespace=traefik

2. Install Cert Manager

helm repo add jetstack https://charts.jetstack.io
helm repo update jetstack
helm upgrade cert-manager jetstack/cert-manager --create-namespace --install --namespace=cert-manager \
    --set=crds.enabled=true

Create a cluster-issuers.yaml file with the following content:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-http01
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-http01
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-http01-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-http01-staging
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik

Apply it:

kubectl --namespace=cert-manager apply --filename=cluster-issuers.yaml

3. Install Dex

Create a dex-values.yaml file:

config:
  enablePasswordDB: true
  issuer: https://localhost # placeholder, overridden by helm --set
  oauth2:
    passwordConnector: local
  staticClients:
    - id: example-client-id
      secret: example-client-secret
      name: example-client-name
  staticPasswords:
    - username: admin
      email: admin@example.com
      # password: admin (echo admin | htpasswd -Bin admin | cut -d: -f2)
      hash: $2y$05$T0abwx/OUS0EZNkDZgznd.rTsUJZrH1QSlRDHirCGQEMYko7LJbgK
  storage:
    type: memory
env:
  DEX_CLIENT_CREDENTIAL_GRANT_ENABLED_BY_DEFAULT: true
image:
  # even the latest release v2.45.1 does not support the grant type "client credentials"
  tag: latest@sha256:bb4835dd1e71986cae4f6b8565e7d79c48f77460098b9e40ad5c117b4ad11465
ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-http01-staging # use staging initially
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
  hosts:
    - host: localhost # placeholder, overridden by helm --set
      paths:
        - path: /
          pathType: Prefix
  tls:
    - hosts:
        - localhost # placeholder, overridden by helm --set
      secretName: dex-tls

Retrieve the load balancer’s external IP address and generate a dynamic nip.io domain:

IP=$(kubectl --namespace=traefik get service/traefik --output=jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "IP=${IP:?}"

# replace dots with dashes (e.g. 1.2.3.4 -> 1-2-3-4) for the subdomain part
HOST=${IP//./-}.nip.io
echo "HOST=${HOST:?}"

Install Dex:

helm repo add dex https://charts.dexidp.io
helm repo update dex
helm upgrade dex dex/dex --create-namespace --install --namespace=dex \
    --values=dex-values.yaml \
    --set="config.issuer=https://${HOST:?}" \
    --set="ingress.hosts[0].host=${HOST:?}" \
    --set="ingress.tls[0].hosts[0]=${HOST:?}"

4. Verify Certificate Issuance

Dex will initially use the staging certificate to avoid rate limits. Verify it is issued by the staging provider:

openssl s_client -showcerts -connect "${HOST:?}:443" </dev/null 2>/dev/null |
    openssl x509 -noout -issuer -nameopt lname | tee -a /dev/stderr |
    grep -F "organizationName=(STAGING) Let's Encrypt,"

Once verified, update Dex’s ingress to use the production Let’s Encrypt issuer:

helm upgrade dex dex/dex --create-namespace --install --namespace=dex --reuse-values \
    --set="ingress.annotations.\"cert-manager.io/cluster-issuer\"=letsencrypt-http01"

Verify the certificate:

openssl s_client -showcerts -connect "${HOST:?}:443" </dev/null 2>/dev/null |
    openssl x509 -noout -issuer -nameopt lname | tee -a /dev/stderr |
    grep -F "organizationName=Let's Encrypt,"

5. Test OIDC Authentication

kubectl oidc-login setup \
    --oidc-issuer-url="https://${HOST:?}" \
    --oidc-client-id=example-client-id \
    --oidc-client-secret=example-client-secret \
    --grant-type=client-credentials

Alternatively, the tutorial PSKE – How to Setup OIDC/2FA on PSKE is also available.

Last modified 18.06.2026: Update _index.md (1d080761)