Files
homelab/k8s/talos-iam/OAUTH_APP_SETUP.md
T
Story Crater Bot 831dd50805 k8s/iam: add cloudnativepg postgres and vault + authentik
- PostgreSQL 3-replica HA with pgvector
- Vault S3 storage backend (MinIO)
- Authentik federated OIDC provider
- Vault auto-unseal via postStart hook
2026-08-18 15:08:00 -07:00

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

    kubectl get pods -n iam | grep authentik-server
    
  2. Vault is running (in storage namespace)

    kubectl get pods -n storage | grep vault-0
    
  3. SSH/API access to Authentik — port-forward available

    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:

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

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

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

  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:

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

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

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

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