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>
30 lines
1.1 KiB
Desktop File
30 lines
1.1 KiB
Desktop File
[Unit]
|
|
Description=left4me web application
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=left4me
|
|
Group=left4me
|
|
WorkingDirectory=/opt/left4me
|
|
Environment=HOME=/var/lib/left4me
|
|
Environment=PATH=/opt/left4me/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
EnvironmentFile=/etc/left4me/host.env
|
|
EnvironmentFile=/etc/left4me/web.env
|
|
ExecStart=/opt/left4me/.venv/bin/gunicorn --workers 1 --threads 8 --bind 0.0.0.0:8000 'l4d2web.app:create_app()'
|
|
Restart=on-failure
|
|
RestartSec=3
|
|
# NoNewPrivileges intentionally not set: the worker invokes fusermount3
|
|
# (setuid-root) and sudo to run the systemctl wrapper.
|
|
# PrivateTmp intentionally not set: it creates a private mount
|
|
# namespace, which would hide per-instance fuse-overlayfs mounts from
|
|
# 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
|
|
ReadWritePaths=/var/lib/left4me
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|