Skip to content

fix: keep the web token out of the systemd journal, and verify the unit for real - #13

Merged
404SecNotFound merged 1 commit into
mainfrom
fix/systemd-unit-verified
Jul 29, 2026
Merged

fix: keep the web token out of the systemd journal, and verify the unit for real#13
404SecNotFound merged 1 commit into
mainfrom
fix/systemd-unit-verified

Conversation

@404SecNotFound

Copy link
Copy Markdown
Owner

scripts/replicant-web.service shipped in #10 marked [Unverified] — no systemd had ever started it. Running it in a container found a defect that reading it could not.

The defect

The startup banner printed the URL with ?token= in it. On a terminal that is the point. Under systemd, stdout is the journal:

replicant[175]:   URL : http://127.0.0.1:9787/?token=BuvY6dvcTqyDs1-CSdPueC8Jtv...

So a codebase that had just gone to the trouble of writing the token 0600 in a 0700 directory was also writing it in cleartext, on every start, into a file readable by root and the entire systemd-journal group. The 0600 was not wrong. It was bypassed by a different sink.

The banner now reveals the token only when sys.stdout.isatty(). Otherwise it prints the URL without it, names the file to read it from, and drops the stop: Ctrl-C hint since nobody is at a keyboard. Conditional, not global — suppressing it everywhere would break the interactive path that legitimately needs a clickable URL.

Verification

Debian 12, systemd 252, PID 1 in a container. All 11 assertions pass:

systemd-analyze verify silent
unit state active
process owner the non-root service user, not root
/api/health 200
/api/catalog unauthenticated 401
token file 0600 in a 0700 dir, despite ProtectHome=read-only
token in journal absent (regression guard for the fix above)
terminal tab disabled on the unit's 0.0.0.0 bind
Restart=on-failure recovers from SIGKILL and serves again
token across restart unchanged, so a bookmarked URL keeps working

The ProtectHome row is the one worth pausing on: it caught a defect at authoring time in #10 (the unit originally kept config in the service user's home, which that directive would have made read-only) and now has a test proving the fix holds.

Why a CI job and not a one-off

systemd-analyze verify was silent on the leaking version. It parses the unit; it cannot tell you what the service writes once it runs. So the new systemd-unit job boots systemd rather than parsing, and runs scripts/verify-systemd-unit.sh — the same script, in the repo, reproducible locally with the three commands the job documents.

This also closes the pattern from #12: the v0.2.0 screenshot method was done ad hoc and lost, so it had to be reinvented. This verification is preserved and automated instead.

Two harness bugs, fixed before the result was believed

Worth recording because both would have produced a confident wrong answer:

  • The restart check first asserted "MainPID changed". systemd sets that the moment it forks, and RestartSec=5s puts the socket several seconds behind it — so the check raced and reported the unit broken when it was merely still starting. It now waits for the condition that actually matters: serving again.
  • It used ps, which a minimal Debian image does not ship. That failed as "runs as ``" rather than as a missing tool. Now reads /proc/PID owner.

Verification of this PR

519 Python tests (516 before, 3 new for the banner), 64 frontend, black/ruff/mypy clean, shellcheck clean under the pinned v0.11.0 image CI uses. The container run was repeated after the fix with a rotated journal, confirming the token is absent from a clean start.

The [Unverified] marker on the unit is cleared in the CHANGELOG and the README.

…it for real

scripts/replicant-web.service had never been started by a systemd. Running it in
a container (Debian 12, systemd 252 as PID 1) found a defect that reading it
could not.

The startup banner printed the URL with ?token= in it. Interactively that is the
point. Under systemd, stdout IS the journal, so every start wrote the token in
cleartext into a file readable by root and the systemd-journal group. The code
had just gone to the trouble of writing that token 0600 in a 0700 directory; the
protection was not wrong, it was bypassed by a different sink.

The banner now reveals the token only when sys.stdout.isatty(). Otherwise it
prints the URL without it, names the file to read it from, and drops the
"stop: Ctrl-C" hint, since nobody is at a keyboard. Conditional rather than
global, because the interactive path legitimately needs the clickable URL.

Verification, all 11 assertions passing against real systemd:
- systemd-analyze verify silent; unit active
- runs as the non-root service user
- serves, and refuses an unauthenticated /api/catalog with 401
- token written 0600 in a 0700 dir despite ProtectHome=read-only and
  ProtectSystem=full
- token absent from the journal (the regression guard for the fix above)
- terminal tab disabled on the unit's 0.0.0.0 bind
- Restart=on-failure recovers from SIGKILL and serves again
- the token survives that restart, so a bookmarked URL keeps working

scripts/verify-systemd-unit.sh preserves those assertions and CI runs them on
every push (job: systemd-unit). systemd-analyze verify alone would have passed
the leaking version, so the job boots systemd rather than parsing the unit.

Two harness bugs worth noting, both fixed before the result was believed: the
first version asserted "MainPID changed" for the restart check, which raced
RestartSec=5s and reported the unit broken while it was still starting, and it
used ps, which a minimal Debian image does not have.

The [Unverified] marker on the unit is cleared. 519 python tests, 64 frontend.
@404SecNotFound
404SecNotFound merged commit dd3f76c into main Jul 29, 2026
9 checks passed
@404SecNotFound
404SecNotFound deleted the fix/systemd-unit-verified branch July 29, 2026 06:40
404SecNotFound added a commit that referenced this pull request Sep 1, 2026
…mark PAN-OS/CP (#90)

Roadmap 2026-09 Now-items 1, 2, 12, 13 (decisions + cheap positioning
edits, no code):

- #1 Scope every external claim to generator-verified / delivery-unverified
  until the first observed rule fire. New README maturity note; the lab test
  is recorded as a hard launch gate in CLAUDE.md and the roadmap section.
- #2 Resolve the identity fork on the record: OSS detection-as-code for
  detection engineers, CLI-first, web optional (CLAUDE.md + README).
- #12 Name the flightsim wedge: log-strings-only, single fail-closed egress,
  no real infrastructure. Added to the README pitch and the prior-art matrix.
- #13 Reframe PAN-OS and Check Point as beta / community-verify ask; lead
  with 'FortiGate, verified' as the trust anchor (badges, intro, roadmap).
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