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.
132 lines
4.3 KiB
YAML
132 lines
4.3 KiB
YAML
# anchorage — example configuration.
|
|
#
|
|
# Copy to configs/anchorage.yaml (or /etc/anchorage/anchorage.yaml) and edit
|
|
# as needed. All keys can also be overridden via environment variables with
|
|
# the ANCHORAGE_ prefix, e.g. ANCHORAGE_SERVER_PORT=9090.
|
|
|
|
server:
|
|
host: 0.0.0.0
|
|
port: 8080
|
|
readTimeout: 10s
|
|
writeTimeout: 30s
|
|
|
|
metrics:
|
|
# Prometheus /metrics is gated by a CIDR allowlist checked against
|
|
# the direct TCP peer IP (not X-Forwarded-For). Unset → loopback +
|
|
# RFC1918 defaults, which works for compose / swarm / k8s scrapers
|
|
# on the same internal network. Explicit empty list ([]) disables
|
|
# the ACL — use a firewall in that case.
|
|
# allowCIDRs:
|
|
# - "10.0.0.0/8"
|
|
# - "127.0.0.1/32"
|
|
|
|
rateLimit:
|
|
# POST /v1/auth/session attempts per IP per minute (brute-force guard).
|
|
sessionPerMinute: 10
|
|
# Unauthenticated requests per IP per minute across the API.
|
|
# Authenticated traffic (valid Bearer or session cookie) is exempt.
|
|
anonymousPerMinute: 120
|
|
|
|
node:
|
|
# Unique ID of this anchorage instance within the cluster.
|
|
# Defaults to the OS hostname when omitted.
|
|
id: node-1
|
|
# Public libp2p multiaddrs of this node's paired Kubo daemon.
|
|
# Clients receive these as PinStatus.delegates for direct data push.
|
|
multiaddrs:
|
|
- /dns4/node-1.example.com/tcp/4001/p2p/12D3Koo...
|
|
|
|
ipfs:
|
|
# HTTP RPC endpoint of the local paired Kubo daemon (same host / pod).
|
|
rpc: http://localhost:5001
|
|
timeout: 2m
|
|
reconciler:
|
|
# How often each node diffs its Postgres placements vs local `ipfs pin ls`.
|
|
interval: 10m
|
|
# When true, the reconciler re-pins drifted CIDs instead of just reporting.
|
|
autoRepair: false
|
|
|
|
cluster:
|
|
# 1 on a single-node install, 2+ for HA. See plan for placement rules.
|
|
minReplicas: 2
|
|
heartbeatInterval: 5s
|
|
downAfter: 30s
|
|
rebalanceInterval: 1m
|
|
# When true, the rebalancer acts; when false, it only logs what it would do.
|
|
autoRepair: false
|
|
# Time a drained node gives in-flight jobs before shutting down its consumer.
|
|
drainGracePeriod: 2m
|
|
maintenance:
|
|
# Safety rail: if cluster-wide maintenance has been on longer than this,
|
|
# emit a loud warning so forgotten flags don't silently mask outages.
|
|
maxDuration: 1h
|
|
|
|
postgres:
|
|
dsn: postgres://anchorage:***@pg-primary/anchorage?sslmode=require
|
|
maxConns: 20
|
|
# Run golang-migrate on boot. Safe with N replicas starting simultaneously
|
|
# thanks to Postgres advisory locks.
|
|
autoMigrate: true
|
|
requeueSweeper: 30s
|
|
|
|
nats:
|
|
dataDir: /var/lib/anchorage/nats
|
|
client:
|
|
host: 0.0.0.0
|
|
port: 4222
|
|
cluster:
|
|
name: anchorage
|
|
host: 0.0.0.0
|
|
port: 6222
|
|
# Peers discovered via gossip; list at least one existing cluster member.
|
|
routes:
|
|
- nats://peer-a:6222
|
|
- nats://peer-b:6222
|
|
jetstream:
|
|
# Stream replication factor. Capped at clusterSize at runtime.
|
|
replicas: 3
|
|
|
|
auth:
|
|
authentik:
|
|
issuer: https://auth.example.com/application/o/anchorage/
|
|
clientID: anchorage-web
|
|
audience: anchorage
|
|
apiToken:
|
|
# Signing keys. Exactly one entry must have `primary: true` — that's
|
|
# the key anchorage mints new tokens with; any additional entries
|
|
# are verify-only, used during a rotation overlap window. See
|
|
# deploy/README.md "Rotating the JWT signing key" for the
|
|
# three-step procedure.
|
|
#
|
|
# Leave signingKeys empty for local dev — anchorage will fall back
|
|
# to a built-in dev key with kid="dev" and log a loud warning.
|
|
signingKeys:
|
|
- id: "2026-04"
|
|
path: /etc/anchorage/jwt.key
|
|
primary: true
|
|
|
|
# Interactive / web-session tokens.
|
|
defaultTTL: 24h
|
|
# IPFS clients, CI, long-lived service identities — 1 year plus a
|
|
# 30-day grace window so operators have time to rotate before expiry.
|
|
maxTTL: 9480h
|
|
|
|
bootstrap:
|
|
# Emails promoted to sysadmin on their first Authentik login.
|
|
sysadmins:
|
|
- admin@example.com
|
|
|
|
logging:
|
|
# debug | info | warn | error. Controls the minimum level for the file sink.
|
|
level: info
|
|
# text | json — controls the stderr sink (warn+). The file sink is always JSON.
|
|
format: text
|
|
# When true, every record carries source file:line attributes.
|
|
source: false
|
|
# Empty disables the file sink; stderr then receives every level.
|
|
file: /var/log/anchorage/anchorage.log
|
|
maxSizeMB: 100
|
|
maxBackups: 10
|
|
maxAgeDays: 30
|
|
compress: true
|