fix(env): fail closed on weak DECNET_ADMIN_PASSWORD

DECNET_ADMIN_PASSWORD defaulted to the literal "admin" with no guard, so
a master that never set it seeded an admin/admin account. Resolve it
lazily via __getattr__ -> _require_env (the same pattern as
DECNET_JWT_SECRET): unset or a known-bad default (admin/secret/...) is
rejected, and <12 chars is rejected outside DECNET_DEVELOPER. Only the
master web/api processes that import the DB layer resolve it; workers
never do, and the pytest short-circuit keeps the dev loop unaffected.
The module attribute stays addressable for the admin-seed monkeypatch.
This commit is contained in:
2026-05-30 17:31:00 -04:00
parent 28327a9b4e
commit fdb6507c6f
2 changed files with 78 additions and 4 deletions

View File

@@ -46,13 +46,19 @@ def _require_env(name: str) -> str:
f"Environment variable '{name}' is set to an insecure default ('{value}'). "
f"Choose a strong, unique value before starting DECNET."
)
_developer = os.environ.get("DECNET_DEVELOPER", "False").lower() == "true"
if name == "DECNET_JWT_SECRET" and len(value) < 32:
_developer = os.environ.get("DECNET_DEVELOPER", "False").lower() == "true"
if not _developer:
raise ValueError(
f"DECNET_JWT_SECRET is too short ({len(value)} bytes). "
f"Use at least 32 characters to satisfy HS256 requirements (RFC 7518 §3.2)."
)
if name == "DECNET_ADMIN_PASSWORD" and len(value) < 12:
if not _developer:
raise ValueError(
f"DECNET_ADMIN_PASSWORD is too short ({len(value)} chars). "
f"Use at least 12 characters, or set DECNET_DEVELOPER=true for local runs."
)
return value
@@ -132,7 +138,9 @@ DECNET_BATCH_MAX_WAIT_MS: int = int(os.environ.get("DECNET_BATCH_MAX_WAIT_MS", "
DECNET_WEB_HOST: str = os.environ.get("DECNET_WEB_HOST", "127.0.0.1")
DECNET_WEB_PORT: int = _port("DECNET_WEB_PORT", 8080)
DECNET_ADMIN_USER: str = os.environ.get("DECNET_ADMIN_USER", "admin")
DECNET_ADMIN_PASSWORD: str = os.environ.get("DECNET_ADMIN_PASSWORD", "admin")
# DECNET_ADMIN_PASSWORD is resolved lazily via __getattr__ (like DECNET_JWT_SECRET)
# so it is validated only on the master processes that seed the admin user, and
# never silently defaults to "admin". See _require_env + __getattr__ below.
DECNET_DEVELOPER: bool = os.environ.get("DECNET_DEVELOPER", "False").lower() == "true"
# Host role — seeded by /etc/decnet/decnet.ini or exported directly.
@@ -277,6 +285,6 @@ def validate_public_binding() -> None:
def __getattr__(name: str) -> str:
"""Lazy resolution for secrets only the master web/api process needs."""
if name == "DECNET_JWT_SECRET":
return _require_env("DECNET_JWT_SECRET")
if name in ("DECNET_JWT_SECRET", "DECNET_ADMIN_PASSWORD"):
return _require_env(name)
raise AttributeError(f"module 'decnet.env' has no attribute {name!r}")