Approved-but-not-executed plan to collapse the two-user model (left4me + l4d2-sandbox) into one. The build-time-idmap that translates sandbox writes back to left4me uid becomes a no-op when source uid == target uid, so it's removed along with ~30 lines of helper plumbing. Hardening already covers the same-uid attack surface the sandbox uid was defending against, so collapsing makes the architecture consistent with the web/server hardening-only decision. Plan: docs/superpowers/plans/2026-05-15-uid-collapse.md Handoff: docs/superpowers/specs/2026-05-15-session-handoff.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.5 KiB
Session handoff — next: execute uid-collapse plan
The hardening refactor landed and was verified on left4.me earlier
in this session day. A follow-up question surfaced — the
two-user model (left4me + l4d2-sandbox) is inconsistent now
that systemd hardening covers the same-uid attack surface. The
asymmetry was hashed out and Option C (collapse to one user)
chosen. A plan was written but not executed. The next session
picks it up.
What just landed (committed + pushed earlier today)
The hardening refactor — full directive composition deployed to
left4.me. server@1 went 7.5 → 1.3 systemd-analyze; web 8.7 → 4.1;
all Test 8 attack vectors blocked. See the prior session-handoff
content in this file's git history (git log --oneline -- this-file)
and the close-out commits.
What's next: execute 2026-05-15-uid-collapse.md
Plan: docs/superpowers/plans/2026-05-15-uid-collapse.md. Approved
in plan-mode this session; not executed.
Scope (10 steps; see plan for detail):
- Strip the idmap block from
scripts/libexec/left4me-script-sandbox(~30 lines deleted), changeUser=l4d2-sandbox→User=left4me,BindPaths="${STAGING}:/overlay"→BindPaths="${OVERLAY_DIR}:/overlay". Keep thensenterself-wrap (it's about namespace escape, not uid — unaffected). - Update
scripts/tests/test_script_sandbox.py— assertion changes- delete the
test_script_sandbox_uses_idmap_stagingtest.
- delete the
- Update two inline comments referencing
l4d2-sandbox. 4-6. Doc updates: mark2026-05-15-build-time-idmap.mdand2026-05-14-overlay-idmap.mdsuperseded; revise the user-uid-split superseded header to say "1 user" instead of "2"; one-line notes in the hardening specs. - Remove
l4d2-sandboxfrom~/Projekte/ckn-bw/bundles/left4me/items.py(users + groups dicts). Tighten/var/lib/left4memode from0711→0755. - On-host pre-flight:
ssh left4.me+sudo find -uid 981, chown any stragglers toleft4meBEFORE applying. ckn-bw won't remove a user whose files (or processes) are still on disk gracefully. - Push both repos;
bw apply ovh.left4me. - Verify:
getent passwd l4d2-sandboxempty, no uid-981 files, sandbox build runs as left4me end-to-end via the web UI.
Rollback path documented in the plan (git revert + bw apply recreates the user).
Why we're doing this
The two-user setup was the inconsistent middle ground:
- Server + web run as
left4mebecause hardening covers the threat — uid split would be 1-2 days of cross-repo migration for marginal kernel-enforcement benefit. - Sandbox runs as
l4d2-sandboxfor historical reasons — the build-time-idmap design baked it in.
The hardening composition on the sandbox unit (which the
script-sandbox helper applies via systemd-run -p ...) already
gives the same protection profile as the gameserver unit. The
separate uid is defense-in-depth only.
Picking one principle:
- C (collapse to one): cheap, deletes ~30 lines of helper code, removes the build-time-idmap concern entirely. Architecture simpler. Consistent with the web/server hardening-only decision.
- A (status quo): inconsistent. Documented but not principled.
- B (split fully): 1-2 days of work; we already rejected this for server/web.
Operator picked C.
Decision-relevant context already on the host
- After the hardening refactor + bw apply earlier,
left4me-server@*andleft4me-webare running with the full hardening profile.kernel.yama.ptrace_scope=2is set system-wide via the bundle. - The sandbox unit is currently inactive (it's transient — only
exists during a build). Per the build-time-idmap plan, the
staging path lives at
/var/lib/left4me/tmp/sandbox-idmap-<id>during a build. - ckn-bw's
usersbundle handles the removal mechanically; no custom dance needed beyond the pre-flight chown.
Open questions to clarify with the operator before/during execution
- Whether to expand the pre-flight
find -uid 981from/var/lib/left4me+/opt/left4meto all of/for paranoia. Probably not needed; flag for the implementer's judgement. - Whether to combine left4me + ckn-bw into a single PR-equivalent
cross-repo commit pair, or push left4me first then ckn-bw. Plan
assumes both pushed before
bw apply.
What's NOT next
build-overlay-unitrefactor (docs/superpowers/specs/2026-05-15-build-overlay-unit-design.md). Sequenced after this; will inheritUser=left4mecleanly.- Broader configmgmt responsibility reshape (drop-ins owned by left4me, ckn-bw as thin file-shipper). Deliberately deferred.
- Stale RCON port app bug flagged in the earlier executor's handoff. Separate scope.
- Renaming
left4meto anything else. Cosmetic.
Pointers
- The plan to execute:
docs/superpowers/plans/2026-05-15-uid-collapse.md - Hardening refactor that just landed:
docs/superpowers/plans/2026-05-15-hardening-refactor.md - Hardening threat model + defenses survey + test plan (commit
461b8d0recorded the test results inline):docs/superpowers/specs/2026-05-15-hardening-{threat-model,defenses-survey,test-plan}.md - Build-time-idmap plan (about to be marked superseded):
docs/superpowers/plans/2026-05-15-build-time-idmap.md - uid-split spec (also affected — answer revises from "stay at 2"
to "collapse to 1"):
docs/superpowers/specs/2026-05-15-user-uid-split-design.md - Live source for unit emission:
~/Projekte/ckn-bw/bundles/left4me/metadata.py - Live source for users/groups:
~/Projekte/ckn-bw/bundles/left4me/items.py