OAuth / Authentication
SkillSecurityOAuth2-proxy authentication setup, configuration, and troubleshooting for the K3s cluster.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the OAuth / Authentication skill
What this skill tells your AI
The instructions your AI receives, as published by gilesknap/tpi-k3s-ansible in .claude/skills/oauth/SKILL.md and read by ahel’s review.
Architecture
- Provider: GitHub via oauth2-proxy (lightweight, ~128Mi vs ~2GB for Authentik/Keycloak)
- Flow: Cloudflare Access (wildcard
*.gkcluster.org) → oauth2-proxy gateway → backend services - No native OIDC — Cloudflare Access + oauth2-proxy only (branch
external-auth-n-zwas parked)
Configuration
- oauth2-proxy enabled in
kubernetes-services/values.yaml - Reusable ingress sub-chart at
kubernetes-services/additions/ingress/supportsoauth2_proxyannotation mode - Services behind OAuth get ingress annotations pointing to the oauth2-proxy auth endpoint
MCP servers need a different auth model — do NOT try to reuse oauth2-proxy
Non-browser MCP clients (claude.ai's connector, Claude Code
--transport http) cannot authenticate through this cluster's
standard pattern. Two reasons:
- Cloudflare Access challenges with Google SSO — solvable only in a browser.
- oauth2-proxy is cookie-based forward-auth — it's not an OAuth 2.1 authorization server, so MCP clients can't run a token flow against it.
The established pattern for MCP servers in this cluster (open-brain-mcp historically; thoth going forward) is:
- The MCP server embeds an OAuth 2.1 authorization server (RFCs 8414 + 9728 + 7591 DCR + 7636 PKCE) and issues its own JWTs with GitHub as the upstream IdP.
- The MCP hostname (
brain.gkcluster.org,thoth.gkcluster.org) needs a Cloudflare Access bypass policy so the in-pod OAuth flow isn't shadowed by Google SSO. See/cloudflareskill. - The MCP server's 401 response on
/mcpmust includeWWW-Authenticate: Bearer resource_metadata="<host>/.well-known/oauth-protected-resource"— without this header, MCP clients silently fail to discover the auth server and just give up. - Single-replica MCP pods can keep PKCE state and DCR
client_ids in-memory; clients re-register transparently on pod restart. - Reference implementation:
gilesknap/open-brain-mcp/oauth.py(provider-agnostic). When standing up the next MCP service, port the structure rather than reinventing.
Gotchas
- Chrome caching can cause stale redirects/blank pages after auth changes — fix with
chrome://settings/reset - SealedSecrets for oauth2-proxy credentials must match name+namespace exactly (encryption is bound)
- Merging into existing secrets requires
sealedsecrets.bitnami.com/managed: "true"annotation on target - ArgoCD Dex audiences are hardcoded —
server.additional.audiencesdoes nothing for Dex. Override theargo-cdclient index.configwithtrustedPeersinstead. Seeadditions/argocd/README.md. oidc.configdisables Dex — havingoidc.configin argocd-cm causesIsDexDisabled()=true. Usedex.configonly.- Re-sealing secrets requires pod restart — pod env vars from
secretKeyRefare read at startup. After rotating withjust seal-argocd-dex <subcommand>, restart affected pods. - Dex secret needs ArgoCD label — the
argocd-dex-secretSealedSecret template must includeapp.kubernetes.io/part-of: argocdlabel. Without it, ArgoCD's$secret:keyresolution index.configsilently fails, passing literal key names as OAuth client IDs (→ GitHub 404). - Dex static client secrets must all be in argocd-dex-secret —
dex.configuses$argocd-dex-secret:keyfor each static client. Missing keys silently resolve to empty, causing "Failed to get token from provider" on login. The rebuild path (scripts/seal-from-json) seals all clients (grafana, open-webui, argocd-monitor) along with the matching service-side secrets in one pass. - Sidecar oauth2-proxy cookie clash — the shared oauth2-proxy sets
cookie-domain=.gkcluster.org, so its_oauth2_proxycookie reaches all subdomains. Any service with its own oauth2-proxy sidecar (e.g. argocd-monitor) must use--cookie-name=<unique>to avoid validating the shared proxy's cookie with its own secret (→ infinite login loop). - oauth2-proxy
email_domainsmust be[]— the Helm chart defaults toemail_domains = ["*"], which allows any GitHub user through and silently overridesauthenticatedEmailsFile. The fix isconfig.configFilewithemail_domains = []. This bug was found and fixed in PR #279 — do not remove theconfigFileoverride. - oauth2-proxy
cookie-secretsize — must be exactly 16, 24, or 32 bytes for the AES cipher.base64.b64encode(token_bytes(32))produces 44 chars and crashes oauth2-proxy. Usesecrets.token_hex(16)(32 hex chars = 32 bytes). This bug has regressed before — do not change the generation inscripts/seal-argocd-dex. See/sealed-secretsskill. - DEX duplicate
argo-cdstatic client — ArgoCD auto-generates anargo-cdDEX client (withouttrustedPeers). Ourdex.configalso declares one (withtrustedPeers: [argocd-monitor]). DEX v2.45+ stores the first and drops the duplicate, sotrustedPeersnever takes effect. Fixed in PR #297 by addingoidc.configwithallowedAudiences: [argo-cd, argocd-monitor], which lets argocd-monitor authenticate as itself. The duplicateargo-cdclient index.configis harmless but redundant — kept for clarity. - Dex/Grafana need restart after re-sealing — pods that read secrets
via
envFromorsecretKeyRefcache values at startup. After--tags clusterorjust seal-argocd-dex, runjust restart-dexandkubectl rollout restart sts grafana-prometheus -n monitoring. Without this, Dex reports "invalid client_secret" even though the Secret objects match. See/sealed-secretsfor the full namespace list. - Ingress auth-url must be cluster-internal — the ingress sub-chart's
auth-urluses the internal service (oauth2-proxy.oauth2-proxy.svc). Using the external domain resolves via Cloudflare to IPv6, which is unreachable from the cluster, causing intermittent 500s on all oauth2-protected ingresses. - Dex base URL redirects —
/api/dex301s to/api/dex/which returns 404. OIDC clients that don't follow redirects (e.g. Open WebUI's authlib) need the full discovery URL:.well-known/openid-configuration. - Grafana 12.x requires
[users].allow_sign_up— the per-providerallow_sign_upunder[auth.generic_oauth]is not sufficient alone. Also set[auth].disable_signup_form: trueto block manual signup.
Key Files
kubernetes-services/values.yaml— oauth2-proxy toggle and configkubernetes-services/additions/ingress/— reusable ingress with auth modeskubernetes-services/templates/— ArgoCD Application CRDs
Signals
- GitHub stars
- 32
- Forks
- 5
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
oauth- Source
- github.com/gilesknap/tpi-k3s-ansible