Files
anchorage/docs/authentik-setup.md
William Gill 12bf35caf8 anchorage v1.0 initial tree
Greenfield Go multi-tenant IPFS Pinning Service wire-compatible with the
IPFS Pinning Services API spec. Paired 1:1 with Kubo over localhost RPC,
clustered via embedded NATS JetStream, Postgres source-of-truth with
RLS-enforced tenancy, Fiber + huma v2 for the HTTP surface, Authentik
OIDC for session login with kid-rotated HS256 JWT API tokens.

Feature-complete against the 22-milestone build plan, including the
ship-it v1.0 gap items:

  * admin CLIs: drain/uncordon, maintenance, mint-token, rotate-key,
    prune-denylist, rebalance --dry-run, cache-stats, cluster-presences
  * TTL leader election via NATS KV, fence tokens, JetStream dedup
  * rebalancer (plan/apply split), reconciler, requeue sweeper
  * ristretto caches with NATS-backed cross-node invalidation
    (placements live-nodes + token denylist)
  * maintenance watchdog for stuck cluster-pause flag
  * Prometheus /metrics with CIDR ACL, HTTP/pin/scheduler/cache gauges
  * rate limiting: session (10/min) + anonymous global (120/min)
  * integration tests: rebalance, refcount multi-org, RLS belt
  * goreleaser (tar + deb/rpm/apk + Alpine Docker) targeting Gitea

Stack: Cobra/Viper, Fiber v2 + huma v2, embedded NATS JetStream,
pgx/sqlc/golang-migrate, ristretto, TypeID, prometheus/client_golang,
testcontainers-go.
2026-04-16 18:13:36 -05:00

293 lines
10 KiB
Markdown

# 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/<slug>/` — 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/<slug>/` 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 <https://jwt.io> 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:<port>/` 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 <email>` 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.