Skip to content

Cluster OIDC Login (Authentik)

The service-cluster kube-apiserver trusts Authentik as an OIDC provider, so operators authenticate to kubectl with their Authentik account instead of the static admin kubeconfig. Authentik group membership drives Kubernetes RBAC.

  • Issuer: https://login.homelab.muehlena.de/application/o/kubernetes/ (Let’s Encrypt cert via le-issuer-dns-homelab, so the API server trusts it with no oidc-ca-file).
  • Username claim: email → surfaces to RBAC as oidc:<email>.
  • Groups claim: groups → surfaces as oidc:<group-name>.

Set on the control plane via cluster.apiServer.extraArgs in tf/modules/pve-talos-k8s-cluster/talos.tf:

FlagValue
oidc-issuer-urlhttps://login.homelab.muehlena.de/application/o/kubernetes/
oidc-client-idkubernetes
oidc-username-claimemail
oidc-username-prefixoidc:
oidc-groups-claimgroups
oidc-groups-prefixoidc:

These --oidc-* flags are deprecated in Kubernetes 1.34 but functional. Once the cluster moves to the native Talos AuthenticationConfig machine-config object, migrate to a structured AuthenticationConfiguration.

Apply with a control-plane terraform apply on tf/infrastructure/k8s.services.homelab.muehlena.de. This rewrites the kube-apiserver static-pod manifest and rolls it — expect a brief API blip per control-plane node.

The provider, application, and groups are declared in tf/application-parameters/authentik/ — no manual UI steps:

  • applications.tfmodule "application_kubernetes" creates the OAuth2 provider + application (slug kubernetes, which is what makes the issuer path .../o/kubernetes/). It pins a fixed client_id = "kubernetes" (the oauth2 module gained an optional client_id override for exactly this — the apiserver’s --oidc-client-id must match) and sets client_type = "public", so there is no client secret — kubelogin and Nautik are public clients and prove the exchange with PKCE. Redirect URIs cover kubelogin’s local callback via a regex (http://(localhost|127.0.0.1):(8000|18000)(/.*)?) and the Nautik desktop client (io.nautik.Nautik:/oauth2redirect). Default scopes (openid, email, profile, offline_access) already carry the groups claim through Authentik’s default profile mapping.
  • groups.tfhomelab__k8s (access gate) and homelab__k8s__admin (→ cluster-admin), mirroring the homelab__grafana__admin convention.
  • An authentik_policy_binding gates the application to homelab__k8s members.

Apply with terraform apply in tf/application-parameters/authentik/. Because this is a public client, operators need no secret — only the client_id (kubernetes) and the issuer URL, both public.

Add operators to homelab__k8s__admin (which implies homelab__k8s) to grant cluster-admin.

k8s/manifests/base/cluster-services/cluster-oidc-rbac/cluster-admin-crb.yaml binds the Authentik group to cluster-admin:

kind: ClusterRoleBinding
metadata:
name: oidc-homelab-k8s-admin
subjects:
- kind: Group
name: oidc:homelab__k8s__admin # oidc-groups-prefix + Authentik group name
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

Wired through the tenant overlay + Flux Kustomization cluster-services-cluster-oidc-rbac. To grant narrower access, add a group (e.g. homelab__k8s__viewer) and bind oidc:homelab__k8s__viewer to a lesser ClusterRole.

Install the plugin (kubectl oidc-login):

Terminal window
brew install int128/kubelogin/kubelogin # or: krew install oidc-login

Add a user + context to your kubeconfig (API endpoint is the VIP on k8s.services.homelab.muehlena.de:6443). This is a public client, so there is no secret — --oidc-use-pkce proves the exchange:

Terminal window
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://login.homelab.muehlena.de/application/o/kubernetes/ \
--exec-arg=--oidc-client-id=kubernetes \
--exec-arg=--oidc-extra-scope=email \
--exec-arg=--oidc-extra-scope=profile \
--exec-arg=--oidc-extra-scope=offline_access
kubectl config set-context k8s-oidc \
--cluster=<your-cluster-entry> --user=oidc
kubectl config use-context k8s-oidc

First kubectl call opens a browser to Authentik; on success the token is cached under ~/.kube/cache/oidc-login/ and refreshed via offline_access.

Verify the identity the API server sees:

Terminal window
kubectl auth whoami
# Username oidc:you@muehlena.de
# Groups oidc:homelab__k8s__admin, system:authenticated
  • Unauthorized: the id_token aud must equal kubernetes; confirm the Authentik Client ID and --oidc-client-id match.
  • Authenticated but Forbidden: identity works, RBAC doesn’t — check kubectl auth whoami groups against the ClusterRoleBinding subject (remember the oidc: prefix).
  • oidc: could not verify token: issuer did not match: the --oidc-issuer-url (trailing slash included) must exactly equal the issuer field in https://login.homelab.muehlena.de/application/o/kubernetes/.well-known/openid-configuration.
  • No groups: ensure the user is in homelab__k8s__admin and the provider includes the default profile scope mapping.