480 lines
14 KiB
Markdown
480 lines
14 KiB
Markdown
# 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=<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:
|
|
|
|
```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.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.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.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.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.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.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
|