deploy: extract hardening into drop-in files alongside the units
Hardening directives leave the base unit body and live in: deploy/files/etc/systemd/system/left4me-web.service.d/10-hardening.conf deploy/files/etc/systemd/system/left4me-server@.service.d/10-hardening.conf Reference units now describe just the base operational shape (exec, env, restart, resources). Tests split: base-unit content and hardening profile are asserted separately. Part of 2026-05-15-deployment-responsibility-design.md migration step 2. ckn-bw lands the matching reactor surgery + symlink delivery.
This commit is contained in:
parent
949f1bae78
commit
e9c172a619
5 changed files with 209 additions and 142 deletions
|
|
@ -0,0 +1,74 @@
|
|||
# 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
|
||||
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.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# Hardening drop-in for left4me-web.service.
|
||||
#
|
||||
# Source of truth: this file (in left4me/deploy/files/). ckn-bw deploys
|
||||
# it to /etc/systemd/system/left4me-web.service.d/10-hardening.conf via a
|
||||
# target-side symlink into the checkout.
|
||||
#
|
||||
# See docs/superpowers/specs/2026-05-15-hardening-defenses-survey.md
|
||||
# and 2026-05-15-hardening-test-plan.md for the threat model and the
|
||||
# verification matrix.
|
||||
#
|
||||
# This unit is the web app; some sudo-incompatible directives are
|
||||
# intentionally absent:
|
||||
# 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 exclusion in SystemCallFilter — blocks sudo's setuid syscall
|
||||
# All of those are unconditional on the gameserver unit (no sudo there).
|
||||
[Service]
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
ProtectProc=invisible
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectClock=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHostname=true
|
||||
LockPersonality=true
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
RemoveIPC=true
|
||||
KeyringMode=private
|
||||
UMask=0027
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
# left4me gameserver — system unit, one instance per gameserver.
|
||||
#
|
||||
# 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-server@.service'). Hardening directives live in
|
||||
# the HARDENING_SERVER constant near the top of the same file.
|
||||
# This file is hand-synced; edit both together.
|
||||
# This is the REFERENCE COPY of the deployed unit base body. The live
|
||||
# source is the systemd/units reactor at
|
||||
# ~/Projekte/ckn-bw/bundles/left4me/metadata.py (look for
|
||||
# 'left4me-server@.service').
|
||||
#
|
||||
# Hardening: see left4me-server@.service.d/10-hardening.conf
|
||||
#
|
||||
# Threat model: docs/superpowers/specs/2026-05-15-hardening-threat-model.md
|
||||
# Defenses survey: docs/superpowers/specs/2026-05-15-hardening-defenses-survey.md
|
||||
|
|
@ -29,10 +30,11 @@ EnvironmentFile=/var/lib/left4me/instances/%i/instance.env
|
|||
# once ExecStartPre's overlay mount succeeds.
|
||||
WorkingDirectory=-/var/lib/left4me/runtime/%i/merged/left4dead2
|
||||
# `+` prefix runs the helper as PID 1 (root, all caps, host
|
||||
# namespaces) — required because the unit has NoNewPrivileges=true
|
||||
# AND PrivateUsers=true; both block sudo's setuid path. nsenter into
|
||||
# PID 1's mount namespace ensures the umount in ExecStopPost succeeds
|
||||
# without EBUSY from the unit's own slave-mount tree.
|
||||
# namespaces) — required because the hardening drop-in sets
|
||||
# NoNewPrivileges and PrivateUsers; both block sudo's setuid path.
|
||||
# nsenter into PID 1's mount namespace ensures the umount in
|
||||
# ExecStopPost succeeds without EBUSY from the unit's own
|
||||
# slave-mount tree.
|
||||
ExecStartPre=+/usr/bin/nsenter --mount=/proc/1/ns/mnt -- /usr/local/libexec/left4me/left4me-overlay mount %i
|
||||
# Run from the merged overlay, NOT installation/. srcds_run cds to its
|
||||
# own dirname before exec'ing srcds_linux; the binary's path determines
|
||||
|
|
@ -57,72 +59,5 @@ KillSignal=SIGINT
|
|||
TimeoutStopSec=15s
|
||||
LogRateLimitIntervalSec=0
|
||||
|
||||
# === Identity / privilege drop ===
|
||||
NoNewPrivileges=true # block setuid escalation (defense: D3)
|
||||
RestrictSUIDSGID=true # block setuid()/setgid() syscalls
|
||||
CapabilityBoundingSet= # drop all caps — no privilege to escalate
|
||||
AmbientCapabilities=
|
||||
|
||||
# === Filesystem virtualization ===
|
||||
# Mask /var/lib, /etc, /opt, etc. with empty tmpfs; bind back only
|
||||
# what srcds needs. The DB (/var/lib/left4me/left4me.db) and web.env
|
||||
# (/etc/left4me/web.env) are intentionally not bound — they don't
|
||||
# exist in this unit's filesystem view (defenses: D1.a, D1.b).
|
||||
TemporaryFileSystem=/var/lib /etc /opt /home /root /srv /mnt /media
|
||||
BindReadOnlyPaths=/var/lib/left4me/installation
|
||||
BindReadOnlyPaths=/var/lib/left4me/overlays
|
||||
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 # belt-and-braces with TemporaryFileSystem
|
||||
ProtectHome=true
|
||||
|
||||
# === Process namespacing ===
|
||||
PrivateUsers=true # own user namespace; cross-uid ptrace blocked (D2)
|
||||
PrivatePIDs=true # own PID namespace; hides peer-srcds + gunicorn (D2.b, D5)
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
PrivateIPC=true
|
||||
RestrictNamespaces=true # block unshare()/clone(CLONE_NEW*)
|
||||
|
||||
# === /proc and /sys ===
|
||||
ProtectProc=invisible # foreign-uid /proc hidden (paired with PrivatePIDs for full hide)
|
||||
ProcSubset=pid # /proc shows only PID dirs, no kallsyms/cpuinfo
|
||||
ProtectKernelTunables=true # /proc/sys, /sys read-only
|
||||
ProtectKernelModules=true # no module load/unload
|
||||
ProtectKernelLogs=true # no /dev/kmsg or syslog()
|
||||
ProtectClock=true # no settimeofday()
|
||||
ProtectControlGroups=true # /sys/fs/cgroup read-only
|
||||
ProtectHostname=true # no sethostname()
|
||||
LockPersonality=true # no personality() switches
|
||||
|
||||
# === Syscall filter ===
|
||||
# srcds_linux is i386 (Source 2007 engine). 'native x86' allows both
|
||||
# x86_64 (from srcds_run + the dynamic linker) and i386 (from srcds_linux).
|
||||
# Bare 'native' traps srcds_run in a respawn loop.
|
||||
SystemCallArchitectures=native x86
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete @privileged
|
||||
# ~@debug is the load-bearing block for D2.a: drops ptrace(), process_vm_readv/writev().
|
||||
# ~@privileged blocks anything requiring CAP_*, redundant with empty bounding set.
|
||||
# MemoryDenyWriteExecute=true is NOT set — Source engine i386 .so files
|
||||
# have text relocations that need mprotect(W+X) during dynamic-linker pass.
|
||||
|
||||
# === Network ===
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # AF_UNIX needed for journald
|
||||
# Lock srcds bindable sockets to the game port range.
|
||||
SocketBindAllow=udp:27000-27999
|
||||
SocketBindAllow=tcp:27000-27999
|
||||
|
||||
# === Misc hygiene ===
|
||||
RestrictRealtime=true # no real-time scheduling
|
||||
RemoveIPC=true # clean up SysV IPC on unit stop
|
||||
KeyringMode=private # private kernel keyring
|
||||
UMask=0027
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
|||
|
|
@ -1,22 +1,11 @@
|
|||
# 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.
|
||||
# This is the REFERENCE COPY of the deployed unit base body. The live
|
||||
# source is the systemd/units reactor at
|
||||
# ~/Projekte/ckn-bw/bundles/left4me/metadata.py (look for
|
||||
# 'left4me-web.service').
|
||||
#
|
||||
# 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.
|
||||
# Hardening: see left4me-web.service.d/10-hardening.conf
|
||||
#
|
||||
# Threat model + defenses + tests: see docs/superpowers/specs/2026-05-15-hardening-*
|
||||
|
||||
|
|
@ -35,7 +24,7 @@ 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()'
|
||||
ExecStart=/var/lib/left4me/.venv/bin/gunicorn --workers 1 --threads 32 --bind 127.0.0.1:8000 'l4d2web.app:create_app()'
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
|
|
@ -44,42 +33,5 @@ RestartSec=3
|
|||
# (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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ SYSCTL_CONF = DEPLOY / "files/etc/sysctl.d/99-left4me.conf"
|
|||
SANDBOX_RESOLV_CONF = DEPLOY / "files/etc/left4me/sandbox-resolv.conf"
|
||||
HOST_ENV = DEPLOY / "templates/etc/left4me/host.env"
|
||||
WEB_ENV_TEMPLATE = DEPLOY / "templates/etc/left4me/web.env.template"
|
||||
WEB_HARDENING_DROPIN = DEPLOY / "files/etc/systemd/system/left4me-web.service.d/10-hardening.conf"
|
||||
SERVER_HARDENING_DROPIN = DEPLOY / "files/etc/systemd/system/left4me-server@.service.d/10-hardening.conf"
|
||||
|
||||
|
||||
def test_global_unit_files_exist_at_product_level_paths():
|
||||
|
|
@ -41,14 +43,14 @@ def test_web_unit_contains_required_runtime_contract():
|
|||
# NoNewPrivileges must remain unset because sudo (used by the overlay,
|
||||
# systemctl and journalctl helpers) is setuid.
|
||||
assert "NoNewPrivileges=true" not in unit
|
||||
# Restored now that fuse-overlayfs propagation is no longer the mechanism.
|
||||
assert "PrivateTmp=true" in unit
|
||||
assert "ProtectSystem=full" in unit
|
||||
assert "ReadWritePaths=/var/lib/left4me" in unit
|
||||
# Mounts now happen in PID 1's namespace via the left4me-overlay helper,
|
||||
# so MountFlags propagation is irrelevant — and the previous assumption
|
||||
# that MountFlags=shared made it work was incorrect.
|
||||
assert "MountFlags=" not in unit
|
||||
# Hardening directives belong in the drop-in; must not appear in the base unit.
|
||||
assert "PrivateTmp=" not in unit
|
||||
assert "ProtectSystem=" not in unit
|
||||
|
||||
|
||||
def test_server_unit_contains_required_runtime_contract():
|
||||
|
|
@ -69,15 +71,14 @@ def test_server_unit_contains_required_runtime_contract():
|
|||
assert "ExecStart=/var/lib/left4me/runtime/%i/merged/srcds_run" in unit
|
||||
assert "$L4D2_ARGS" in unit
|
||||
assert "${L4D2_ARGS}" not in unit
|
||||
assert "NoNewPrivileges=true" in unit
|
||||
assert "PrivateTmp=true" in unit
|
||||
assert "PrivateDevices=true" in unit
|
||||
assert "ProtectHome=true" in unit
|
||||
assert "ProtectSystem=strict" in unit
|
||||
assert "ReadOnlyPaths=/var/lib/left4me/installation /var/lib/left4me/overlays" in unit
|
||||
assert "ReadWritePaths=/var/lib/left4me/runtime/%i" in unit
|
||||
assert "RestrictSUIDSGID=true" in unit
|
||||
assert "LockPersonality=true" in unit
|
||||
# Hardening directives belong in the drop-in; must not appear in the base unit.
|
||||
assert "NoNewPrivileges=" not in unit
|
||||
assert "PrivateTmp=" not in unit
|
||||
assert "PrivateDevices=" not in unit
|
||||
assert "ProtectHome=" not in unit
|
||||
assert "ProtectSystem=" not in unit
|
||||
assert "RestrictSUIDSGID=" not in unit
|
||||
assert "LockPersonality=" not in unit
|
||||
|
||||
|
||||
def test_server_unit_mounts_overlay_via_exec_start_pre():
|
||||
|
|
@ -232,3 +233,69 @@ def test_sandbox_resolv_conf_exists():
|
|||
if first_octet == 172:
|
||||
second_octet = int(ns.split(".")[1])
|
||||
assert not (16 <= second_octet <= 31), ns
|
||||
|
||||
|
||||
def test_web_hardening_dropin_present_with_directives():
|
||||
assert WEB_HARDENING_DROPIN.is_file()
|
||||
text = WEB_HARDENING_DROPIN.read_text()
|
||||
assert "[Service]" in text
|
||||
# COMMON
|
||||
for d in (
|
||||
"ProtectProc=invisible",
|
||||
"ProtectKernelTunables=true",
|
||||
"ProtectKernelModules=true",
|
||||
"ProtectKernelLogs=true",
|
||||
"ProtectClock=true",
|
||||
"ProtectControlGroups=true",
|
||||
"ProtectHostname=true",
|
||||
"LockPersonality=true",
|
||||
"ProtectSystem=strict",
|
||||
"ProtectHome=true",
|
||||
"PrivateTmp=true",
|
||||
"RestrictNamespaces=true",
|
||||
"RestrictRealtime=true",
|
||||
"RemoveIPC=true",
|
||||
"KeyringMode=private",
|
||||
"UMask=0027",
|
||||
"RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX",
|
||||
):
|
||||
assert d in text, f"missing {d!r} in web hardening drop-in"
|
||||
# WEB-specific
|
||||
assert "SystemCallArchitectures=native" in text
|
||||
assert "SystemCallFilter=@system-service" in text
|
||||
assert "SystemCallFilter=~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete" in text
|
||||
# WEB must NOT include the sudo-incompatible directives.
|
||||
assert "NoNewPrivileges=" not in text
|
||||
assert "PrivateUsers=" not in text
|
||||
assert "RestrictSUIDSGID=" not in text
|
||||
assert "CapabilityBoundingSet=" not in text
|
||||
assert "~@privileged" not in text
|
||||
|
||||
|
||||
def test_server_hardening_dropin_present_with_directives():
|
||||
assert SERVER_HARDENING_DROPIN.is_file()
|
||||
text = SERVER_HARDENING_DROPIN.read_text()
|
||||
assert "[Service]" in text
|
||||
for d in (
|
||||
"NoNewPrivileges=true",
|
||||
"RestrictSUIDSGID=true",
|
||||
"PrivateUsers=true",
|
||||
"PrivatePIDs=true",
|
||||
"PrivateIPC=true",
|
||||
"PrivateDevices=true",
|
||||
"CapabilityBoundingSet=",
|
||||
"AmbientCapabilities=",
|
||||
"SystemCallArchitectures=native x86",
|
||||
"ProcSubset=pid",
|
||||
"TemporaryFileSystem=/var/lib /etc /opt /home /root /srv /mnt /media",
|
||||
"BindReadOnlyPaths=/var/lib/left4me/installation",
|
||||
"BindReadOnlyPaths=/var/lib/left4me/overlays",
|
||||
"BindReadOnlyPaths=/etc/left4me/host.env",
|
||||
"BindPaths=/var/lib/left4me/runtime/%i",
|
||||
"SocketBindAllow=udp:27000-27999",
|
||||
"SocketBindAllow=tcp:27000-27999",
|
||||
):
|
||||
assert d in text, f"missing {d!r} in server hardening drop-in"
|
||||
assert "SystemCallFilter=~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete @privileged" in text
|
||||
# MemoryDenyWriteExecute must remain absent (Source engine compat).
|
||||
assert "MemoryDenyWriteExecute" not in text
|
||||
|
|
|
|||
Loading…
Reference in a new issue