From 37309ba399b9e58a3ac53706d10c992aaba8133e Mon Sep 17 00:00:00 2001 From: mwiegand Date: Fri, 15 May 2026 14:58:46 +0200 Subject: [PATCH] spec(hardening-test-plan): fix four bugs surfaced by executor Four corrections noted by the test plan's executor in commit 461b8d0: - PID-lookup race: pgrep+head can pick the wrong instance. Replace with systemctl show -p MainPID --value left4me-server@N.service. - gdb-from-host ptrace check: nsenter into only the mount namespace with root caps bypasses the SECCOMP filter, so the test is a false positive. Replace with systemd-run-with-same-directives probe, or syscall-filter inspection. - D5 pgrep pattern: 'srcds_linux.*\@2' doesn't match because @N is in the unit name, not argv. Use systemctl show -p MainPID. - scmp_sys_resolver is in the seccomp package on Debian 13, not libseccomp-dev. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../specs/2026-05-15-hardening-test-plan.md | 109 +++++++++++++++--- 1 file changed, 93 insertions(+), 16 deletions(-) diff --git a/docs/superpowers/specs/2026-05-15-hardening-test-plan.md b/docs/superpowers/specs/2026-05-15-hardening-test-plan.md index 9898e62..c3d81a5 100644 --- a/docs/superpowers/specs/2026-05-15-hardening-test-plan.md +++ b/docs/superpowers/specs/2026-05-15-hardening-test-plan.md @@ -132,7 +132,7 @@ pgrep -af srcds_linux # Expect: at least one PID matching left4dead2 # 4. From inside the unit's namespace: process appears as configured uid -PID=$(pgrep -f 'srcds_linux.*left4dead2' | head -1) +PID=$(systemctl show -p MainPID --value left4me-server@1.service) sudo cat /proc/$PID/status | grep -E '^Uid|^Gid' # Expect: uid 980 (left4me) — outside the namespace, the kernel reports # the unit's User=. Inside the namespace it's also 980 (identity map). @@ -194,7 +194,7 @@ sudo systemctl restart left4me-server@1 sudo systemctl is-active left4me-server@1 # 2. From inside the unit's namespace: invisible files -PID=$(pgrep -f 'srcds_linux.*left4dead2' | head -1) +PID=$(systemctl show -p MainPID --value left4me-server@1.service) sudo nsenter --target $PID --mount -- ls -la /var/lib/left4me/left4me.db 2>&1 # Expect: No such file or directory @@ -328,9 +328,28 @@ sudo journalctl -u left4me-server@1 -kf # 4. Verify ptrace is blocked GUNICORN_PID=$(pgrep -f 'gunicorn.*l4d2web' | head -1) -PID=$(pgrep -f 'srcds_linux.*left4dead2' | head -1) -sudo nsenter --target $PID --mount -- /usr/bin/gdb --batch -p $GUNICORN_PID 2>&1 | tail -5 -# Expect: ptrace: Operation not permitted (or seccomp denial) +PID=$(systemctl show -p MainPID --value left4me-server@1.service) +# NOTE: A naive `sudo nsenter --target $PID --mount -- gdb -p $TARGET` +# runs gdb as root with full caps in only the mount namespace; the +# unit's SECCOMP filter doesn't apply, so the result is not meaningful. +# Use one of these instead: +# +# Option A: probe inside the same hardening profile. +sudo systemd-run --pty --uid=left4me --gid=left4me \ + -p NoNewPrivileges=true \ + -p PrivateUsers=true \ + -p CapabilityBoundingSet= \ + -p AmbientCapabilities= \ + -p SystemCallArchitectures='native x86' \ + -p SystemCallFilter='@system-service' \ + -p SystemCallFilter='~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete @privileged' \ + -- /usr/bin/gdb --batch -p $GUNICORN_PID 2>&1 | tail -3 +# Expect: ptrace: Operation not permitted (or seccomp-related kill) +# +# Option B: inspect the compiled SystemCallFilter directly. +sudo systemd-analyze syscall-filter left4me-server@1.service 2>&1 \ + | grep -E '^(ptrace|process_vm)' || echo "blocked (not in allow list)" +# Expect: "blocked (not in allow list)" ``` **Pass criteria:** unit active for ≥10 min, no SECCOMP kills, plugins @@ -338,7 +357,7 @@ load, ptrace blocked. **Failure handling:** if SECCOMP kills appear: - Identify the syscall from the audit line (`syscall= compat=0`), - resolve via `scmp_sys_resolver -a $(uname -m) ` (libseccomp-dev). + resolve via `scmp_sys_resolver -a $(uname -m) ` (`seccomp` package on Debian 13). - Relax the filter: remove the offending group from the deny list, OR switch from kill (default) to log (`SystemCallErrorNumber=EPERM`) for that group. @@ -374,7 +393,7 @@ sudo systemctl restart left4me-server@1 sudo systemctl is-active left4me-server@1 # 2. /proc visibility narrowed -PID=$(pgrep -f 'srcds_linux.*left4dead2' | head -1) +PID=$(systemctl show -p MainPID --value left4me-server@1.service) sudo nsenter --target $PID --mount --pid -- ls /proc | head -20 # Expect: only the unit's own PIDs (srcds_run, srcds_linux, # child threads). NOT gunicorn or other PIDs. @@ -541,7 +560,7 @@ sudo systemctl status left4me-server@1 --no-pager | head -10 # Active (running), recent green # S2: srcds is in-game -PID=$(pgrep -f 'srcds_linux.*left4dead2' | head -1) +PID=$(systemctl show -p MainPID --value left4me-server@1.service) [ -n "$PID" ] && echo "OK: srcds PID $PID" || echo "FAIL" # S3: from outside, RCON responds @@ -590,7 +609,7 @@ work end-to-end. **Verify:** ```bash -PID=$(pgrep -f 'srcds_linux.*left4dead2' | head -1) +PID=$(systemctl show -p MainPID --value left4me-server@1.service) GUNICORN_PID=$(pgrep -f 'gunicorn.*l4d2web' | head -1) # D1.a — srcds cannot read DB @@ -606,8 +625,27 @@ sudo nsenter --target $PID --mount -- ls /opt 2>&1 | head -5 # Expect: empty listing or No such file or directory # D2.a — srcds cannot ptrace gunicorn (syscall filter) -sudo nsenter --target $PID --mount -- /usr/bin/gdb --batch -p $GUNICORN_PID 2>&1 | tail -3 -# Expect: Operation not permitted +# NOTE: A naive `sudo nsenter --target $PID --mount -- gdb -p $TARGET` +# runs gdb as root with full caps in only the mount namespace; the +# unit's SECCOMP filter doesn't apply, so the result is not meaningful. +# Use one of these instead: +# +# Option A: probe inside the same hardening profile. +sudo systemd-run --pty --uid=left4me --gid=left4me \ + -p NoNewPrivileges=true \ + -p PrivateUsers=true \ + -p CapabilityBoundingSet= \ + -p AmbientCapabilities= \ + -p SystemCallArchitectures='native x86' \ + -p SystemCallFilter='@system-service' \ + -p SystemCallFilter='~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete @privileged' \ + -- /usr/bin/gdb --batch -p $GUNICORN_PID 2>&1 | tail -3 +# Expect: ptrace: Operation not permitted (or seccomp-related kill) +# +# Option B: inspect the compiled SystemCallFilter directly. +sudo systemd-analyze syscall-filter left4me-server@1.service 2>&1 \ + | grep -E '^(ptrace|process_vm)' || echo "blocked (not in allow list)" +# Expect: "blocked (not in allow list)" # D2.b — srcds cannot read /proc//environ sudo nsenter --target $PID --mount -- cat /proc/$GUNICORN_PID/environ 2>&1 | head -1 @@ -622,9 +660,28 @@ sudo nsenter --target $PID --mount -- sudo -n /usr/local/libexec/left4me/left4me # Expect: a sudo error about no new privileges, or operation not permitted # D5 — server@1 cannot ptrace server@2's srcds -PID2=$(pgrep -f 'srcds_linux.*\@2' | head -1) -[ -n "$PID2" ] && sudo nsenter --target $PID --mount -- /usr/bin/gdb --batch -p $PID2 2>&1 | tail -3 -# Expect: Operation not permitted (cross-instance userns OR syscall filter) +PID2=$(systemctl show -p MainPID --value left4me-server@2.service) +# NOTE: A naive `sudo nsenter --target $PID --mount -- gdb -p $TARGET` +# runs gdb as root with full caps in only the mount namespace; the +# unit's SECCOMP filter doesn't apply, so the result is not meaningful. +# Use one of these instead: +# +# Option A: probe inside the same hardening profile. +[ -n "$PID2" ] && sudo systemd-run --pty --uid=left4me --gid=left4me \ + -p NoNewPrivileges=true \ + -p PrivateUsers=true \ + -p CapabilityBoundingSet= \ + -p AmbientCapabilities= \ + -p SystemCallArchitectures='native x86' \ + -p SystemCallFilter='@system-service' \ + -p SystemCallFilter='~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete @privileged' \ + -- /usr/bin/gdb --batch -p $PID2 2>&1 | tail -3 +# Expect: ptrace: Operation not permitted (or seccomp-related kill) +# +# Option B: inspect the compiled SystemCallFilter directly. +sudo systemd-analyze syscall-filter left4me-server@1.service 2>&1 \ + | grep -E '^(ptrace|process_vm)' || echo "blocked (not in allow list)" +# Expect: "blocked (not in allow list)" # Bonus — confirm PrivateUsers is in effect sudo readlink /proc/$PID/ns/user @@ -744,8 +801,26 @@ WEB_PID=$(pgrep -f 'gunicorn.*l4d2web' | head -1) sudo -u left4me /usr/bin/gdb --batch -p $PID 2>&1 | tail -3 # (might still succeed if the operator runs as root — what matters is # from inside the web unit's namespace) -sudo nsenter --target $WEB_PID --mount -- /usr/bin/gdb --batch -p $PID 2>&1 | tail -3 -# Expect: Operation not permitted (SystemCallFilter blocks ptrace) +# NOTE: A naive `sudo nsenter --target $WEB_PID --mount -- gdb -p $TARGET` +# runs gdb as root with full caps in only the mount namespace; the +# unit's SECCOMP filter doesn't apply, so the result is not meaningful. +# Use one of these instead: +# +# Option A: probe inside the same hardening profile. +sudo systemd-run --pty --uid=left4me --gid=left4me \ + -p NoNewPrivileges=true \ + -p CapabilityBoundingSet= \ + -p AmbientCapabilities= \ + -p SystemCallArchitectures='native x86' \ + -p SystemCallFilter='@system-service' \ + -p SystemCallFilter='~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete' \ + -- /usr/bin/gdb --batch -p $PID 2>&1 | tail -3 +# Expect: ptrace: Operation not permitted (or seccomp-related kill) +# +# Option B: inspect the compiled SystemCallFilter directly. +sudo systemd-analyze syscall-filter left4me-web.service 2>&1 \ + | grep -E '^(ptrace|process_vm)' || echo "blocked (not in allow list)" +# Expect: "blocked (not in allow list)" ``` **Pass criteria:** all of above. @@ -1065,6 +1140,8 @@ When all tests complete: - **Test 3 baseline**: spec says ptrace_scope default is `1`; observed `0` on Debian 13. Update the "Expect" line. +**Resolved 2026-05-15 via the hardening-refactor plan.** The four bugs are fixed in-place in the test commands above. See `docs/superpowers/plans/2026-05-15-hardening-refactor.md` Task 8. + ## Pointers - Threat model: `docs/superpowers/specs/2026-05-15-hardening-threat-model.md`