Mirrors ckn-bw fix: ProcSubset=pid hides /proc/sys/kernel/random/boot_id, which journalctl needs at startup; web unit invokes journalctl for live log streaming. 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=/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 ===
|
|
# 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
|