Set Up OAuth Authentication#
See also
For a high-level overview of how authentication works across all services, see Authentication Architecture.
This guide walks through configuring both authentication paths used by the cluster. Each path uses its own GitHub OAuth App:
Part A — Dex OIDC, used by ArgoCD, Grafana, Open WebUI, and argocd-monitor (services with native OIDC support).
Part B — oauth2-proxy, used by Supabase Studio and Headlamp (admin-only services with no native OIDC).
Prerequisites#
A working cluster with ingress-nginx and cert-manager
A GitHub account (the OAuth provider)
kubesealinstalled locally (see Manage Sealed Secrets)
Part A: Dex OIDC setup#
Dex runs inside the ArgoCD server pod and acts as a shared OIDC provider for all services that support native OIDC login. It uses a GitHub OAuth App as its upstream identity source.
A1: Create a GitHub OAuth App for Dex#
Click New OAuth App.
Fill in the form:
Field |
Value |
|---|---|
Application name |
|
Homepage URL |
|
Authorization callback URL |
|
Click Register application.
Copy the Client ID.
Click Generate a new client secret and copy it immediately.
A2: Generate Dex client secrets#
Each Dex static client needs its own secret. Generate them:
# One secret per client
for client in argo-cd argocd-monitor grafana open-webui; do
echo "$client: $(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
done
Note
The argo-cd client secret has a special requirement — it must equal
base64url(SHA256(server.secretkey)[:30 bytes]). The just seal-argocd-dex
recipe handles this automatically.
A3: Create and seal the Dex secret#
Use the just seal-argocd-dex recipe, which prompts for the GitHub
credentials and client secrets, then creates the SealedSecret:
just seal-argocd-dex
This creates kubernetes-services/additions/argocd/argocd-dex-secret.yaml.
A4: Seal per-service OAuth secrets#
Grafana and Open WebUI each need their own SealedSecret containing the Dex client secret:
# Grafana
kubectl create secret generic grafana-oauth-secret \
--namespace monitoring \
--from-literal=CLIENT_SECRET="<grafana-client-secret>" \
--dry-run=client -o yaml | \
kubeseal --controller-name sealed-secrets --controller-namespace kube-system -o yaml > \
kubernetes-services/additions/grafana/grafana-oauth-secret.yaml
# Open WebUI
kubectl create secret generic open-webui-oauth-secret \
--namespace open-webui \
--from-literal=client-secret="<open-webui-client-secret>" \
--dry-run=client -o yaml | \
kubeseal --controller-name sealed-secrets --controller-namespace kube-system -o yaml > \
kubernetes-services/additions/open-webui/open-webui-oauth-secret.yaml
A5: Configure admin and viewer emails#
Edit kubernetes-services/values.yaml and set the email lists:
# Full admin access to all services
admin_emails:
- alice@example.com
Also add admin_emails to group_vars/all.yml (required for
Ansible-rendered ArgoCD RBAC):
admin_emails:
- alice@example.com
Important
admin_emails must be kept in sync between values.yaml and
group_vars/all.yml. After changing the latter, re-run
ansible-playbook pb_all.yml --tags cluster.
A6: Deploy#
Commit and push all SealedSecrets. ArgoCD syncs automatically. After sync, restart pods that read secrets from environment variables:
kubectl rollout restart deployment/grafana-prometheus -n monitoring
kubectl rollout restart deployment/open-webui -n open-webui
kubectl rollout restart deployment/headlamp -n headlamp
Adding a new Dex static client#
To add a new service that authenticates via Dex:
Add a
staticClientsentry in thedex.configsection ofroles/cluster/tasks/argocd.yml, with the service’s redirect URI and a reference to its secret key inargocd-dex-secret.Re-seal
argocd-dex-secretwith the new client secret included (just seal-argocd-dex).Configure the service’s OIDC settings to point at
https://argocd.<your-domain>/api/dex.Run
ansible-playbook pb_all.yml --tags clusterto apply the updated Dex config, then commit and push the sealed secret.
Part B: oauth2-proxy setup#
oauth2-proxy is a lightweight reverse proxy that authenticates users directly with GitHub. It protects services that lack native OIDC support.
B1: Create a GitHub OAuth App for oauth2-proxy#
Click New OAuth App.
Fill in the form:
Field |
Value |
|---|---|
Application name |
|
Homepage URL |
|
Authorization callback URL |
|
Click Register application.
Copy the Client ID and generate a Client Secret.
B3: Create and seal the credentials#
printf 'GitHub Client ID: ' && read -r CLIENT_ID
printf 'GitHub Client Secret: ' && read -rs CLIENT_SECRET && echo
printf 'Cookie Secret: ' && read -rs COOKIE_SECRET && echo
kubectl create secret generic oauth2-proxy-credentials \
--namespace oauth2-proxy \
--from-literal=client-id="$CLIENT_ID" \
--from-literal=client-secret="$CLIENT_SECRET" \
--from-literal=cookie-secret="$COOKIE_SECRET" \
--dry-run=client -o yaml | \
kubeseal --controller-name sealed-secrets --controller-namespace kube-system -o yaml > \
kubernetes-services/additions/oauth2-proxy/oauth2-proxy-secret.yaml
unset CLIENT_ID CLIENT_SECRET COOKIE_SECRET
B4: Add a DNS record#
Add a grey-cloud (DNS-only) A record for the oauth2-proxy ingress:
Type |
Name |
Content |
Proxy status |
|---|---|---|---|
A |
|
|
DNS only |
B5: Enable oauth2-proxy#
In kubernetes-services/values.yaml:
enable_oauth2_proxy: true
Commit and push. ArgoCD adds OAuth annotations to all protected ingresses.
How oauth2-proxy is wired to services#
Services use the shared ingress template at
kubernetes-services/additions/ingress/templates/ingress.yaml. To protect
a service, set oauth2_proxy: true in its ingress values:
# In the ArgoCD Application template (e.g. dashboard.yaml)
helm:
valuesObject:
oauth2_proxy: true # ← enables auth annotations
This adds nginx auth-url and auth-signin annotations that check with
oauth2-proxy before forwarding each request.
Services protected by oauth2-proxy (admin-only):
Supabase Studio — requires a dashboard password after OAuth login
Headlamp — uses OAuth as the outer access control layer
Integrating with Cloudflare Access#
For services exposed via the Cloudflare tunnel, add a second authentication layer using Cloudflare Access at zero cluster overhead. Configure an Access Application in the Cloudflare Zero Trust dashboard with an email allowlist covering everyone who should have access. See Set Up a Cloudflare SSH Tunnel for Remote Cluster Access for how Access Applications work.
Troubleshooting#
Redirect loop after enabling OAuth#
The oauth2-proxy ingress must not itself be protected by OAuth. Check
that oauth2-proxy.yaml does not set oauth2_proxy: true on its own
ingress values.
403 after GitHub login#
For oauth2-proxy services (Headlamp, Supabase Studio): the email must be
in admin_emails in values.yaml. Viewer users cannot access these services
by design.
For Dex-authenticated services: any GitHub user can log in. A 403 likely means the service has additional access restrictions.
OAuth not enforced (service loads without login)#
Verify the ingress annotations are present:
kubectl get ingress -n <namespace> <service>-ingress -o yaml | grep auth
You should see auth-url and auth-signin annotations.
Dex login returns 404#
Ensure the OIDC discovery URL includes the full path:
https://argocd.<your-domain>/api/dex/.well-known/openid-configuration.
The base path /api/dex redirects to /api/dex/ which returns 404 —
some OIDC libraries do not follow this redirect.
Dex rejects redirect URI#
Cloudflare Tunnel services may generate http:// callback URIs. Add both
http:// and https:// redirect URIs to the Dex static client
configuration.