Files
warmbly/docs/INTERNAL_API_AUTH.md
T
Matthew Meszaros 53c5371bf6 docs: SaaS-style README, internal-auth doc, vendor lock-in audit
README is now a landing-page-style entry point: centered hero banner,
badges, tagline, feature grid, architecture diagram, self-hosting
table, stack rationale, quick start, project layout, testing summary,
docs index. Image placeholders reference docs/assets/{banner,
dashboard-preview,admin-preview}.png with sizing guidance in
docs/assets/README.md.

docs/INTERNAL_API_AUTH.md walks through the bearer-token model the
worker uses to call /api/v1/internal/* endpoints: where the token
lives on both sides, the constant-time compare, fail-closed semantics
when INTERNAL_API_TOKEN is unset, and the planned per-worker JWT
upgrade path.

docs/VENDOR_LOCKIN.md is the honest scorecard: each external
dependency (AWS KMS, DynamoDB, S3, Cloud Tasks, Stripe, Turnstile,
Sentry) gets a row covering self-host status, alternatives, and what
'free of this dependency' actually looks like. Includes the minimum-
viable self-host env-var set.
2026-05-27 14:44:23 +00:00

4.9 KiB

Internal API Auth

The backend exposes a set of internal endpoints under /api/v1/internal/* that workers and other backend processes call. These are not user-facing — they sit behind a dedicated bearer-token middleware so workers can fetch encrypted DEKs, their runtime config, and report heartbeats without ever opening a Postgres connection of their own.

Endpoints

Method Path Caller Purpose
GET /api/v1/internal/dek/:userID worker Fetch a user's encrypted DEK
PUT /api/v1/internal/dek/:userID worker (rarely) Store a new encrypted DEK
DELETE /api/v1/internal/dek/:userID admin / cleanup Delete an encrypted DEK
GET /api/v1/internal/worker/config worker Fetch runtime config for the worker
POST /api/v1/internal/worker/heartbeat worker Liveness report

How auth works

A single static bearer token, sourced from the INTERNAL_API_TOKEN environment variable on the backend, gates every endpoint in the group. Workers send the same token in Authorization: Bearer ….

Worker                              Backend
------                              -------
GET /api/v1/internal/dek/<uuid>
Authorization: Bearer <token>  --->
                                    middleware.InternalAuthMiddleware
                                      └─ subtle.ConstantTimeCompare token
                                    if OK -> handler runs
                                    if NOT -> 401 Unauthorized
                              <---  200 OK { "encrypted_data_key": "..." }
                                or  401 Unauthorized
                                or  404 Not Found (no DEK)

Fail-closed

If INTERNAL_API_TOKEN is unset on the backend, the middleware rejects every request with 401. This is deliberate: workers must not be able to read DEKs from a misconfigured server that silently accepts unauthenticated calls.

Timing-safe compare

The token check uses crypto/subtle.ConstantTimeCompare so that an attacker can't recover the token byte-by-byte via timing differences.

Where the token lives

Process Env var Used for
Backend INTERNAL_API_TOKEN Validates incoming worker requests
Worker ENCRYPTED_KEYS_WORKER_TOKEN Sends in Authorization header

Both must hold the same string. In the self-hostable installer, the operator sets INTERNAL_API_TOKEN once on the backend and passes the same value as ENCRYPTED_KEYS_WORKER_TOKEN to each worker via the install script.

A reasonable token is 32 bytes of base64:

openssl rand -base64 32

Treat it like a password. Rotate by:

  1. Set the new token as a secondary value the backend will also accept (not yet implemented — for now, plan a short coordinated swap window).
  2. Roll the workers (install-worker.sh --update after updating the env).
  3. Remove the old token from the backend.

Why a shared token (for now)

It's the simplest primitive that satisfies the constraint: workers reach backend endpoints without touching the user-facing session/cookie auth. Workers are not "users" — they don't have a UI session, OAuth flow, or refresh token cycle. A shared bearer satisfies the "internal service identity" use case without overbuilding.

Planned: per-worker JWTs

The shared bearer is convenient but coarse. The follow-up plan, tracked alongside the worker config endpoint:

  1. Each worker registers once via POST /api/v1/worker/register with the shared bootstrap token.
  2. The backend mints a per-worker JWT signed with a backend-private key. The JWT carries the worker ID and tier in its claims.
  3. Workers send the JWT instead of the bootstrap token on all subsequent calls. The middleware verifies the signature and reads claims.
  4. The bootstrap token becomes a one-shot used only on first install.

Benefits: ability to revoke a single worker without rotating the whole fleet, audit-log a specific worker's actions, and tier-gate DEK access (e.g., a free-tier worker can only request DEKs for free-tier users).

Implementation lives behind Task #9 in the project tracker.

Where it lives in the code

  • Middleware: internal/api/middleware/internal_auth.go
  • Handlers: internal/api/handler/internal_dek.go, internal/api/handler/internal_worker_config.go
  • Routes: internal/api/routes.go (search for /api/v1/internal)
  • Worker-side client: internal/infrastructure/encryptedkeys/http.go
  • Tests: internal/api/middleware/internal_auth_test.go, internal/api/handler/internal_dek_test.go