Sync deployment references for the runtime state relocation
shipped via ckn-bw (commit 6fae2fd). /opt/left4me/ is now a
root-owned deploy-artifact root (just src/); .venv and steamcmd
live at /var/lib/left4me/{.venv,steam}.
Touches:
- deploy/files/.../left4me-web.service: PATH + ExecStart
- deploy/files/.../left4me-workshop-refresh.service: WorkingDirectory
(was /opt/left4me, now /opt/left4me/src to match the web unit),
PATH, ExecStart
- scripts/sbin/left4me wrapper: flask path
- deploy/tests/test_example_units.py: PATH + ExecStart assertions
for the web unit; also fix a pre-existing broken assertion that
read "Environment=PATH=..." (the unit has Environment=HOME=...
PATH=... on one line, so "Environment=PATH=" was never present)
- now reads just "PATH=..."
- deploy/README.md: paths
- l4d2host/tests/test_cli.py: LEFT4ME_STEAMCMD fixture path
Design + as-shipped record:
docs/superpowers/specs/2026-05-15-runtime-state-relocation-design.md.
The original (narrower) prereq spec at
docs/superpowers/specs/2026-05-15-handoff-noneditable-install.md
is marked superseded with a pointer to what shipped + why the
scope grew (setuptools writes egg-info to source during PEP 517
build prep).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
3.2 KiB
Desktop File
85 lines
3.2 KiB
Desktop File
# left4me web application — system unit.
|
|
#
|
|
# This is the REFERENCE COPY of the deployed unit. The live source is
|
|
# the systemd/units reactor at ~/Projekte/ckn-bw/bundles/left4me/metadata.py
|
|
# (look for 'left4me-web.service'). Hardening directives live in
|
|
# the HARDENING_WEB constant near the top of the same file.
|
|
# This file is hand-synced; edit both together.
|
|
#
|
|
# Several directives that the gameserver uses are intentionally absent
|
|
# from this unit:
|
|
# NoNewPrivileges — blocks sudo's setuid escalation
|
|
# PrivateUsers — breaks sudo's host-root mapping
|
|
# RestrictSUIDSGID — blocks setuid()/setgid()
|
|
# CapabilityBoundingSet= — empty value would deny sudo's caps
|
|
# ~@privileged in SystemCallFilter — blocks sudo's setuid syscall
|
|
# The web app invokes privileged helpers (left4me-systemctl,
|
|
# left4me-overlay, left4me-script-sandbox) via sudo, so these
|
|
# directives can't be applied here. A future refactor replacing sudo
|
|
# with systemctl-managed transient units would unlock them.
|
|
#
|
|
# Threat model + defenses + tests: see docs/superpowers/specs/2026-05-15-hardening-*
|
|
|
|
[Unit]
|
|
Description=left4me web application
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=left4me
|
|
Group=left4me
|
|
WorkingDirectory=/opt/left4me/src
|
|
Environment=HOME=/var/lib/left4me PATH=/var/lib/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
|
|
# Placeholder values for --workers / --threads. Live emission interpolates
|
|
# from metadata.get('left4me/gunicorn_workers') and gunicorn_threads.
|
|
ExecStart=/var/lib/left4me/.venv/bin/gunicorn --workers 4 --threads 4 --bind 127.0.0.1:8000 'l4d2web.app:create_app()'
|
|
Restart=on-failure
|
|
RestartSec=3
|
|
|
|
# Web writes broadly under /var/lib/left4me (DB, instance configs,
|
|
# overlays, runtime). Kept inline because it's web-specific
|
|
# (server@ uses BindPaths to bind only its instance dir).
|
|
ReadWritePaths=/var/lib/left4me
|
|
|
|
# === Filesystem ===
|
|
ProtectSystem=strict # tightened from prior 'full'; via HARDENING_COMMON
|
|
ProtectHome=true
|
|
PrivateTmp=true
|
|
|
|
# === /proc + kernel ===
|
|
# Note: ProcSubset=pid is intentionally NOT set on the web unit.
|
|
# It hides /proc/sys/kernel/random/boot_id which journalctl reads at
|
|
# startup, and the web invokes `sudo -n left4me-journalctl` to stream
|
|
# live server logs into the UI. The server unit can keep ProcSubset=pid
|
|
# because srcds doesn't shell out to journalctl.
|
|
ProtectProc=invisible # foreign-uid /proc hidden (defense: D4)
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
ProtectKernelLogs=true
|
|
ProtectClock=true
|
|
ProtectControlGroups=true
|
|
ProtectHostname=true
|
|
LockPersonality=true
|
|
|
|
# === Syscall filter (sudo-compatible — note absence of ~@privileged) ===
|
|
SystemCallArchitectures=native
|
|
SystemCallFilter=@system-service
|
|
SystemCallFilter=~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete
|
|
# ~@debug blocks ptrace + process_vm_readv/writev (D4).
|
|
# ~@privileged intentionally omitted — sudo needs setuid().
|
|
|
|
# === Network ===
|
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
|
|
|
# === Misc hygiene ===
|
|
RestrictNamespaces=true
|
|
RestrictRealtime=true
|
|
RemoveIPC=true
|
|
KeyringMode=private
|
|
UMask=0027
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|