fix(settings): fail fast when an nsec is set but ROUTSTR_SECRET_KEY is missing

Encryption of the Nostr identity at rest is mandatory. When a legacy nsec is
present (env or blob) but no ROUTSTR_SECRET_KEY is set, bootstrap previously
fell into vault.encrypt and surfaced its generic "key not set" error. Raise an
explicit, nsec-contextual error first so the boot failure is intentional and
actionable — it names the missing key and prints the generation command —
rather than relying on vault throwing incidentally. No secret is dropped: the
node refuses to start until the operator sets the key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jeroen Ubbink
2026-07-23 10:51:20 +02:00
co-authored by Claude Opus 4.8
parent afcb3f7cda
commit 56a67c0a86
2 changed files with 32 additions and 0 deletions
+11
View File
@@ -498,6 +498,17 @@ async def bootstrap_secrets(db_session: AsyncSession) -> None:
else:
legacy_nsec = _legacy_plaintext(raw_blob, "NSEC", "nsec")
if legacy_nsec:
# The node has a Nostr identity to protect. Encryption at rest is
# mandatory: without a key we fail fast (clear, actionable boot
# error) rather than silently persisting the nsec in plaintext.
if not os.environ.get("ROUTSTR_SECRET_KEY"):
raise RuntimeError(
"An nsec is configured but ROUTSTR_SECRET_KEY is not set. "
"The key is required to encrypt the Nostr identity at rest. "
"Generate one and set it in the environment:\n"
' python -c "from cryptography.fernet import Fernet; '
'print(Fernet.generate_key().decode())"'
)
secret.encrypted_nsec = vault.encrypt(legacy_nsec)
settings.nsec = legacy_nsec
changed = True
@@ -163,6 +163,27 @@ async def test_fail_fast_when_nsec_encrypted_with_different_key(
await bootstrap_secrets(integration_session)
# --- encryption is mandatory: a node with an nsec needs ROUTSTR_SECRET_KEY -----
@pytest.mark.asyncio
async def test_legacy_nsec_without_secret_key_fails_fast(
clean_secret_env: None,
integration_session: AsyncSession,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# A node with a Nostr identity must not boot without a key to encrypt it:
# encryption at rest is mandatory, not opt-in. The failure names the missing
# key and hands over the generation command rather than crashing opaquely or
# silently persisting the nsec in plaintext. No env/blob copy is dropped — the
# node refuses until the operator sets the key.
monkeypatch.delenv("ROUTSTR_SECRET_KEY", raising=False)
monkeypatch.setenv("NSEC", NSEC_HEX)
with pytest.raises(RuntimeError, match="ROUTSTR_SECRET_KEY"):
await bootstrap_secrets(integration_session)
# --- boot ordering: rescue legacy blob secrets before they are stripped ----