6d224861 came from a --no-checkout worktree whose empty index made the
commit drop every file except two. This restores 05b50d2b's full tree and
correctly adds stacks/stem95su/gdrive-sync.tf + the service-catalog stem95su
entry. Forward-only (parent=6d224861, no force-push); [ci skip] since the
live infra was never applied from the broken commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.8 KiB
| name | description | author | version | date |
|---|---|---|---|---|
| authentik-oidc-kubernetes | Configure Authentik as OIDC provider for Kubernetes API server authentication. Use when: (1) setting up OIDC auth for kubectl with Authentik, (2) kube-apiserver rejects OIDC tokens with "oidc: email not verified", (3) JWKS endpoint returns empty {} despite provider being configured, (4) kubelogin fails with "claim not present" for email, (5) redirect_uri mismatch errors during kubelogin browser auth, (6) kube-apiserver static pod manifest changes don't take effect after restart. Covers all gotchas discovered when integrating Authentik 2025.10.x with Kubernetes 1.34.x using kubelogin (int128/kubelogin). | Claude Code | 1.0.0 | 2026-02-17 |
Authentik OIDC for Kubernetes API Authentication
Problem
Setting up Authentik as an OIDC identity provider for Kubernetes kubectl access involves multiple non-obvious pitfalls that cause silent failures at different stages of the authentication flow.
Context / Trigger Conditions
- Setting up multi-user kubectl access with OIDC
- Using Authentik as the identity provider and kubelogin (int128/kubelogin) as the kubectl plugin
- Any of these errors:
oidc: email not verifiedoidc: parse username claims "email": claim not presentThe request fails due to a missing, invalid, or mismatching redirection URI- JWKS endpoint (
/application/o/<app>/jwks/) returns{} Unauthorizedafter successful browser login
Solution
Gotcha 1: Signing Key Must Be Assigned
Authentik's OAuth2 provider does NOT assign a signing key by default. Without it,
the JWKS endpoint returns {} and kube-apiserver can't validate tokens.
Fix: Assign a signing key (e.g., "authentik Self-signed Certificate") to the OAuth2 provider:
# Via Django shell (kubectl exec into authentik server pod)
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.crypto.models import CertificateKeyPair
provider = OAuth2Provider.objects.get(name='kubernetes')
cert = CertificateKeyPair.objects.filter(name='authentik Self-signed Certificate').first()
provider.signing_key = cert
provider.save()
Or via API:
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"$AUTHENTIK_URL/api/v3/providers/oauth2/<pk>/" \
-d '{"signing_key": "<certificate-keypair-uuid>"}'
Gotcha 2: Default Email Mapping Sets email_verified: False
Authentik's built-in email scope mapping hardcodes email_verified: False:
return {
"email": request.user.email,
"email_verified": False # <-- This causes kube-apiserver to reject the token
}
kube-apiserver requires email_verified: true by default.
Fix: Create a custom scope mapping with email_verified: True and assign it
to the provider instead of the default:
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
# Create custom mapping
mapping, _ = ScopeMapping.objects.get_or_create(
name='Kubernetes Email (verified)',
defaults={
'scope_name': 'email',
'expression': 'return {"email": request.user.email, "email_verified": True}'
}
)
# Replace default email mapping on the provider
provider = OAuth2Provider.objects.get(name='kubernetes')
default_email = ScopeMapping.objects.filter(
managed='goauthentik.io/providers/oauth2/scope-email'
).first()
if default_email:
provider.property_mappings.remove(default_email)
provider.property_mappings.add(mapping)
Gotcha 3: kubelogin Needs Extra Scopes
By default, kubelogin only requests the openid scope. The token will lack
email and groups claims, causing:
oidc: parse username claims "email": claim not present
Fix: Add --oidc-extra-scope flags to the kubeconfig exec plugin:
users:
- name: oidc-user
user:
exec:
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://authentik.example.com/application/o/kubernetes/
- --oidc-client-id=kubernetes
- --oidc-extra-scope=email # Required!
- --oidc-extra-scope=profile
- --oidc-extra-scope=groups
Gotcha 4: Redirect URIs Must Use Regex Mode
kubelogin picks a random available port (tries 8000, 18000, then random).
Strict redirect URI matching like http://localhost:8000/callback will fail
when kubelogin uses a different port.
Fix: Use regex matching in the Authentik provider:
{
"redirect_uris": [
{"matching_mode": "regex", "url": "http://localhost:.*"},
{"matching_mode": "regex", "url": "http://127\\.0\\.0\\.1:.*"}
]
}
Gotcha 5: Property Mappings API Endpoint Changed
In Authentik 2025.10.x, scope mappings are at:
propertymappings/provider/scope/(new, correct)- NOT
propertymappings/scope/(old, returns 405 Method Not Allowed on POST)
Gotcha 6: Static Pod Manifest Changes Need Full Cycle
See skill: kubelet-static-pod-manifest-update for the full restart procedure.
Verification
After all fixes:
# 1. JWKS has a key
curl -s https://authentik.example.com/application/o/kubernetes/jwks/ | jq '.keys | length'
# Expected: 1 (or more)
# 2. Test auth
KUBECONFIG=/path/to/oidc-kubeconfig kubectl get namespaces
# Expected: browser opens, login, namespaces returned
# 3. Check API server logs for success
ssh master "sudo kubectl logs -n kube-system kube-apiserver-* | grep oidc | tail -5"
# Expected: no "Unable to authenticate" errors
Notes
- The OAuth2 provider should use
client_type: public(no client secret needed for kubelogin) - Set
sub_mode: user_emailso the OIDC subject matches the RBAC binding - Set
include_claims_in_id_token: truefor the token to contain claims directly - Use
issuer_mode: per_providerfor a clean issuer URL - RBAC ClusterRoleBindings should match on the user's email (the
--oidc-username-claim=emailvalue)