Auth0 provides authentication for internal services via oauth2-proxy.
Docs: https://auth0.com/docs
- Go to auth0.com/signup
- Create tenant (e.g.,
myproject-dev) - Note your domain (e.g.,
myproject-dev.us.auth0.com)
Use your own domain instead of *.auth0.com:
- Auth0: Settings → Custom Domains → Add domain (e.g.,
login.example.com) - Auth0 shows CNAME target (e.g.,
xxx.edge.tenants.us.auth0.com) - Cloudflare DNS → Add record:
- Type:
CNAME - Name:
login - Target: value from Auth0
- Proxy status: DNS only (grey cloud, not orange!)
- Type:
- Wait 1-5 min → click Verify in Auth0
Note: If you deleted and recreated the domain, wait up to 4 hours before verification works.
Save your domain as <AUTH0_DOMAIN>:
- With custom domain:
login.example.com - Without:
myproject-dev.us.auth0.com
- Applications → Applications → Create Application
- Name:
oauth2-proxy - Type: Regular Web Application
- Click Create
- Save Client ID as
<AUTH0_CLIENT_ID> - Add to Doppler:
AUTH0_CLIENT_SECRET
In Application Settings → Application URIs:
| Setting | Value |
|---|---|
| Allowed Callback URLs | https://argocd.<TAILNET_NAME>.ts.net/oauth2/callback, https://grafana.<TAILNET_NAME>.ts.net/oauth2/callback, https://longhorn.<TAILNET_NAME>.ts.net/oauth2/callback |
| Allowed Logout URLs | https://argocd.<TAILNET_NAME>.ts.net, https://grafana.<TAILNET_NAME>.ts.net, https://longhorn.<TAILNET_NAME>.ts.net |
| Allowed Web Origins | https://argocd.<TAILNET_NAME>.ts.net, https://grafana.<TAILNET_NAME>.ts.net, https://longhorn.<TAILNET_NAME>.ts.net |
Replace
<TAILNET_NAME>with your tailnet — see Tailscale Setup
When adding new protected services, add their URLs here.
Auth0 doesn't include roles in ID token by default. Create an Action to add them.
- Actions → Library → Build Custom
- Name:
Add Groups to Token - Trigger: Login / Post Login
- Code:
exports.onExecutePostLogin = async (event, api) => {
const namespace = '<AUTH0_GROUPS_CLAIM>'; // e.g., https://myapp
if (event.authorization && event.authorization.roles) {
api.idToken.setCustomClaim(`${namespace}/groups`, event.authorization.roles);
api.accessToken.setCustomClaim(`${namespace}/groups`, event.authorization.roles);
}
};- Click Deploy
Save your namespace as
<AUTH0_GROUPS_CLAIM>(e.g.,https://myapp)
- Actions → Triggers → post-login
- Drag
Add Groups to Tokenfrom right panel into the flow (between Start and Complete) - Click Apply
Why namespaced claim?
Auth0 requires namespace for custom claims. Without a namespace prefix (e.g., https://myapp/groups), the claim will be ignored.
In oauth2-proxy this is configured as:
oidc_groups_claim = "<AUTH0_GROUPS_CLAIM>/groups"
- User Management → Roles → Create Role
- Create roles:
infra-admins— full access to all servicesargocd-admins— ArgoCD accessmonitoring-admins— Grafana accesslonghorn-admins— Longhorn access
- User Management → Users → Select user
- Roles tab → Assign Roles
- Select required roles
How group-based access works
- User logs in via Auth0
- Auth0 Action adds roles to
<namespace>/groupsclaim - oauth2-proxy receives groups from token
- NGINX passes
allowed_groupsto auth-url - oauth2-proxy checks intersection
Example ingress annotation:
nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy/oauth2/auth?allowed_groups=infra-admins,argocd-admins"Placeholders for values.yaml:
<AUTH0_DOMAIN>— e.g.,myproject-dev.us.auth0.com<AUTH0_CLIENT_ID>— from Application Settings<AUTH0_GROUPS_CLAIM>— namespace used in Action (e.g.,https://myapp)
Add to Doppler shared:
AUTH0_CLIENT_SECRET— from Application Settings
"Unable to verify OIDC token"
- Check
auth0.domaindoesn't havehttps://prefix - Check oauth2-proxy can reach Auth0
kubectl logs -n oauth2-proxy -l app.kubernetes.io/name=oauth2-proxyGroups empty in token
- Check Action is deployed
- Check Action is in Login Flow
- Check user has Roles assigned
- Check
groupsClaimin values.yaml matches Action namespace
# Check oauth2-proxy logs for groups
kubectl logs -n oauth2-proxy -l app.kubernetes.io/name=oauth2-proxy | grep groups403 after login
- Groups don't match
allowed_groupsin ingress annotation - Check user's roles in Auth0 Dashboard
- Check spelling (case-sensitive)
Callback URL mismatch
- URLs in Auth0 must exactly match ingress hosts
- Include protocol (
https://) and path (/oauth2/callback) - No trailing slash