# Hardening drop-in for left4me-server@.service. # # Source of truth: this file (in left4me/deploy/files/). ckn-bw deploys # it to /etc/systemd/system/left4me-server@.service.d/10-hardening.conf # via a target-side symlink into the checkout. # # Gameserver unit: full hardening profile. No sudo path inside; no # sudo-incompatibility carve-outs. [Service] NoNewPrivileges=true RestrictSUIDSGID=true CapabilityBoundingSet= AmbientCapabilities= # srcds_linux is i386 (Source 2007 engine). Bare 'native' kills every # 32-bit syscall and traps srcds_run in a respawn loop. SystemCallArchitectures=native x86 SystemCallFilter=@system-service SystemCallFilter=~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete @privileged TemporaryFileSystem=/var/lib /etc /opt /home /root /srv /mnt /media BindReadOnlyPaths=/var/lib/left4me/installation BindReadOnlyPaths=/var/lib/left4me/overlays # Workshop VPKs in overlays are symlinks into workshop_cache; # without this bind they dangle inside the unit and Source # silently fails to load the addons. BindReadOnlyPaths=/var/lib/left4me/workshop_cache # Steam SDK: srcds dlopen's ~/.steam/sdk32/steamclient.so for # Steam master-server registration. Without this, SteamAPI_Init # fails and the server falls back to LAN-only mode regardless # of sv_lan=0 — clients then get "LAN servers are restricted # to local clients (class C)". .steam holds symlinks into # /var/lib/left4me/steam, so both paths need to be bound back # through TemporaryFileSystem. BindReadOnlyPaths=/var/lib/left4me/.steam BindReadOnlyPaths=/var/lib/left4me/steam BindReadOnlyPaths=/etc/left4me/host.env BindReadOnlyPaths=/etc/ssl BindReadOnlyPaths=/etc/ca-certificates BindReadOnlyPaths=/etc/resolv.conf BindReadOnlyPaths=/etc/nsswitch.conf BindReadOnlyPaths=/etc/alternatives BindPaths=/var/lib/left4me/runtime/%i ProtectSystem=strict ProtectHome=true PrivateUsers=true # PrivatePIDs is the test-plan amendment that closes D2.b: same-uid # ProtectProc=invisible cannot hide gunicorn from srcds (both run as # uid 980); a private PID namespace does. PrivatePIDs=true PrivateTmp=true PrivateDevices=true PrivateIPC=true RestrictNamespaces=true RestrictRealtime=true ProtectProc=invisible # ProcSubset=pid intentionally OMITTED — it hides /proc/cpuinfo and # /proc/sys/*, which breaks Source's tier0/cpu.cpp and (downstream) # SteamAPI_Init's pipe-creation step. Server then registers as LAN-only # and rejects external clients with "LAN servers are restricted to # local clients (class C)". PrivatePIDs=true (kernel PID namespace) is # the load-bearing peer-process isolation; ProtectProc=invisible is the # foreign-uid /proc hide. Losing ProcSubset=pid only exposes host kernel # info (cpuinfo, meminfo, sysctls), which is not sensitive in this # threat model. See ckn-bw commit 4339289 for the original fix. ProtectKernelTunables=true ProtectKernelModules=true ProtectKernelLogs=true ProtectClock=true ProtectControlGroups=true ProtectHostname=true LockPersonality=true RemoveIPC=true KeyringMode=private UMask=0027 RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # Lock srcds bindable sockets to the game port range. Hard-coded range # because systemd directive variable substitution is uneven for # SocketBindAllow. SocketBindAllow=udp:27000-27999 SocketBindAllow=tcp:27000-27999 # W+X mprotect (text relocations in Source engine i386 .so files) is # incompatible with the memory-deny-write-execute directive; that # directive is therefore intentionally absent from this drop-in.