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

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.
  • anchorage installed 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)
  • 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 anchoragemust 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)
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

  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:

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.