# 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=/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 # Placeholder values for --workers / --threads. Live emission interpolates # from metadata.get('left4me/gunicorn_workers') and gunicorn_threads. ExecStart=/opt/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 === ProtectProc=invisible # foreign-uid /proc hidden (defense: D4) ProcSubset=pid 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