# 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.
