Skip to content

feat(boot_guards): refuse to start in web mode with insecure config (#441) [stacked on #475] - #476

Open
olegbrok wants to merge 1 commit into
feat/security-headers-stackedfrom
feat/boot-guards-stacked
Open

feat(boot_guards): refuse to start in web mode with insecure config (#441) [stacked on #475]#476
olegbrok wants to merge 1 commit into
feat/security-headers-stackedfrom
feat/boot-guards-stacked

Conversation

@olegbrok

Copy link
Copy Markdown
Collaborator

Summary

PR-5 of the #433 web-exposed hardening track. Loud-failure boot-time checks for the obviously-unsafe configurations an internet-exposed deployment must not run with.

Stacked on #475#474#473#472.

Closes part of #433. Refs #441.

Scope of this PR

Ships the guards that don't depend on unshipped pieces of the track. Remaining guards land alongside their owning PRs (admin user + legacy UI password → #435; multi-worker rate-limit backend → #438; audit emission for overrides → #440). Each deferral is marked with a TODO referencing the owning PR.

Guards (fatal, web mode only)

Name Triggers when
`SESSION_SECRET` `PINKY_SESSION_SECRET` unset, < 32 chars, placeholder (`changeme`/`secret`/`pinky`/`password`/`default`/`admin`/`test`/`change-me`), or trivially low-entropy (≤ 2 distinct chars)
`PUBLIC_BIND_NO_TLS` Bound to `0.0.0.0` / non-loopback host without `PINKY_BEHIND_TLS_PROXY=true`
`MISSING_TRUSTED_PROXIES` `PINKY_BEHIND_TLS_PROXY=true` with `PINKY_TRUSTED_PROXIES` empty (otherwise audit + rate-limit attribution flatten to `127.0.0.1` because #442's resolver refuses to honor XFF from an untrusted peer)
`DB_PERMISSIONS` DB file or its parent directory has loose permissions (group/other bits set). No-op on Windows

Warnings (non-fatal, all modes)

Name Triggers when
`RUNNING_AS_ROOT` Daemon process UID == 0

Override mechanism

Per-guard `PINKY_ALLOW_INSECURE_=true` env vars. No global escape hatch by design — operators that need to bypass a guard for an emergency must do so per-guard, and each override is logged loudly at startup. Once #440 lands, overrides will also emit an audit event.

Wiring

`assert_web_mode_safe(mode, host, db_path)` called from `pinky_daemon.main._run_api` after argparse + mode resolution and before `uvicorn.run`. Failure → preformatted rejection block to stderr; `sys.exit(3)`. Distinct exit code from invalid-mode (#434 uses 2) so wrapper scripts can route on it.

Tests (34 new)

`tests/test_boot_guards.py` covers each guard, the override mechanism, the warning runner, the aggregator, and the message formatter. All scenarios: trusted/lan skip, web-all-pass, web-aggregates-failures, web-overrides-let-boot-proceed.

286 existing api tests + all earlier hardening-track tests still green.

Test plan

  • `pytest tests/test_boot_guards.py -v` (34 passing)
  • `pytest tests/test_*.py` (no regression)
  • `ruff check src/pinky_daemon/boot_guards.py src/pinky_daemon/main.py tests/test_boot_guards.py` (clean)
  • Manual: `PINKY_DEPLOYMENT_MODE=web python -m pinky_daemon` → confirm rejection block + exit 3
  • Manual: same with all overrides set → boot proceeds + warnings logged

Out of scope (deferred to owning PRs)

🤖 Opened by Barsik

…441)

PR-5 of the #433 web-exposed hardening track. Loud-failure boot-time
checks for the obviously-unsafe configurations an internet-exposed
deployment must not run with. Stacked on #475#474#473#472.

Scope of this PR
================
Ships the guards that don't depend on unshipped pieces of the track.
The remaining guards land alongside their owning PRs (admin user,
legacy UI password → #435; multi-worker rate-limit backend → #438;
audit emission for overrides → #440). Each deferral is marked with a
TODO referencing the owning PR.

Guards (fatal, web mode only)
-----------------------------
* SESSION_SECRET — PINKY_SESSION_SECRET unset, < 32 chars, one of a
  small placeholder set (changeme/secret/pinky/...), or trivially
  low-entropy (≤ 2 distinct chars).
* PUBLIC_BIND_NO_TLS — bound to 0.0.0.0 / a non-loopback host without
  PINKY_BEHIND_TLS_PROXY=true to acknowledge upstream TLS termination.
* MISSING_TRUSTED_PROXIES — PINKY_BEHIND_TLS_PROXY=true with
  PINKY_TRUSTED_PROXIES empty; otherwise audit + rate-limit attribution
  silently flatten to 127.0.0.1 because #442's resolver refuses to
  honor XFF from an untrusted peer.
* DB_PERMISSIONS — DB file or its parent directory has loose
  permissions (group/other bits set). No-op on Windows.

Warnings (non-fatal, all modes)
-------------------------------
* RUNNING_AS_ROOT — daemon process UID == 0.

Override mechanism
------------------
Per-guard PINKY_ALLOW_INSECURE_<NAME>=true env vars. **No global
escape hatch by design** — operators who need to bypass a guard for
an emergency must do so per-guard, and each override is logged loudly
at startup. Once #440 lands, overrides will also emit an audit event.

Wiring
------
* assert_web_mode_safe(mode, host, db_path) called from
  pinky_daemon.__main__._run_api after argparse + mode resolution and
  before uvicorn.run.
* Failure → preformatted rejection block to stderr; sys.exit(3).
  Distinct exit code from invalid-mode (#434 uses 2) so wrapper
  scripts can route on it.

Tests (34 new)
--------------
* check_session_secret: unset / short / placeholder (parametrised) /
  long-random-passes / low-entropy / override.
* check_public_bind_requires_tls_marker: loopback variants pass /
  public bind no marker fails / public bind with marker passes /
  specific public address fails / override.
* check_trusted_proxies_when_behind_tls: no marker passes / marker
  with proxies passes / marker without proxies fails.
* check_db_permissions: nonexistent skips / secure dir + file pass /
  world-readable dir + file fail.
* run_warning_guards: empty list when nothing to warn / running as
  root warning / silenced by override.
* assert_web_mode_safe: trusted/lan skip all / web all-pass / web
  aggregates failures / overrides let boot proceed.
* format_guard_error: contains all expected sections.

Closes part of #433. Refs #441.

🤖 Authored by Barsik
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant