Security review · APPLY5, runs started from a terminal in 0.0.1¶
security-reviewer on autopilot/apply-observed-runs at c66a29b, 2026-10-28: the design in architecture §13.8, dev-env §15.3 and P1 Tasks 25 and 26. There is no product code yet, so every finding becomes one line of the new §15.3 req. 12 and named tests in P1. Format: README.md.
SECURITY step=APPLY5 review=block b/M/m=1/4/4 applied=9/9 rounds=1
Trust boundaries, now in §15.3: (i) to (iii) as in APPLY4, plus (iv) other users who can write a launch directory, such as a group-writable project directory, for runs started from a terminal.
| Id | Severity | Finding | Status |
|---|---|---|---|
| SEC-141 | blocker | the bridge wrote into <launch dir>/.spectio without checking it; another user who can write the launch directory could plant a symlink and make the bridge write elsewhere as the user |
applied: req. 12 Bridge, launch directory (O_DIRECTORY\|O_NOFOLLOW, fstat, fd-relative O_NOFOLLOW creates, spool 0600, otherwise nothing written); Java SecureDirectoryStream and EMBL NFSv4 inherited ACLs [to be validated] (inbox #64 note); Task 25 test_bridge_refuses_foreign_spectio_dir and its _snakemake twin |
| SEC-142 | major | the server read a terminal run's .spectio/ and engine logs by path; a swap after the check, a symlink or a FIFO could leak or hang |
applied: req. 12 Server, reads (one checked descriptor; logs O_NOFOLLOW\|O_NONBLOCK, regular file owned by the user, last 64 KiB, else "log not readable"); Task 25 test_spectio_dir_swapped_after_check_not_read, test_terminal_log_symlink_fifo_foreign_refused |
| SEC-143 | major | two terminal runs in one launch directory shared spool files | applied: spool files <run ID>-<producer>-<nnnn>.jsonl (architecture §13.8), only the pointer's run ID read, other envelopes dropped and counted (req. 12); Task 25 test_two_terminal_runs_same_launch_dir_kept_apart |
| SEC-144 | major | the head check used the pointer's host and PID on any node; a terminal run could be marked lost, or reach the launcher | applied: checked only when the boot ID equals the server's, else "head on cli-observed, boot reconciliation included (req. 12 No signal, architecture §13.8 Its end); Task 25 test_terminal_run_other_boot_id_never_head_lost, test_runner_never_signals_cli_observed |
| SEC-145 | major | nf-spectio, snakemake-logger-plugin-spectio, spectio and spectio-open are unclaimed (registry and PyPI 404 on 2026-10-28) while the repository is public, and the install lines were unpinned |
applied: req. 12 Names and pins (Thomas claims the four names before the guide merges; exact pins; bundled copies until then); inbox #66 rewritten, blocking the guide only; Task 25 test_guide_pins_bridge_versions |
| SEC-146 | minor | a loose ~/.spectio or observed/ would accept pointers from others; the pointer's keys were open-ended |
applied: req. 12 Bridge, pointer and Server, scan (real directories owned by the user, 0700; exact keys; a loose observed/ skipped with a warning, never blocks the start); Task 25 test_loose_spectio_home_no_pointer, test_pointer_keys_exact, test_loose_observed_dir_not_scanned |
| SEC-147 | minor | a pointer could alias a driven run or point into the runs root | applied: the runs root wins; such pointers skipped and counted (req. 12 Server, scan); Task 25 test_pointer_cannot_alias_driven_run |
| SEC-148 | minor | every tick listed observed/, and ended pointers piled up |
applied: list at boot, then only when the mtime changes; ended pointers move to observed/ended/, read only by a rebuild (req. 12, dev-env §15.3 Where data lives); Task 25 test_scan_skips_unchanged_dir, test_ended_pointer_moved |
| SEC-149 | minor | the release dry run could pick up a user's Gradle cache or keys, and the Gradle plugin and wrapper were unpinned | applied: req. 12 Release (fresh GRADLE_USER_HOME, pinned plugin and distributionSha256Sum, release from CI or a laptop, never the cluster; release steps owned by dev-env §12); Task 26 no-secrets-in-artefacts check in tests/packaging/test_bridges_release.sh |