- 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>
11 KiB
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.
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:
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
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
# 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
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:
# 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:
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
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
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
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):
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):
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
- Open
$SERVICE_URLin a browser - Click SSO / OIDC login
- Should redirect to
auth.echo6.co→ authenticate → redirect back to the app - Verify user info is correct (email, display name)
Step 8: Store Credentials
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:
- Access token validity too short — increase to at least
hours=1 - Missing
offline_accessscope — app can't refresh tokens, session expires immediately - Missing signing key — JWKS endpoint returns empty, app can't verify tokens
Debug via API:
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:
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
- JWKS endpoint is empty → signing key missing from provider
- Authentik unreachable from the app → test with
curlfrom the app's host - 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:
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)
# 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 |