Allow providing the master API key via MASTER_API_KEY env var #19

Closed
opened 2026-08-07 10:19:41 +00:00 by james.oates · 1 comment
Owner

Problem

The master API key is only auto-generated on first boot (main.rs, when get_master_key() returns None) and printed once to stdout. There is no way to pre-define it — unlike ENCRYPTION_KEY, which the operator can supply via env.

This blocks automated / declarative deployment: you can't bake the master key into your deploy secrets ahead of time and have a fresh instance come up using it. You currently have to boot, scrape the key from the logs, and feed it back in — not viable for reproducible infra.

Fix

Read a MASTER_API_KEY env var at startup (unprefixed, matching ENCRYPTION_KEY/DATABASE_URL infra-config convention):

  • If set, it is the source of truth: seed it on first boot, or update the stored master key to match on later boots (declarative, like ENCRYPTION_KEY). Do not print it — the operator already has it.
  • If unset, keep current behaviour (auto-generate + print once on first boot).
  • Reject an obviously-weak provided value (min length guard).

Coordination with #18

Forward-compatible with hashing API keys at rest (#18): the env var always carries the raw key; when #18 lands, the ingestion path here hashes it before storing, exactly like a key created via the API. No change to the env contract.

Picking this up immediately — it unblocks automated ER deployment.

## Problem The master API key is **only** auto-generated on first boot (`main.rs`, when `get_master_key()` returns `None`) and printed once to stdout. There is no way to pre-define it — unlike `ENCRYPTION_KEY`, which the operator can supply via env. This blocks automated / declarative deployment: you can't bake the master key into your deploy secrets ahead of time and have a fresh instance come up using it. You currently have to boot, scrape the key from the logs, and feed it back in — not viable for reproducible infra. ## Fix Read a `MASTER_API_KEY` env var at startup (unprefixed, matching `ENCRYPTION_KEY`/`DATABASE_URL` infra-config convention): - If set, it is the source of truth: seed it on first boot, or update the stored master key to match on later boots (declarative, like `ENCRYPTION_KEY`). Do **not** print it — the operator already has it. - If unset, keep current behaviour (auto-generate + print once on first boot). - Reject an obviously-weak provided value (min length guard). ## Coordination with #18 Forward-compatible with hashing API keys at rest (#18): the env var always carries the **raw** key; when #18 lands, the ingestion path here hashes it before storing, exactly like a key created via the API. No change to the env contract. Picking this up immediately — it unblocks automated ER deployment.
Author
Owner

Resolved in 5a0008b.

MASTER_API_KEY (unprefixed, like ENCRYPTION_KEY) is read at startup and is authoritative when set:

  • fresh DB → seeded with the provided value;
  • already matches the stored key → no-op;
  • differs → the stored master key is rotated to the new value (the previous value stops working — logged as a warning, so it's a conscious, visible change);
  • unset → unchanged auto-generate-and-print-once behaviour.

A value under 16 chars is rejected at startup (loud failure beats a weak key in an automated deploy). The seed/generate/match/rotate decision is a pure decide_master_key function with a unit test per branch; the IO stays in main.

Secrets are never echoed (covers your follow-up on this): an env-provided master key is never printed — only an auto-generated one is (so you can capture it). While here, the encryption-key load paths now log which source they came from (env / file / persisted path) without the key material — they were previously silent.

Forward-compatible with #18: the env carries the raw key; the single equality check and the store write are each in one place for the hashing work to update.

Docs: MASTER_API_KEY added to the env table, plus a commented docker-compose.yml example. Tests: all five decision branches + a DB test proving a rotate makes the new value the master and invalidates the old one. Full suite green (168), clippy and fmt clean.

This unblocks pre-defining the key for automated ER deployment.

Resolved in 5a0008b. `MASTER_API_KEY` (unprefixed, like `ENCRYPTION_KEY`) is read at startup and is **authoritative** when set: - fresh DB → seeded with the provided value; - already matches the stored key → no-op; - differs → the stored master key is **rotated** to the new value (the previous value stops working — logged as a warning, so it's a conscious, visible change); - unset → unchanged auto-generate-and-print-once behaviour. A value under 16 chars is rejected at startup (loud failure beats a weak key in an automated deploy). The seed/generate/match/rotate decision is a pure `decide_master_key` function with a unit test per branch; the IO stays in `main`. **Secrets are never echoed** (covers your follow-up on this): an env-provided master key is never printed — only an auto-generated one is (so you can capture it). While here, the encryption-key load paths now log *which source* they came from (env / file / persisted path) without the key material — they were previously silent. Forward-compatible with #18: the env carries the raw key; the single equality check and the store write are each in one place for the hashing work to update. Docs: `MASTER_API_KEY` added to the env table, plus a commented `docker-compose.yml` example. Tests: all five decision branches + a DB test proving a rotate makes the new value the master and invalidates the old one. Full suite green (168), clippy and fmt clean. This unblocks pre-defining the key for automated ER deployment.
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
IsoHex/edge-router#19
No description provided.