Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLIPublic
Assigneesstack72

Relationships

#2182 Auth gate: require authentication, fail open on outage

Opened by stack72 · 9/16/2026· Shipped 9/16/2026

Problem

Today, swamp runs fully anonymous by default. Of ~181 commands, only 3 gate on authentication (vault create, two serve subcommands, two datastore setup subcommands). Everything else works without a swamp-club account. Users get a daily nudge to log in, but it's skippable.

We're changing this: every CLI event requires a swamp-club account. But we can't let a swamp-club outage brick the CLI for authenticated users.

Design principle: You must prove who you are. Once you have, we won't punish you for our infrastructure being down.

Design: Opaque Keys + Signed Verification Proof

Full design doc: https://claude.ai/code/artifact/bb9d365d-fe65-447b-9ace-cfff45f84d50

Why not JWTs as credentials?

JWTs are hard to revoke — once issued, they're valid until expiry. We need instant revocation. Instead, we keep the opaque API key as the credential (instantly revocable — just delete from DB) and add a server-signed verification proof that the CLI caches for offline use.

How it works

  1. The API key stays opaque (swamp_org_* or personal key in auth.json). The server validates it by DB lookup. Revocation = delete the record.
  2. On a successful /api/whoami call, the server returns the identity response plus a signed verification proof: a canonical JSON payload signed with Ed25519, containing user identity, scopes, collectives, and a fingerprint of the API key (fpr).
  3. The CLI caches this proof locally (auth_verified.json for interactive, SWAMP_SIGNIN_TOKEN env var for CI).
  4. On subsequent runs, the CLI validates the proof locally (microsecond signature check — no server call in the hot path).
  5. Once a week (background, non-blocking), the CLI calls /api/whoami to refresh the proof and catch revocations.

Proof payload structure

{
  "sub": "user or collective ID",
  "iat": 1726000000,
  "exp": 1727209600,
  "fpr": "sha256 fingerprint of the API key",
  "kid": "which signing key was used",
  "org": ["collective-slugs"],
  "scopes": ["vault:*", "serve:*"]
}

The server returns alongside the existing whoami response:

  • verificationProof — canonical JSON payload string
  • verificationSignature — raw Ed25519 signature, base64url-encoded
  • publicKeys — array of { kid, key } for verification (usually one; two during key rotation)

Three rules

  1. No credential? Hard block. No auth.json and no SWAMP_API_KEY → exit with "run swamp auth login".
  2. Must verify at least once. Credential must be verified via /api/whoami to obtain a signed proof. Without a cached proof, the server must be reachable.
  3. Verified before + server unreachable = fail open. Valid cached proof (signature OK, not expired, fpr matches key) → proceed. A live 401 always overrides. A timeout/5xx never blocks.

Startup flow

credential exists?
  no → hard block
  yes → valid signed proof? (from auth_verified.json or SWAMP_SIGNIN_TOKEN)
    no → call /api/whoami
      200 OK → cache proof, proceed
      any failure → hard block
    yes → proceed immediately (local validation only, no server call)

Weekly background check (non-blocking):

  • Proof iat older than 7 days → call /api/whoami in background
  • 200 OK → refresh proof
  • 401/403 → delete proof, next command hard blocks
  • timeout/5xx → retry next run

CI: SWAMP_SIGNIN_TOKEN

Ephemeral CI environments have no persistent filesystem. The proof cache doesn't survive between runs. Solution:

SWAMP_API_KEY=[REDACTED-SECRET-1]
SWAMP_SIGNIN_TOKEN=<proof>.<signature>
  • SWAMP_API_KEY — the credential. Used for server calls. Instantly revocable.
  • SWAMP_SIGNIN_TOKEN — the signed proof. Used for offline verification. Lasts forever. Set once when the token is created on swamp-club, never touch again unless the API key is rotated.
  • Bound to the API key via fpr — rotating the key automatically invalidates the signin token.
  • Both values come from the token creation page on swamp-club (same "copy once" pattern as the API key today).

Error classification

