# OAuth App Setup & Management Guide This guide provides a standardized workflow for registering new services with the homelab OIDC provider (Authentik) and wiring them for Vault JWT authentication. > **TL;DR:** Run `register_oauth_app.py` with your app's details; it handles Authentik + Vault + K8s secrets automatically. --- ## Architecture Overview The homelab uses a three-tier authentication stack: ``` User / Service ↓ Authentik (OIDC IdP) ├─ Grafana ├─ MinIO ├─ Forgejo ├─ Argo CD ├─ Your New Service └─ ... ↓ (device code / authorization code flow) Vault (KV + JWT auth) ├─ JWT role mapping (sub/groups → policies) ├─ OIDC browser login (vault-browser provider) └─ KV secrets (secret/cluster/*, secret/mcp/*) ``` **Key concepts:** - **Authentik** is the OIDC identity provider — it issues tokens and manages users/groups. - **Vault** validates Authentik's JWT tokens and maps them to policies & secret access. - **Kubernetes secrets** store OAuth credentials that services read at startup (ConfigMap-based). - **Groups** in Authentik control access policies and Vault role assignment. --- ## Prerequisites Before registering a new OAuth service, ensure: 1. **Authentik is running** (in `iam` namespace) ```bash kubectl get pods -n iam | grep authentik-server ``` 2. **Vault is running** (in `storage` namespace) ```bash kubectl get pods -n storage | grep vault-0 ``` 3. **SSH/API access to Authentik** — port-forward available ```bash kubectl port-forward -n iam svc/authentik-server 7000:80 ``` 4. **Vault bootstrap is complete** (run `setup_vault.sh` if not already done) 5. **Required environment variables** — populate these in `~/.authentik/.env`: ```bash # Token for Authentik API (from bootstrap) AUTHENTIK_BOOTSTRAP_TOKEN= # Service-specific client secret (generate via openssl rand -hex 32) # Example: MY_SERVICE_OIDC_CLIENT_SECRET= ``` --- ## One-Line Setup (Recommended) For most services, use the automated registration script: ```bash cd /Users/rockliang/workplace/homelab # Register an OAuth app with automatic Vault JWT wiring python3 k8s/talos-iam/register_oauth_app.py \ --service-name my-app \ --namespace my-namespace \ --redirect-uri "https://my-app.riotpiao.homelab.com/oauth2/callback" \ --service-name-in-vault "MY_SERVICE" \ --vault-jwt-policy "shell-secrets" ``` The script will: 1. ✅ Create an OAuth2 provider in Authentik (from credentials in Vault) 2. ✅ Create an Application in Authentik 3. ✅ Create/bind groups for RBAC (optional) 4. ✅ Create a Kubernetes secret with the client credentials 5. ✅ Create a Vault JWT role for the service's JWT auth method 6. ✅ Write policies for the service's scope in Vault --- ## Manual Setup (Step-by-Step) If you prefer manual control, or if the script doesn't fit your use case: ### Step 1: Generate Credentials ```bash # Generate a new client secret CLIENT_SECRET=$(openssl rand -hex 32) echo "CLIENT_SECRET=$CLIENT_SECRET" # Store in Vault (required for service initialization) # Convention: YOURSERVICE_OIDC_CLIENT_SECRET talos put cluster/YOURSERVICE_OIDC_CLIENT_SECRET YOURSERVICE_OIDC_CLIENT_SECRET="$CLIENT_SECRET" # Or set in .env temporarily export YOURSERVICE_OIDC_CLIENT_SECRET="$CLIENT_SECRET" ``` ### Step 2: Create Authentik OAuth2 Provider Access Authentik UI via port-forward: ```bash kubectl port-forward -n iam svc/authentik-server 7000:80 & # http://localhost:7000/if/admin/ → login with akadmin ``` **In Authentik UI:** 1. Navigate to **Applications → Providers → Create → OpenID Connect (OAuth2) Provider** 2. Fill in: - **Name:** `yourservice` - **Client ID:** `yourservice` (or custom) - **Client Secret:** (paste from `$CLIENT_SECRET` above) - **Redirect URIs:** `https://your-app.riotpiao.homelab.com/oauth2/callback` (or your app's callback URL) - **Sub Mode:** `Hashed User ID` - **Include claims in ID Token:** ✓ Enabled - **Grant Types:** `Authorization Code`, `Refresh Token` - **Signing Key:** (select the homelab-oidc key) 3. **Save** and note the **provider slug** (usually auto-generated from Name). ### Step 3: Create Authentik Application **In Authentik UI:** 1. Navigate to **Applications → Applications → Create** 2. Fill in: - **Name:** `Your App Display Name` - **Slug:** `yourservice` (must match provider slug) - **Provider:** (select the provider created above) - **Meta Launch URL:** `https://your-app.riotpiao.homelab.com` (optional, for app launcher) 3. **Save** ### Step 4: Configure OIDC in Your Service Pass the following environment variables to your service: ```bash # OIDC endpoint (in-cluster: authentik-server.iam.svc.cluster.local) OIDC_ISSUER_URL=http://authentik-server.iam.svc.cluster.local/application/o/yourservice/ # OAuth2 credentials (from step 1-2) OIDC_CLIENT_ID=yourservice OIDC_CLIENT_SECRET=$CLIENT_SECRET # Redirect URI (must match what you configured in step 2) OIDC_REDIRECT_URI=https://your-app.riotpiao.homelab.com/oauth2/callback # Optionally, token validation endpoint OIDC_TOKEN_URL=http://authentik-server.iam.svc.cluster.local/application/o/yourservice/token/ OIDC_USERINFO_URL=http://authentik-server.iam.svc.cluster.local/application/o/yourservice/userinfo/ # Optional: JWKS endpoint for offline token validation OIDC_JWKS_URL=http://authentik-server.iam.svc.cluster.local/application/o/yourservice/jwks/ ``` **Store secrets in Vault:** ```bash # Store the client secret for runtime retrieval talos put cluster/YOURSERVICE_OIDC_CLIENT_SECRET YOURSERVICE_OIDC_CLIENT_SECRET="$CLIENT_SECRET" # Store other config if needed talos put cluster/yourservice-oidc \ client_id=yourservice \ issuer_url="http://authentik-server.iam.svc.cluster.local/application/o/yourservice/" \ callback_uri="https://your-app.riotpiao.homelab.com/oauth2/callback" ``` ### Step 5: Mount Credentials in Your Service **Option A: Kubernetes Secret (ConfigMap-based)** Create a Kubernetes secret with the credentials: ```bash kubectl create secret generic yourservice-oidc \ --from-literal=client_id=yourservice \ --from-literal=client_secret="$CLIENT_SECRET" \ -n your-namespace ``` Reference in your Helm values or Pod spec: ```yaml # In Helm values env: - name: OIDC_CLIENT_ID valueFrom: secretKeyRef: name: yourservice-oidc key: client_id - name: OIDC_CLIENT_SECRET valueFrom: secretKeyRef: name: yourservice-oidc key: client_secret ``` **Option B: Vault KV Secret (Runtime)** Store in Vault and retrieve at startup: ```bash # Store full config vault kv put secret/services/yourservice \ client_id=yourservice \ client_secret="$CLIENT_SECRET" \ issuer_url="http://authentik-server.iam.svc.cluster.local/application/o/yourservice/" # Service reads at startup: # curl -H "Authorization: Bearer $VAULT_TOKEN" \ # http://vault.storage.svc.cluster.local:8200/v1/secret/data/services/yourservice ``` --- ## Vault JWT Authentication (For Services) If your service needs to authenticate to Vault directly (beyond just reading secrets), create a JWT role: ### 1. Create Vault JWT Role ```bash # Authenticate to Vault (as cluster-admin or root) kubectl port-forward -n storage svc/vault 8200:8200 & export VAULT_ADDR=http://127.0.0.1:8200 vault login -method=oidc role=homelab # Create a service-specific JWT role vault write auth/jwt/role/yourservice \ role_type=jwt \ bound_audiences="vault" \ user_claim="sub" \ bound_claims='{"client_id":["yourservice"]}' \ policies="shell-secrets,service-read-secrets" \ ttl=4h \ max_ttl=8h ``` ### 2. Create Service Policy in Vault ```bash # Policy that allows the service to read its own secrets vault policy write service-read-yourservice - <<'EOF' path "secret/data/services/yourservice" { capabilities = ["read"] } path "secret/data/cluster/minio" { capabilities = ["read"] } EOF # Update the role to use this policy vault write auth/jwt/role/yourservice \ policies="service-read-secrets,service-read-yourservice" ``` ### 3. Service JWT Authentication Flow Your service obtains a Vault token from Authentik's JWT: ```python # Example service code (Python) import requests import json # 1. Authenticate to Authentik OIDC (get ID token) oidc_response = requests.post( "http://authentik-server.iam.svc.cluster.local/application/o/yourservice/token/", data={ "grant_type": "client_credentials", "client_id": "yourservice", "client_secret": os.environ["OIDC_CLIENT_SECRET"], "audience": "vault" # Important: request JWT for Vault } ) id_token = oidc_response.json()["access_token"] # 2. Authenticate to Vault using the JWT vault_response = requests.post( "http://vault.storage.svc.cluster.local:8200/v1/auth/jwt/login", data=json.dumps({ "role": "yourservice", "jwt": id_token }), headers={"Content-Type": "application/json"} ) vault_token = vault_response.json()["auth"]["client_token"] # 3. Use Vault token to read secrets secret_response = requests.get( "http://vault.storage.svc.cluster.local:8200/v1/secret/data/services/yourservice", headers={"X-Vault-Token": vault_token} ) secrets = secret_response.json()["data"]["data"] ``` --- ## RBAC & Groups ### Adding Group-Based Access Control Groups in Authentik map to Vault policies and app bindings: ```bash # Create a group for your service's administrators python3 k8s/talos-iam/provision_oidc.py --add-group "yourservice-admins" # Add users to the group (Authentik UI) # Applications → Groups → yourservice-admins → Users # Bind group to app (Authentik UI) # Applications → Applications → yourservice → Policies → Binding # Select the group and enable the binding ``` ### Group-Based Vault Policy Mapping Create a Vault policy that checks Authentik group membership: ```bash vault policy write service-admin-yourservice - <<'EOF' # Full access for admins path "secret/data/services/yourservice/*" { capabilities = ["read", "create", "update", "delete"] } path "secret/data/mcp/*" { capabilities = ["read"] } EOF # Create a separate JWT role for admins vault write auth/jwt/role/yourservice-admin \ role_type=jwt \ bound_audiences="vault" \ user_claim="sub" \ bound_claims='{"groups":["yourservice-admins"]}' \ policies="shell-secrets,service-admin-yourservice" \ ttl=4h ``` --- ## Verification Checklist After registration, verify everything works: ```bash # 1. Verify Authentik provider exists curl -H "Authorization: Bearer $AUTHENTIK_BOOTSTRAP_TOKEN" \ http://localhost:7000/api/v3/providers/oauth2/?name=yourservice # 2. Verify Authentik application exists curl -H "Authorization: Bearer $AUTHENTIK_BOOTSTRAP_TOKEN" \ http://localhost:7000/api/v3/core/applications/?slug=yourservice # 3. Verify K8s secret is mounted kubectl get secret yourservice-oidc -n your-namespace -o jsonpath='{.data}' | base64 -d # 4. Verify Vault JWT role vault read auth/jwt/role/yourservice # 5. Test OAuth login (requires port-forward to your app) # Open http://your-app.riotpiao.homelab.com/login # Should redirect to Authentik, then back to your app # 6. Test JWT auth to Vault (if applicable) # Service obtains ID token and authenticates to Vault vault login -method=jwt role=yourservice jwt=$ID_TOKEN ``` --- ## Troubleshooting ### "JWKS URL not found" error in Vault **Cause:** Authentik is not reachable from Vault pod at the configured URL. **Fix:** 1. Check Authentik is running: `kubectl get pods -n iam` 2. Verify URL uses in-cluster address: `http://authentik-server.iam.svc.cluster.local` 3. Test DNS from Vault pod: ```bash kubectl exec -n storage vault-0 -- nslookup authentik-server.iam.svc.cluster.local ``` ### "OAuth callback failed" or redirect loop **Cause:** Redirect URI in Authentik does not match what the service sends. **Fix:** 1. Check Authentik provider's Redirect URIs: UI → Applications → Providers → Edit 2. Must exactly match the URI your service redirects to (protocol, domain, port, path) 3. Common mistake: `https://` in service but `http://` in Authentik ### Service can't read K8s secret **Cause:** Secret is in wrong namespace or service account lacks permissions. **Fix:** 1. Verify secret exists: `kubectl get secret yourservice-oidc -n your-namespace` 2. Check RBAC for service account: ```bash kubectl describe sa yourservice -n your-namespace ``` ### "client_id mismatch" or "client authentication failed" **Cause:** Client secret in K8s secret does not match what's in Authentik. **Fix:** 1. Rotate the secret in Authentik (delete and recreate) 2. Update the K8s secret with the new value 3. Restart the service pod(s) --- ## Advanced: Custom Property Mappings For services that need custom JWT claims (e.g., MinIO's `policy` claim), use property mappings: ```bash # Create a custom scope with expression curl -X POST http://localhost:7000/api/v3/propertymappings/provider/scope/ \ -H "Authorization: Bearer $AUTHENTIK_BOOTSTRAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "yourservice-claims", "scope_name": "yourservice-custom", "expression": "return {\"department\": request.user.attributes.get(\"department\", \"unknown\")}" }' # Add to provider's property_mappings: # Authentik UI → Applications → Providers → Edit yourservice # Select the custom scope in the Property Mappings list ``` --- ## Integration Examples ### Integrate with Grafana See `k8s/logging/grafana-values.yaml` for an example of configuring generic OAuth in Grafana. ### Integrate with MinIO See `provision_oidc.py` for MinIO's group-based policy claim injection. ### Integrate with Custom Go App See `k8s/talos-iam/go-example-oidc/` for a minimal Authorization Code flow example. --- ## Related Documentation - [`README.md`](README.md) — Authentik deployment & key rotation - [`provision_oidc.py`](provision_oidc.py) — Automated OIDC provisioning for core apps - [`setup_vault.sh`](setup_vault.sh) — Vault initialization & JWT auth wiring - [`CLAUDE.md`](../CLAUDE.md) — Homelab architecture & secrets management