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.
10 KiB
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 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. anchorageinstalled and reachable on that hostname (see ../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)emailprofilegroups— 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 organchorage-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-userswith Enabled = true, Negate = false - Bind
anchorage-sysadminswith 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) |
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:
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:
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
# 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
- Open
https://anchor.example.com/in a browser. - Click Sign in → should redirect to
https://auth.example.com/application/o/authorize/?client_id=anchorage-web&…. - Log in at Authentik.
- You are redirected back to
https://anchor.example.com/?code=…&state=…. - 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:
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 (
httpsvshttp). - 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
audis the Client ID. Setauth.authentik.audiencein anchorage.yaml to the same value (typicallyanchorage-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
emailfield. bootstrap.sysadminsis only read on the first login for a givenauthentik_sub. For an existing user, useanchorage 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:
- Confirm the new
kidis in the JWKS document. - 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.