# Setting up Authentik OIDC for anchorage anchorage delegates human authentication to Authentik (or any OIDC provider). The web UI runs an **Authorization Code + PKCE** flow against Authentik; anchorage validates the returned ID token against Authentik's JWKS and upserts the user on first login. API clients (`ipfs pin remote`, CI) use per-device JWTs minted by anchorage itself — see [auth-flow.md](auth-flow.md) for that side of the story. This document walks through the Authentik side, end-to-end, and the matching anchorage config. > Tested against Authentik 2024.12+; paths should be the same on any > recent 2024.x / 2025.x release. --- ## Prerequisites - A running Authentik instance with admin access. - A public hostname for anchorage (e.g. `https://anchor.example.com`) with valid TLS — OIDC requires HTTPS for non-localhost redirect URIs. - `anchorage` installed and reachable on that hostname (see [../deploy/README.md](../deploy/README.md)). --- ## 1. Create the OIDC provider **Applications → Providers → Create → OAuth2/OpenID Provider**. Under **Protocol settings**: | Field | Value | Notes | |---|---|---| | Name | `anchorage-oidc` | internal label | | Authentication flow | `default-authentication-flow` | or your org's SSO flow | | Authorization flow | `default-provider-authorization-implicit-consent` | explicit consent also works | | Client type | **Public** | anchorage's web UI is a browser app; no client secret | | Client ID | `anchorage-web` | must match `auth.authentik.clientID` in anchorage.yaml | | Client Secret | *(leave empty)* | public client + PKCE | | Redirect URIs / Origins | `https://anchor.example.com/` (one per line; add `http://localhost:5173/` etc. for dev) | exact match — including trailing `/` | | Signing Key | `authentik Self-signed Certificate` (default) or your managed cert | RS256 | | Subject mode | *Based on the User's hashed ID* | stable across email changes | | Include claims in id_token | ✅ enabled | needed so anchorage can read email/groups without a separate /userinfo call | | Issuer mode | **Each provider has a different issuer** | produces `https://auth/…/o//` — what anchorage expects | Under **Scopes**, grant: - `openid` (mandatory) - `email` - `profile` - `groups` — only if you add the property mapping in §4 below Save. On the provider's detail page note the **OpenID Configuration Issuer** URL — it will look like: ``` https://auth.example.com/application/o/anchorage/ ``` That exact URL (including the trailing slash) goes into `auth.authentik.issuer` in anchorage.yaml. --- ## 2. Create the Application **Applications → Applications → Create**: | Field | Value | |---|---| | Name | `anchorage` | | Slug | `anchorage` — **must match the `/o//` in the issuer URL above** | | Provider | `anchorage-oidc` (the one from §1) | | Launch URL | `https://anchor.example.com/` | | Icon / Group | optional, cosmetic | The slug is load-bearing: changing it later changes the issuer URL and invalidates every outstanding anchorage session. --- ## 3. Create groups and bind them to the application anchorage uses two roles that are checked against Authentik group membership (if you wire up §4) or against the bootstrap list in config. **Directory → Groups → Create**: - `anchorage-users` — ordinary members; can CRUD pins within their org - `anchorage-sysadmins` — platform-wide admin rights (drain, maintenance, cross-org audit) Add users to the appropriate groups. Then bind the groups to the application: **Applications → Applications → anchorage → Policy / Group / User Bindings → Create Binding**: - Bind `anchorage-users` with **Enabled = true**, **Negate = false** - Bind `anchorage-sysadmins` with **Enabled = true**, **Negate = false** Unbound Authentik users are now rejected at the Authentik layer before the ID token is ever minted — a defense-in-depth layer for anchorage's own authz. --- ## 4. (Optional) Property mapping for group-based roles If you want anchorage to decide sysadmin-ness from Authentik groups rather than the static `bootstrap.sysadmins` list, add a scope mapping that surfaces groups in the ID token. **Customisation → Property Mappings → Create → Scope Mapping**: | Field | Value | |---|---| | Name | `anchorage-groups` | | Scope name | `groups` | | Description | `Groups claim for anchorage` | | Expression | *(see below)* | ```python return { "groups": [g.name for g in request.user.ak_groups.all()], } ``` Attach this scope mapping to the provider under **Scopes**. Verify it works by decoding an issued ID token at and confirming the `groups` claim contains `anchorage-sysadmins` for your admin user. --- ## 5. Configure anchorage Open `/etc/anchorage/anchorage.yaml` and set the `auth` and `bootstrap` sections: ```yaml auth: authentik: # Exactly as shown on the Provider's detail page, trailing slash included. issuer: https://auth.example.com/application/o/anchorage/ # Must equal the Client ID configured in Authentik. clientID: anchorage-web # Authentik puts the client ID in the `aud` claim by default. # If you override audience via a property mapping, match it here. audience: anchorage-web apiToken: signingKeys: - id: "2026-04" path: /etc/anchorage/jwt.key primary: true defaultTTL: 24h # web-UI sessions maxTTL: 9480h # 1y + 30-day grace for IPFS client tokens bootstrap: # First Authentik login for one of these emails → user is promoted # to sysadmin. Consulted ONLY on first login; later changes need # `anchorage admin grant-sysadmin`. sysadmins: - admin@example.com ``` Restart anchorage: ```bash systemctl restart anchorage # package install # or: docker service update --force anchorage_anchorage-1 # Swarm ``` --- ## 6. Verify the flow end-to-end ### 6a. Reachability — Authentik side ```bash # Discovery document resolves and the issuer matches. curl -fsS https://auth.example.com/application/o/anchorage/.well-known/openid-configuration \ | jq '{issuer, jwks_uri, authorization_endpoint, token_endpoint}' # JWKS is reachable and non-empty. curl -fsS https://auth.example.com/application/o/anchorage/jwks/ | jq '.keys | length' # → 1 (or more) ``` If either call fails, anchorage can't verify ID tokens regardless of the rest. Fix network reachability / TLS / DNS first. ### 6b. Browser flow — anchorage side 1. Open `https://anchor.example.com/` in a browser. 2. Click **Sign in** → should redirect to `https://auth.example.com/application/o/authorize/?client_id=anchorage-web&…`. 3. Log in at Authentik. 4. You are redirected back to `https://anchor.example.com/?code=…&state=…`. 5. The web UI POSTs the code to `/v1/auth/session`; anchorage exchanges it with Authentik, validates the ID token, upserts the user, and returns a session cookie. Check the Postgres row: ```bash psql -c "SELECT id, email, authentik_sub, is_sysadmin, created_at FROM users ORDER BY created_at DESC LIMIT 5;" ``` You should see your user with `authentik_sub` = Authentik's hashed user ID and `is_sysadmin=true` if your email was in `bootstrap.sysadmins`. ### 6c. Mint an API token through the UI Once signed in, exercise the full token lifecycle: ``` POST /v1/tokens (with the session cookie) body: { "label": "laptop-cli", "scopes": ["pin:write"], "ttl_hours": 720 } ``` The returned JWT is what goes into `ipfs pin remote service add`. --- ## Troubleshooting ### "Invalid redirect URI" Authentik rejects the authorize request because the `redirect_uri` query parameter doesn't match any URL in the provider's list. - Compare byte-for-byte, including trailing `/`. - Check protocol (`https` vs `http`). - For local dev, add `http://localhost:/` to the provider's list. ### "Signature validation failed" anchorage fetched the JWKS but couldn't verify the ID token. - Kubernetes NetworkPolicy or egress firewall blocking Authentik? Curl the JWKS endpoint from inside the anchorage pod/container. - Was the signing key rotated in Authentik? anchorage caches JWKS for 1 hour; restart anchorage or wait for the cache to refresh. ### "iss claim invalid" anchorage's configured issuer doesn't match the `iss` in the ID token. - Confirm the provider's **Issuer mode** is "Each provider has a different issuer" (not the default "global issuer"). The former produces per-application issuers like `…/o/anchorage/`; the latter produces a bare `…/` which won't match. ### "aud claim not accepted" - Authentik's default `aud` is the Client ID. Set `auth.authentik.audience` in anchorage.yaml to the same value (typically `anchorage-web`). - If you configured a custom audience via a property mapping, set anchorage to match. ### Clock skew JWT validation rejects tokens whose `iat` is in the future or whose `exp` has passed. Authentik and anchorage must agree on wall-clock time within ~60s. - Run NTP / chronyd on both hosts. ### First login not promoted to sysadmin `bootstrap.sysadmins` matches on the `email` claim, exact + case- sensitive. - Check the email Authentik is actually sending: decode the ID token payload and look at the `email` field. - `bootstrap.sysadmins` is only read on the *first* login for a given `authentik_sub`. For an existing user, use `anchorage admin grant-sysadmin ` instead. ### Users can reach Authentik's login page but get "unauthorized" afterwards The user isn't a member of a group bound to the anchorage application. Add them to `anchorage-users` (or the equivalent) and retry. --- ## Rotating the Authentik signing certificate When Authentik rotates its signing cert, outstanding ID tokens keep working until they expire (anchorage trusts JWKS, which lists both old and new keys during overlap). Then: 1. Confirm the new `kid` is in the JWKS document. 2. anchorage's in-process JWKS cache refreshes every hour; to force a refresh immediately restart the process. API tokens minted by anchorage are signed with anchorage's own keys (`auth.apiToken.signingKeys`) and are unaffected by Authentik cert rotation — they only care about the Authentik issuer/audience validity at mint time.