- Documents recent infrastructure cleanup (8 CTs destroyed, 35 DNS records removed, Headscale cleanup) - Adds 24 new runbooks covering Authentik, PeerTube, Meshtastic, RECON, Proxmox, Mailcow, Internet Archive, GPU routing - Adds project documentation for headscale, vaultwarden, peertube, matrix, mmud, advbbs, arr stack - Updates services.md, environment.md, caddy.md, authentik.md to match live infrastructure - Removes 4 deprecated runbook duplicates (canonical versions live in projects/) - Adds .gitignore for binary archives and editor temp files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
353 lines
11 KiB
Markdown
353 lines
11 KiB
Markdown
# Add Authentik OIDC to an Application
|
|
|
|
Fully automated via Authentik API. No web UI interaction required.
|
|
|
|
**Prerequisite:** DNS must already exist for the service (run expose-service-contabo.md or expose-service-home.md first).
|
|
|
|
**Authentik instance:** https://auth.echo6.co (Contabo, 100.64.0.6)
|
|
|
|
---
|
|
|
|
## Inputs
|
|
|
|
Prompt the user for all of these before executing any steps:
|
|
|
|
```
|
|
SERVICE_NAME= # Human-readable (e.g., "Vaultwarden", "Headplane")
|
|
SERVICE_SLUG= # URL-safe, lowercase (e.g., "vaultwarden", "headplane")
|
|
SERVICE_URL= # Base URL (e.g., "https://vault.echo6.co")
|
|
OIDC_CALLBACK_PATH= # App's OIDC callback (e.g., "/oidc/callback")
|
|
NEEDS_OFFLINE_ACCESS= # yes/no — does the app need refresh tokens?
|
|
CLIENT_TYPE= # confidential (server-side) or public (SPA/mobile)
|
|
```
|
|
|
|
The redirect URI is `${SERVICE_URL}${OIDC_CALLBACK_PATH}`.
|
|
|
|
### When to set NEEDS_OFFLINE_ACCESS=yes
|
|
|
|
- The app stores sessions that must survive service restarts (Headscale, Vaultwarden)
|
|
- The app uses refresh tokens for long-lived sessions
|
|
- Users shouldn't have to re-authenticate after every restart
|
|
|
|
### Reserved slugs
|
|
|
|
These conflict with Authentik's internal OAuth2 endpoints and **cannot be used**: `authorize`, `token`, `device`, `userinfo`, `introspect`, `revoke`.
|
|
|
|
---
|
|
|
|
## Step 1: Get API Token
|
|
|
|
Create an API token from the Authentik admin account. This only needs to happen once — reuse the token across all OIDC setups.
|
|
|
|
```bash
|
|
ssh root@100.64.0.6 "docker exec authentik-server \
|
|
ak create_token --user akadmin --identifier oidc-automation --expiring 2>/dev/null \
|
|
|| echo 'Token may already exist — check credentials file'"
|
|
```
|
|
|
|
If the token already exists, retrieve it from `/home/zvx/projects/.ref/credentials` (`AUTHENTIK_API_TOKEN`).
|
|
|
|
Store it for use in subsequent steps:
|
|
|
|
```bash
|
|
AK_TOKEN="<token>"
|
|
AK_API="https://auth.echo6.co/api/v3"
|
|
```
|
|
|
|
---
|
|
|
|
## Step 2: Look Up Authentik Internal IDs
|
|
|
|
The API requires UUIDs for flows, scope mappings, and signing keys. These are stable per Authentik instance but must be looked up once.
|
|
|
|
### Authorization flow
|
|
|
|
```bash
|
|
ssh root@100.64.0.6 "curl -s \
|
|
-H 'Authorization: Bearer $AK_TOKEN' \
|
|
'$AK_API/flows/instances/?slug=default-provider-authorization-implicit-consent' \
|
|
| jq -r '.results[0].pk'"
|
|
```
|
|
|
|
Store as `AUTH_FLOW_PK`.
|
|
|
|
### Scope mappings
|
|
|
|
```bash
|
|
# Get all scope mapping UUIDs at once
|
|
ssh root@100.64.0.6 "curl -s \
|
|
-H 'Authorization: Bearer $AK_TOKEN' \
|
|
'$AK_API/propertymappings/provider/scope/?ordering=scope_name' \
|
|
| jq -r '.results[] | select(.scope_name == \"openid\" or .scope_name == \"email\" or .scope_name == \"profile\" or .scope_name == \"offline_access\") | \"\(.scope_name): \(.pk)\"'"
|
|
```
|
|
|
|
Store each UUID: `SCOPE_OPENID_PK`, `SCOPE_EMAIL_PK`, `SCOPE_PROFILE_PK`, `SCOPE_OFFLINE_PK`.
|
|
|
|
### Signing key
|
|
|
|
```bash
|
|
ssh root@100.64.0.6 "curl -s \
|
|
-H 'Authorization: Bearer $AK_TOKEN' \
|
|
'$AK_API/crypto/certificatekeypairs/?name=authentik+Self-signed+Certificate&has_key=true' \
|
|
| jq -r '.results[0].pk'"
|
|
```
|
|
|
|
Store as `SIGNING_KEY_PK`.
|
|
|
|
### Gate
|
|
|
|
All five values must be non-null. If any are missing, Authentik's default objects may not have been created yet — check that the instance is healthy.
|
|
|
|
---
|
|
|
|
## Step 3: Create the OAuth2 Provider
|
|
|
|
Build the scope mappings array based on whether offline_access is needed:
|
|
|
|
```bash
|
|
# Base scopes (always included)
|
|
SCOPES="[\"$SCOPE_OPENID_PK\", \"$SCOPE_EMAIL_PK\", \"$SCOPE_PROFILE_PK\"]"
|
|
|
|
# Add offline_access if needed
|
|
if [ "$NEEDS_OFFLINE_ACCESS" = "yes" ]; then
|
|
SCOPES="[\"$SCOPE_OPENID_PK\", \"$SCOPE_EMAIL_PK\", \"$SCOPE_PROFILE_PK\", \"$SCOPE_OFFLINE_PK\"]"
|
|
fi
|
|
```
|
|
|
|
Create the provider:
|
|
|
|
```bash
|
|
PROVIDER_RESPONSE=$(ssh root@100.64.0.6 "curl -s \
|
|
-X POST '$AK_API/providers/oauth2/' \
|
|
-H 'Authorization: Bearer $AK_TOKEN' \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
\"name\": \"$SERVICE_NAME\",
|
|
\"authorization_flow\": \"$AUTH_FLOW_PK\",
|
|
\"client_type\": \"$CLIENT_TYPE\",
|
|
\"redirect_uris\": [{
|
|
\"matching_mode\": \"strict\",
|
|
\"url\": \"${SERVICE_URL}${OIDC_CALLBACK_PATH}\"
|
|
}],
|
|
\"signing_key\": \"$SIGNING_KEY_PK\",
|
|
\"property_mappings\": $SCOPES,
|
|
\"access_token_validity\": \"hours=1\",
|
|
\"refresh_token_validity\": \"days=30\"
|
|
}'")
|
|
|
|
# Extract the values we need
|
|
PROVIDER_PK=$(echo "$PROVIDER_RESPONSE" | jq -r '.pk')
|
|
CLIENT_ID=$(echo "$PROVIDER_RESPONSE" | jq -r '.client_id')
|
|
CLIENT_SECRET=$(echo "$PROVIDER_RESPONSE" | jq -r '.client_secret')
|
|
|
|
echo "Provider PK: $PROVIDER_PK"
|
|
echo "Client ID: $CLIENT_ID"
|
|
echo "Client Secret: $CLIENT_SECRET"
|
|
```
|
|
|
|
### Gate
|
|
|
|
`PROVIDER_PK` must be a number (not null or an error). If the API returns an error, common causes:
|
|
|
|
- **Duplicate name** — a provider with this name already exists
|
|
- **Invalid flow PK** — the authorization flow UUID is wrong
|
|
- **Invalid scope PK** — one of the scope mapping UUIDs is wrong
|
|
|
|
---
|
|
|
|
## Step 4: Create the Application
|
|
|
|
```bash
|
|
ssh root@100.64.0.6 "curl -s \
|
|
-X POST '$AK_API/core/applications/' \
|
|
-H 'Authorization: Bearer $AK_TOKEN' \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
\"name\": \"$SERVICE_NAME\",
|
|
\"slug\": \"$SERVICE_SLUG\",
|
|
\"provider\": $PROVIDER_PK,
|
|
\"meta_launch_url\": \"$SERVICE_URL\"
|
|
}' | jq '{name: .name, slug: .slug, provider: .provider}'"
|
|
```
|
|
|
|
### Gate
|
|
|
|
Response must include the slug and provider PK. If it fails, the slug may already be in use.
|
|
|
|
---
|
|
|
|
## Step 5: Verify Authentik Side
|
|
|
|
### Discovery endpoint
|
|
|
|
```bash
|
|
curl -s "https://auth.echo6.co/application/o/$SERVICE_SLUG/.well-known/openid-configuration" | jq '{issuer, authorization_endpoint, token_endpoint, jwks_uri}'
|
|
```
|
|
|
|
Must return all four fields with valid URLs.
|
|
|
|
### JWKS endpoint
|
|
|
|
```bash
|
|
curl -s "https://auth.echo6.co/application/o/$SERVICE_SLUG/jwks/" | jq '.keys | length'
|
|
```
|
|
|
|
Must return at least `1`. If it returns `0`, the signing key was not attached to the provider — go back and fix Step 3.
|
|
|
|
---
|
|
|
|
## Step 6: Configure the Application
|
|
|
|
This step varies per application. Use the Client ID, Client Secret, and issuer URL from above.
|
|
|
|
### OIDC endpoints (all derived from the slug)
|
|
|
|
```
|
|
Issuer: https://auth.echo6.co/application/o/$SERVICE_SLUG/
|
|
Authorize: https://auth.echo6.co/application/o/authorize/
|
|
Token: https://auth.echo6.co/application/o/token/
|
|
User Info: https://auth.echo6.co/application/o/userinfo/
|
|
JWKS: https://auth.echo6.co/application/o/$SERVICE_SLUG/jwks/
|
|
```
|
|
|
|
Most apps only need the **Issuer** (or Discovery URL) plus Client ID and Client Secret. The app auto-discovers the rest.
|
|
|
|
### Common config patterns
|
|
|
|
**Environment variables (Docker):**
|
|
|
|
```bash
|
|
OIDC_ISSUER=https://auth.echo6.co/application/o/$SERVICE_SLUG/
|
|
OIDC_CLIENT_ID=$CLIENT_ID
|
|
OIDC_CLIENT_SECRET=$CLIENT_SECRET
|
|
OIDC_SCOPES="openid email profile" # add offline_access if needed
|
|
OIDC_REDIRECT_URI=${SERVICE_URL}${OIDC_CALLBACK_PATH}
|
|
```
|
|
|
|
**Config file (YAML):**
|
|
|
|
```yaml
|
|
oidc:
|
|
issuer: "https://auth.echo6.co/application/o/$SERVICE_SLUG/"
|
|
client_id: "$CLIENT_ID"
|
|
client_secret: "$CLIENT_SECRET"
|
|
scope: ["openid", "profile", "email"] # add "offline_access" if needed
|
|
```
|
|
|
|
### Common alternate names for these values
|
|
|
|
| Concept | Names you'll see |
|
|
|---------|-----------------|
|
|
| Issuer | `authority`, `issuer_url`, `sso_authority`, `provider_url` |
|
|
| Client ID | `client_id`, `oidc_client_id`, `sso_client_id` |
|
|
| Client Secret | `client_secret`, `oidc_client_secret`, `sso_client_secret` |
|
|
| Redirect URI | `redirect_uri`, `callback_url`, `oidc_redirect_url` |
|
|
| Scopes | `scope`, `scopes`, `oidc_scopes`, `sso_scopes` |
|
|
|
|
---
|
|
|
|
## Step 7: Test Login
|
|
|
|
1. Open `$SERVICE_URL` in a browser
|
|
2. Click SSO / OIDC login
|
|
3. Should redirect to `auth.echo6.co` → authenticate → redirect back to the app
|
|
4. Verify user info is correct (email, display name)
|
|
|
|
---
|
|
|
|
## Step 8: Store Credentials
|
|
|
|
```bash
|
|
cat >> /home/zvx/projects/.ref/credentials << EOF
|
|
|
|
# $SERVICE_NAME OIDC
|
|
${SERVICE_SLUG^^}_OIDC_CLIENT_ID=$CLIENT_ID
|
|
${SERVICE_SLUG^^}_OIDC_CLIENT_SECRET=$CLIENT_SECRET
|
|
${SERVICE_SLUG^^}_OIDC_ISSUER=https://auth.echo6.co/application/o/$SERVICE_SLUG/
|
|
EOF
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### SSO login redirects back to login page (loop)
|
|
|
|
Check in order:
|
|
|
|
1. **Access token validity too short** — increase to at least `hours=1`
|
|
2. **Missing `offline_access` scope** — app can't refresh tokens, session expires immediately
|
|
3. **Missing signing key** — JWKS endpoint returns empty, app can't verify tokens
|
|
|
|
Debug via API:
|
|
|
|
```bash
|
|
ssh root@100.64.0.6 "curl -s \
|
|
-H 'Authorization: Bearer $AK_TOKEN' \
|
|
'$AK_API/providers/oauth2/?search=$SERVICE_NAME' \
|
|
| jq '.results[0] | {name, client_id, signing_key, access_token_validity, refresh_token_validity, property_mappings}'"
|
|
```
|
|
|
|
Or via ak shell:
|
|
|
|
```bash
|
|
ssh root@100.64.0.6 "docker exec authentik-server ak shell -c \"
|
|
from authentik.providers.oauth2.models import OAuth2Provider
|
|
p = OAuth2Provider.objects.get(name='$SERVICE_NAME')
|
|
print(f'Access Token: {p.access_token_validity}')
|
|
print(f'Refresh Token: {p.refresh_token_validity}')
|
|
print(f'Signing Key: {p.signing_key}')
|
|
print(f'Scopes: {list(p.property_mappings.values_list(\\\"scope_name\\\", flat=True))}')
|
|
\""
|
|
```
|
|
|
|
### "Failed to discover OpenID provider" / discovery error
|
|
|
|
1. JWKS endpoint is empty → signing key missing from provider
|
|
2. Authentik unreachable from the app → test with `curl` from the app's host
|
|
3. Wrong issuer URL → must include trailing slash, must match the slug exactly
|
|
|
|
### "Invalid redirect URI"
|
|
|
|
The redirect URI in the app config must **exactly** match what's in Authentik — scheme, trailing slashes, path, everything.
|
|
|
|
### User authenticated but gets "access denied"
|
|
|
|
User isn't authorized for the application. By default all authenticated users have access. If you've added group restrictions via policy bindings, verify the user is in the correct group:
|
|
|
|
```bash
|
|
ssh root@100.64.0.6 "curl -s \
|
|
-H 'Authorization: Bearer $AK_TOKEN' \
|
|
'$AK_API/core/applications/$SERVICE_SLUG/' \
|
|
| jq '{name, slug, policy_engine_mode}'"
|
|
```
|
|
|
|
### Token/session breaks after service restart
|
|
|
|
Missing `offline_access` scope. Without refresh tokens, sessions only last as long as the access token validity.
|
|
|
|
### Delete and recreate (nuclear option)
|
|
|
|
```bash
|
|
# Delete application first (it references the provider)
|
|
ssh root@100.64.0.6 "curl -s -X DELETE \
|
|
-H 'Authorization: Bearer $AK_TOKEN' \
|
|
'$AK_API/core/applications/$SERVICE_SLUG/'"
|
|
|
|
# Then delete provider
|
|
ssh root@100.64.0.6 "curl -s -X DELETE \
|
|
-H 'Authorization: Bearer $AK_TOKEN' \
|
|
'$AK_API/providers/oauth2/$PROVIDER_PK/'"
|
|
```
|
|
|
|
Then re-run from Step 3.
|
|
|
|
---
|
|
|
|
## Quick Reference: Existing OIDC Applications
|
|
|
|
| Application | Slug | Redirect URI | offline_access |
|
|
|-------------|------|-------------|----------------|
|
|
| Headscale | `headscale` | `https://vpn.echo6.co/oidc/callback` | Yes |
|
|
| Headplane | `headplane` | `https://vpn.echo6.co/admin/oidc/callback` | No |
|
|
| Vaultwarden | `vaultwarden` | `https://vault.echo6.co/identity/connect/oidc-signin` | Yes |
|