fix(deploy): drop PrivateTmp on web service so fuse mounts propagate

PrivateTmp=true gives the unit a private mount namespace. The worker's
fuse-overlayfs mount lives only inside that namespace, so the host
cannot see it and the gameserver unit (started via systemctl, with its
own namespace inherited from the host) also cannot see it. The
gameserver unit then fails CHDIR on
/var/lib/left4me/runtime/<name>/merged/left4dead2.

The mount must land in the host namespace so the gameserver unit
inherits it at unshare time. Remaining hardening: dedicated user,
ProtectSystem=full, ReadWritePaths, sudoers allowlist limited to two
helper scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-07 01:57:43 +02:00
parent 56b9523d88
commit 593611e194
No known key found for this signature in database

View file

@ -16,10 +16,13 @@ ExecStart=/opt/left4me/.venv/bin/gunicorn --workers 1 --threads 8 --bind 0.0.0.0
Restart=on-failure Restart=on-failure
RestartSec=3 RestartSec=3
# NoNewPrivileges intentionally not set: the worker invokes fusermount3 # NoNewPrivileges intentionally not set: the worker invokes fusermount3
# (setuid-root) to mount FUSE overlays and sudo to run the systemctl # (setuid-root) and sudo to run the systemctl wrapper.
# wrapper. NoNewPrivileges blocks both. Hardening is still provided by # PrivateTmp intentionally not set: it creates a private mount
# dedicated user, PrivateTmp, ProtectSystem=full, and narrow sudoers. # namespace, which would hide per-instance fuse-overlayfs mounts from
PrivateTmp=true # the host and the gameserver units. The mount must land in the host
# namespace so the systemd-managed gameserver service inherits it at
# unshare time. Remaining hardening: dedicated user, ProtectSystem,
# ReadWritePaths, narrow sudoers allowlist.
ProtectSystem=full ProtectSystem=full
ReadWritePaths=/var/lib/left4me ReadWritePaths=/var/lib/left4me