Files
homelab/k8s/security/iam/OAUTH_APP_SETUP.md
T

480 lines
14 KiB
Markdown
Raw Normal View History

# 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.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