Signal Classification Action
200 authenticated: true verified Use live identity, cache fresh proof
401 / authenticated: false rejected Delete proof, hard block, require re-login
403 rejected Hard block
429, 5xx, timeout, DNS failure transient Use cached proof, proceed in degraded mode

Never fail open on a rejection. A 401/403 is the server actively revoking access.

Revocation

  • Normal: Delete opaque key from DB. Weekly check returns 401. CLI deletes proof, next command blocks. Revocation window: at most 7 days for interactive users.
  • CI signin token: Revocation window lasts until next reachable run (CI almost always has connectivity).
  • Emergency (signing key leak): Rotate the signing key. All cached proofs become unverifiable instantly. Everyone must re-verify once. This is the nuclear option.

Public key distribution

  • Public keys come from the whoami response itself — no separate JWKS endpoint.
  • CLI caches them in auth_verified.json alongside the proof.
  • CLI binary ships with an embedded public key as fallback for first-ever offline use.
  • During key rotation, whoami returns both old and new public keys. Grace period: 30 days (configurable via better-auth jwt plugin).

Key management

Uses better-auth's jwt plugin for Ed25519 key pair lifecycle (generation, encrypted storage, rotation tracking). We use it for key management only — no JWT issuance. The existing apiKey plugin continues to handle credential validation (DB lookup).

What stays exempt

These commands work without auth (they're the path TO auth):

  • swamp auth login
  • swamp auth logout
  • swamp auth status
  • swamp --version / swamp --help

Implementation Plan

Ordered by dependency. Each step is a shippable unit.

1. Server: Ed25519 key management

Add the jwt plugin to better-auth config for key pair lifecycle. Configure Ed25519 (EdDSA). Key management only — no JWT issuance, no JWKS endpoint.

  • swamp-club: lib/auth.ts

2. Server: return signed proof from /api/whoami

On authenticated /api/whoami calls, include verificationProof (canonical JSON), verificationSignature (base64url Ed25519 signature), and publicKeys (array of { kid, key }). Default proof expiry: 14 days for interactive. On collective token creation, also return a signin token (proof with no expiry) alongside the API key.

  • swamp-club: routes/api/whoami
  • swamp-club: routes/api/collective-tokens

3. CLI: proof verification and storage

New ProofVerifier that validates Ed25519 signature against cached public keys (by kid, supporting multiple keys for rotation). Checks exp (if set) and fpr. Falls back to embedded public key. Reads proofs from auth_verified.json (interactive) or SWAMP_SIGNIN_TOKEN env var (CI). New AuthVerificationRepository.

  • src/domain/auth/proof_verifier.ts
  • src/domain/auth/embedded_public_key.ts
  • src/infrastructure/persistence/auth_verification_repository.ts

4. CLI: auth gate service

Domain service implementing the startup flow: check for credential, validate cached proof locally, decide pass/block. Schedule the weekly background whoami check. Replaces current best-effort identity loading in mod.ts.

  • src/domain/auth/auth_gate_service.ts
  • src/cli/mod.ts
  • src/cli/auth_context.ts

5. Update swamp auth login

After the existing login flow calls /api/whoami, store the signed proof in auth_verified.json alongside the opaque key in auth.json. No changes to credential format.

  • src/libswamp/auth/login.ts

6. Add authMode to telemetry

Extend TelemetryContext with authMode ("verified", "offline", "none"). Blocked runs send a single event with the block reason.

  • src/infrastructure/telemetry/

Open Questions

  • Signing key rotation grace period: 30 days default via better-auth. Need a runbook for planned vs emergency rotations.
  • Weekly check frequency: 7 days proposed. Should this be server-configurable (returned in proof claims)?
  • Signin token format: SWAMP_SIGNIN_TOKEN needs to carry proof + signature + public key in one value. Dot-separated base64 or single base64 JSON blob?
  • Auth nudge removal: Daily nudge becomes redundant. Remove or repurpose for "proof expires in N days" warnings?
  • Signin token on swamp-club UI: Token creation page needs to show the signin token alongside the API key. Single "copy both" or two separate buttons?
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 7 MOREREVIEW+ 9 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

9/16/2026, 11:11:24 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack729/16/2026, 5:08:36 PM

Sign in to post a ripple.