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.
293 lines
10 KiB
Markdown
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.
|