14 KiB
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.pywith 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:
-
Authentik is running (in
iamnamespace)kubectl get pods -n iam | grep authentik-server -
Vault is running (in
storagenamespace)kubectl get pods -n storage | grep vault-0 -
SSH/API access to Authentik — port-forward available
kubectl port-forward -n iam svc/authentik-server 7000:80 -
Vault bootstrap is complete (run
setup_vault.shif not already done) -
Required environment variables — populate these in
~/.authentik/.env:# Token for Authentik API (from bootstrap) AUTHENTIK_BOOTSTRAP_TOKEN=<your-token> # Service-specific client secret (generate via openssl rand -hex 32) # Example: MY_SERVICE_OIDC_CLIENT_SECRET=<generated-secret>
One-Line Setup (Recommended)
For most services, use the automated registration script:
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.com/oauth2/callback" \
--service-name-in-vault "MY_SERVICE" \
--vault-jwt-policy "shell-secrets"
The script will:
- ✅ Create an OAuth2 provider in Authentik (from credentials in Vault)
- ✅ Create an Application in Authentik
- ✅ Create/bind groups for RBAC (optional)
- ✅ Create a Kubernetes secret with the client credentials
- ✅ Create a Vault JWT role for the service's JWT auth method
- ✅ 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
# 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:
kubectl port-forward -n iam svc/authentik-server 7000:80 &
# http://localhost:7000/if/admin/ → login with akadmin
In Authentik UI:
-
Navigate to Applications → Providers → Create → OpenID Connect (OAuth2) Provider
-
Fill in:
- Name:
yourservice - Client ID:
yourservice(or custom) - Client Secret: (paste from
$CLIENT_SECRETabove) - Redirect URIs:
https://your-app.riotpiao.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)
- Name:
-
Save and note the provider slug (usually auto-generated from Name).
Step 3: Create Authentik Application
In Authentik UI:
-
Navigate to Applications → Applications → Create
-
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.com(optional, for app launcher)
- Name:
-
Save
Step 4: Configure OIDC in Your Service
Pass the following environment variables to your service:
# 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.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:
# 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.com/oauth2/callback"
Step 5: Mount Credentials in Your Service
Option A: Kubernetes Secret (ConfigMap-based)
Create a Kubernetes secret with the credentials:
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:
# 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:
# 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
# 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
# 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:
# 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:
# 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:
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:
# 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.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:
- Check Authentik is running:
kubectl get pods -n iam - Verify URL uses in-cluster address:
http://authentik-server.iam.svc.cluster.local - Test DNS from Vault pod:
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:
- Check Authentik provider's Redirect URIs: UI → Applications → Providers → Edit
- Must exactly match the URI your service redirects to (protocol, domain, port, path)
- Common mistake:
https://in service buthttp://in Authentik
Service can't read K8s secret
Cause: Secret is in wrong namespace or service account lacks permissions.
Fix:
- Verify secret exists:
kubectl get secret yourservice-oidc -n your-namespace - Check RBAC for service account:
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:
- Rotate the secret in Authentik (delete and recreate)
- Update the K8s secret with the new value
- 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:
# 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— Authentik deployment & key rotationprovision_oidc.py— Automated OIDC provisioning for core appssetup_vault.sh— Vault initialization & JWT auth wiringCLAUDE.md— Homelab architecture & secrets management