Files
anchorage/internal/pkg/token/keyload.go
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

92 lines
2.8 KiB
Go

package token
import (
"errors"
"fmt"
"log/slog"
"os"
"strings"
)
// DevOnlySigningKey is the in-memory fallback anchorage uses when no
// signing keys are configured. It is long enough to satisfy the 32-byte
// minimum but trivially predictable — intentionally so, so operators
// notice the loud warning in logs and replace it.
//
// Never use this key in any environment that touches real data.
const DevOnlySigningKey = "dev-only-do-not-use-in-production-0123456789abcdef"
// readKeyFile reads a key file, strips a trailing newline, and enforces
// the 32-byte minimum. Kept unexported — callers use LoadSigningKeys.
//
// Trailing newline handling means `openssl rand -base64 48 > jwt.key`
// works without ceremony.
func readKeyFile(path string) ([]byte, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read signing key %s: %w", path, err)
}
key := []byte(strings.TrimRight(string(raw), "\r\n"))
if len(key) < 32 {
return nil, errors.New("signing key " + path + " is too short (need >= 32 bytes)")
}
return key, nil
}
// KeyFileSpec names a single signing key file on disk. Mirrors
// config.SigningKeyConfig in a shape the token package can depend on
// without importing config.
type KeyFileSpec struct {
Path string
ID string
Primary bool
}
// LoadSigningKeys validates and loads the rotation key set.
//
// - An empty slice returns a dev-only fallback (kid=DevKeyID, primary)
// with a loud warning so operators notice they haven't configured
// real keys.
// - A non-empty slice is validated (unique non-empty IDs, exactly one
// primary, 32-byte minimum on each file) and returned.
//
// Never returns an empty slice on success.
func LoadSigningKeys(rotation []KeyFileSpec) ([]SigningKey, error) {
if len(rotation) == 0 {
slog.Warn("token: no signing keys configured — using the built-in dev key. DO NOT use in production.")
return []SigningKey{{
ID: DevKeyID,
Key: []byte(DevOnlySigningKey),
Primary: true,
}}, nil
}
out := make([]SigningKey, 0, len(rotation))
seen := map[string]bool{}
primaries := 0
for i, spec := range rotation {
if spec.ID == "" {
return nil, fmt.Errorf("token: signingKeys[%d].id is required", i)
}
if spec.Path == "" {
return nil, fmt.Errorf("token: signingKeys[%d].path is required", i)
}
if seen[spec.ID] {
return nil, fmt.Errorf("token: duplicate signingKey id %q", spec.ID)
}
seen[spec.ID] = true
key, err := readKeyFile(spec.Path)
if err != nil {
return nil, fmt.Errorf("token: signingKeys[%d] (%q): %w", i, spec.ID, err)
}
out = append(out, SigningKey{ID: spec.ID, Key: key, Primary: spec.Primary})
if spec.Primary {
primaries++
}
}
if primaries != 1 {
return nil, fmt.Errorf("token: exactly one signingKey must be marked primary (got %d)", primaries)
}
return out, nil
}