mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-26 00:00:41 +00:00
53c5371bf6
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.
110 lines
4.9 KiB
Markdown
110 lines
4.9 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
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`
|