# Hardening refactor — implementation plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Land the proven Test 7 hardening composition into ckn-bw's `systemd/units` reactor for `left4me-server@.service` and `left4me-web.service`, ship the `kernel.yama.ptrace_scope=2` sysctl via the bundle, mirror the reference units with per-directive educational comments, fix four spec bugs in the test plan, mark the uid-split spec superseded, and verify the deploy on `left4.me`. **Architecture:** Three shared Python dicts in ckn-bw's `metadata.py` (`HARDENING_COMMON`, `HARDENING_SERVER`, `HARDENING_WEB`) spread via `**` into the existing `systemd/units` reactor entries. Multi-value directives encoded as Python tuples (existing precedent: `EnvironmentFile` at `metadata.py:201-204`). Reference units in `left4me/deploy/files/usr/local/lib/systemd/system/` hand-synced to match the emission, with per-directive comments explaining each threat addressed. Broader configmgmt responsibility reshape (drop-ins owned by left4me) deliberately deferred. **Tech Stack:** Python 3, bundlewrap (ckn-bw), systemd 257 (Debian 13 Trixie on `left4.me`), pytest (for `deploy/tests/`), bash (for verification on host). **Repos:** This plan touches two repos. Each task header notes which: - `~/Projekte/left4me` (project repo — this file lives here) - `~/Projekte/ckn-bw` (config-management bundle repo) --- ## Task 1: Verify ckn-bw systemd-bundle emitter handles tuples and empty values **Repo:** `~/Projekte/ckn-bw` **Why:** The factoring depends on two emitter behaviors: 1. A tuple of strings → emitted as repeated `Key=Value` lines (needed for `SystemCallFilter`, `BindReadOnlyPaths`, `SocketBindAllow`). 2. An empty string → emitted as `Key=` with no value (needed for `CapabilityBoundingSet=`, `AmbientCapabilities=`). If either doesn't work, the factoring needs to change shape (inline-join strings, or use a sentinel). **Files:** - Read: `~/Projekte/ckn-bw/bundles/systemd/` (the upstream bundlewrap systemd bundle; locate where unit emission happens) - Read: `~/Projekte/ckn-bw/bundles/left4me/metadata.py:201-204` (existing tuple-emission precedent for `EnvironmentFile`) - [ ] **Step 1: Inspect the systemd bundle's emitter for multi-value handling** Run: ```bash cd ~/Projekte/ckn-bw find bundles/systemd -name '*.py' -type f # Expected: items.py, possibly templates/ grep -rn 'tuple\|list\|EnvironmentFile' bundles/systemd/ 2>/dev/null # Look for how multi-value emission works ``` Then read the emitter source. Expected behavior: iterates the dict; if the value is a tuple/list, emits one `Key=Value` line per element; if the value is a string, emits one line. - [ ] **Step 2: Inspect handling of empty values** Run: ```bash grep -rn "''\\|None\\|if value" bundles/systemd/ 2>/dev/null ``` Read the emitter to see if it strips empty strings or emits `Key=`. If unclear from reading, write a tiny test: ```python # Save as /tmp/test-emitter.py — adapt to actual emitter import path from bundles.systemd.items import render_unit # or whatever the real entry point is unit = { 'Service': { 'Type': 'simple', 'CapabilityBoundingSet': '', 'SystemCallFilter': ('@system-service', '~@debug @mount'), } } print(render_unit('test.service', unit)) ``` Run it (adapting paths to ckn-bw's actual layout). Expected: a unit body containing `CapabilityBoundingSet=` (empty value) and two `SystemCallFilter=` lines. - [ ] **Step 3: Record findings in the design doc's "Open items resolved in implementation"** If both behaviors work as expected: Edit `~/Projekte/left4me/docs/superpowers/specs/2026-05-15-hardening-refactor-design.md`. Replace the "Open items resolved in implementation, not design" section with: ```markdown ## Implementation notes (resolved during plan execution) - The ckn-bw systemd-bundle emitter renders Python tuples as repeated `Key=Value` lines and renders empty strings as `Key=` with no value. The factoring uses both. Verified on against bundles/systemd at . - `SocketBindAllow=` value: hard-coded port range `27000-27999` (matches the `LEFT4ME_PORT_RANGE_*` metadata values). Variable substitution in this directive is not supported. ``` If either behavior is broken, document the fallback and adjust later tasks accordingly. The most common fallbacks: - Tuple not supported → inline-join with space for `SystemCallFilter` (it accepts space-separated within a line, though semantics differ — read systemd.exec(5) carefully). - Empty value not supported → patch the emitter or open a ckn-bw issue. - [ ] **Step 4: Commit the design-doc update** ```bash cd ~/Projekte/left4me git add docs/superpowers/specs/2026-05-15-hardening-refactor-design.md git commit -m "$(cat <<'EOF' spec(hardening-refactor): resolve emitter open items Verified during plan execution that the ckn-bw systemd-bundle emitter handles tuples and empty values as expected. SocketBindAllow port range hard-coded since systemd directive variable substitution is not universal. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 2: Add `HARDENING_COMMON`, `HARDENING_SERVER`, `HARDENING_WEB` constants **Repo:** `~/Projekte/ckn-bw` **Files:** - Modify: `~/Projekte/ckn-bw/bundles/left4me/metadata.py` (add the three constants near the top of the file, after imports, before the first `@metadata_reactor.provides` decorator) - [ ] **Step 1: Read the current file head to find insertion point** Run: ```bash head -30 ~/Projekte/ckn-bw/bundles/left4me/metadata.py ``` Identify where the imports end and the first `@metadata_reactor.provides` starts. The new constants go between. - [ ] **Step 2: Add the three constants** Insert into `~/Projekte/ckn-bw/bundles/left4me/metadata.py` between imports and the first reactor: ```python # Hardening composition — proven via the hardening test plan (left4me # commit 461b8d0). See: # docs/superpowers/specs/2026-05-15-hardening-threat-model.md # docs/superpowers/specs/2026-05-15-hardening-defenses-survey.md # docs/superpowers/specs/2026-05-15-hardening-test-plan.md # docs/superpowers/specs/2026-05-15-hardening-refactor-design.md # (paths in the left4me repo) # Directives both managed units take verbatim. HARDENING_COMMON = { 'ProtectProc': 'invisible', 'ProcSubset': 'pid', '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', } # Gameserver unit: COMMON + sudo-incompatible flags + filesystem # virtualization + i386 amendment + per-instance PID namespace + bound # socket binds. HARDENING_SERVER = { **HARDENING_COMMON, 'NoNewPrivileges': 'true', 'RestrictSUIDSGID': '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', 'PrivateIPC': 'true', 'PrivateDevices': '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', '~@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', '/var/lib/left4me/overlays', '/etc/left4me/host.env', '/etc/ssl', '/etc/ca-certificates', '/etc/resolv.conf', '/etc/nsswitch.conf', '/etc/alternatives', ), 'BindPaths': '/var/lib/left4me/runtime/%i', # Lock srcds bindable sockets to the game port range. Hard-coded # range because systemd directive variable substitution is uneven. 'SocketBindAllow': ( 'udp:27000-27999', 'tcp:27000-27999', ), # MemoryDenyWriteExecute=true permanently excluded — Source engine # i386 .so files have text relocations that need mprotect(W+X) # during the dynamic linker's relocation pass. } # Web unit: COMMON + sudo-compatible additions. EXCLUDES # NoNewPrivileges, PrivateUsers, RestrictSUIDSGID, empty # CapabilityBoundingSet, and ~@privileged in the syscall filter — all # sudo-incompatible until a future refactor replaces sudo with # systemctl-managed transient units. HARDENING_WEB = { **HARDENING_COMMON, 'SystemCallArchitectures': 'native', 'SystemCallFilter': ( '@system-service', '~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete', ), } ``` - [ ] **Step 3: Verify the file still parses** Run: ```bash cd ~/Projekte/ckn-bw python3 -c "import ast; ast.parse(open('bundles/left4me/metadata.py').read())" # Expected: no output (no syntax error) ``` - [ ] **Step 4: Run `bw test` if available** Run: ```bash cd ~/Projekte/ckn-bw bw test ovh.left4me 2>&1 | head -50 # Expected: passes, or fails only on already-known issues unrelated # to these constants ``` - [ ] **Step 5: Commit** ```bash cd ~/Projekte/ckn-bw git add bundles/left4me/metadata.py git commit -m "$(cat <<'EOF' bundles/left4me: add HARDENING_{COMMON,SERVER,WEB} constants Three shared dicts capturing the proven hardening composition from the left4me hardening test plan (left4me commit 461b8d0). HARDENING_COMMON is the directive set both managed units take verbatim; HARDENING_SERVER adds the sudo-incompatible flags + filesystem virtualization + i386 amendment + PrivatePIDs + SocketBindAllow; HARDENING_WEB adds the sudo-compatible syscall filter. Not yet spread into the unit emission — that's the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 3: Spread `HARDENING_SERVER` into `left4me-server@.service` reactor entry **Repo:** `~/Projekte/ckn-bw` **Files:** - Modify: `~/Projekte/ckn-bw/bundles/left4me/metadata.py` (the `left4me-server@.service` entry inside the `systemd/units` reactor, currently at ~line 221+) - [ ] **Step 1: Read the current emission** Run: ```bash sed -n '221,276p' ~/Projekte/ckn-bw/bundles/left4me/metadata.py ``` Identify the `Service` dict inside the `left4me-server@.service` entry. Note any existing hardening keys (`NoNewPrivileges`, `PrivateTmp`, `ProtectSystem`, `ReadOnlyPaths`, `ReadWritePaths`, etc.) — they will be removed in favor of the spread. - [ ] **Step 2: Edit the entry to spread `HARDENING_SERVER` and remove duplicated keys** Modify the `Service` dict to look like: ```python 'left4me-server@.service': { 'Unit': { 'Description': 'left4me server instance %i', 'After': 'network-online.target', 'Wants': 'network-online.target', 'StartLimitBurst': '5', 'StartLimitIntervalSec': '60s', }, 'Service': { 'Type': 'simple', 'User': 'left4me', 'Group': 'left4me', 'EnvironmentFile': ( '/etc/left4me/host.env', '/var/lib/left4me/instances/%i/instance.env', ), 'WorkingDirectory': '-/var/lib/left4me/runtime/%i/merged/left4dead2', 'ExecStartPre': '+/usr/bin/nsenter --mount=/proc/1/ns/mnt -- /usr/local/libexec/left4me/left4me-overlay mount %i', 'ExecStart': '/var/lib/left4me/runtime/%i/merged/srcds_run -game left4dead2 +hostport ${L4D2_PORT} $L4D2_ARGS', 'ExecStopPost': '+/usr/bin/nsenter --mount=/proc/1/ns/mnt -- /usr/local/libexec/left4me/left4me-overlay umount %i', 'Restart': 'on-failure', 'RestartSec': '5', # Resource control (baseline from prior performance work). 'Slice': 'l4d2-game.slice', 'Nice': '-5', 'IOSchedulingClass': 'best-effort', 'IOSchedulingPriority': '4', 'OOMScoreAdjust': '-200', 'MemoryHigh': '1.5G', 'MemoryMax': '2G', 'TasksMax': '256', 'LimitNOFILE': '65536', 'KillSignal': 'SIGINT', 'TimeoutStopSec': '15s', 'LogRateLimitIntervalSec': '0', # Hardening profile — see HARDENING_SERVER constant near top # of this file for per-directive rationale. **HARDENING_SERVER, }, 'Install': { 'WantedBy': {'multi-user.target'}, }, }, ``` Removed from the prior emission (now provided by `HARDENING_SERVER`): `NoNewPrivileges`, `PrivateTmp`, `PrivateDevices`, `ProtectHome`, `ProtectSystem`, `RestrictSUIDSGID`, `LockPersonality`, `ReadOnlyPaths`, `ReadWritePaths`. (`ReadOnlyPaths` and `ReadWritePaths` are superseded by `TemporaryFileSystem` + `BindReadOnlyPaths` + `BindPaths` in `HARDENING_SERVER`.) - [ ] **Step 3: Verify the file still parses and `bw test` passes** Run: ```bash cd ~/Projekte/ckn-bw python3 -c "import ast; ast.parse(open('bundles/left4me/metadata.py').read())" bw test ovh.left4me 2>&1 | head -50 ``` - [ ] **Step 4: Render the unit to a temp file and visually diff against the proven Test 7 composition** If `bw` has a render-without-apply mode (`bw apply --interactive` or `bw items` etc.), use it. Otherwise: ```bash cd ~/Projekte/ckn-bw bw items ovh.left4me 'svc_systemd:left4me-server@.service' 2>/dev/null || \ bw debug -n ovh.left4me -c "from bundlewrap.utils import get_file_contents; print(repo.get_node('ovh.left4me').metadata.get('systemd/units')['left4me-server@.service'])" ``` Adapt to whatever inspection command ckn-bw supports. Compare visually to the Test 7 drop-in (recorded inline in left4me's `docs/superpowers/specs/2026-05-15-hardening-test-plan.md` under "## Test 7" section): every directive that was in the proven Test 7 drop-in should now be present in the rendered unit. Note specifically: `PrivatePIDs=true` and `SystemCallArchitectures=native x86` must appear (the test amendments). `MemoryDenyWriteExecute=true` must NOT appear (permanently excluded). - [ ] **Step 5: Commit** ```bash cd ~/Projekte/ckn-bw git add bundles/left4me/metadata.py git commit -m "$(cat <<'EOF' bundles/left4me: spread HARDENING_SERVER into left4me-server@.service Replaces the inline hardening directives on the gameserver unit with the shared HARDENING_SERVER dict. Removes legacy ReadOnlyPaths / ReadWritePaths (superseded by TemporaryFileSystem + BindReadOnlyPaths + BindPaths in the dict). Brings the unit to the proven Test 7 composition with the i386 amendment (SystemCallArchitectures=native x86) and PrivatePIDs=true. Not deployed until bw apply. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 4: Spread `HARDENING_WEB` into `left4me-web.service` reactor entry **Repo:** `~/Projekte/ckn-bw` **Files:** - Modify: `~/Projekte/ckn-bw/bundles/left4me/metadata.py` (the `left4me-web.service` entry, ~line 186) - [ ] **Step 1: Read the current emission** Run: ```bash sed -n '186,220p' ~/Projekte/ckn-bw/bundles/left4me/metadata.py ``` The current entry has `ProtectSystem='full'` and `PrivateTmp='true'` and a comment about `NoNewPrivileges` intentionally not being set. - [ ] **Step 2: Edit the entry to spread `HARDENING_WEB`** ```python 'left4me-web.service': { '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', '/etc/left4me/web.env', ), 'ExecStart': ( '/opt/left4me/.venv/bin/gunicorn ' f'--workers {workers} --threads {threads} ' "--bind 127.0.0.1:8000 'l4d2web.app:create_app()'" ), 'Restart': 'on-failure', 'RestartSec': '3', # Web app writes broadly under /var/lib/left4me. Kept inline # because it's web-specific (server@ uses BindPaths to bind # only its instance dir). 'ReadWritePaths': '/var/lib/left4me', # Hardening profile — see HARDENING_WEB constant near top of # this file. NoNewPrivileges intentionally NOT set: workers # sudo to the helpers. PrivateUsers and RestrictSUIDSGID also # absent for the same reason. ProtectSystem tightens from # 'full' to 'strict' via HARDENING_COMMON. **HARDENING_WEB, }, 'Install': { 'WantedBy': {'multi-user.target'}, }, }, ``` Removed from the prior emission (now provided by `HARDENING_WEB`): `PrivateTmp`, `ProtectSystem` (tightened from `full` → `strict`). - [ ] **Step 3: Verify the file still parses and `bw test` passes** ```bash cd ~/Projekte/ckn-bw python3 -c "import ast; ast.parse(open('bundles/left4me/metadata.py').read())" bw test ovh.left4me 2>&1 | head -50 ``` - [ ] **Step 4: Commit** ```bash cd ~/Projekte/ckn-bw git add bundles/left4me/metadata.py git commit -m "$(cat <<'EOF' bundles/left4me: spread HARDENING_WEB into left4me-web.service Adds the sudo-compatible hardening subset to the web unit. Tightens ProtectSystem=full → strict. NoNewPrivileges, PrivateUsers, RestrictSUIDSGID, empty CapabilityBoundingSet, and ~@privileged in the syscall filter intentionally absent (sudo-incompatible until a future refactor replaces the helper sudo with systemctl-managed transient units). Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 5: Manage `kernel.yama.ptrace_scope=2` sysctl via the bundle **Repo:** `~/Projekte/ckn-bw` **Files:** - Locate or create: a sysctl drop-in. Path depends on the convention discovered in Step 1; candidates: - `~/Projekte/ckn-bw/bundles/left4me/files/etc/sysctl.d/99-left4me-ptrace.conf` for the file-based pattern - A `sysctl/options` reactor entry inline in `bundles/left4me/metadata.py` for the reactor-based pattern - [ ] **Step 1: Find the existing sysctl pattern in ckn-bw** Run: ```bash cd ~/Projekte/ckn-bw grep -rn 'sysctl\.d\|kernel\.' bundles/ 2>/dev/null | grep -v '^bundles/left4me/' | head -10 ``` Look for an existing `pkg_files:` or `files:` entry that targets `/etc/sysctl.d/*`, or a `metadata.get('sysctl/...')` reactor pattern. Read the relevant bundle's `items.py` or `metadata.py` to understand the convention. If ckn-bw has a `sysctl` bundle with a reactor for `sysctl/*` keys, use that. Otherwise, fall back to a direct `files:` entry in the left4me bundle. - [ ] **Step 2: Add the sysctl drop-in** If ckn-bw has a sysctl reactor: Add to `~/Projekte/ckn-bw/bundles/left4me/metadata.py`, in a new `@metadata_reactor.provides('sysctl/...')` reactor or by appending to an existing one: ```python @metadata_reactor.provides('sysctl/options') def sysctl_left4me(metadata): return { 'sysctl': { 'options': { # Block ptrace except from CAP_SYS_PTRACE holders. Belt-and- # braces with SystemCallFilter=~@debug + PrivateUsers=true # in the gameserver unit. See: # left4me docs/superpowers/specs/2026-05-15-hardening-defenses-survey.md 'kernel.yama.ptrace_scope': '2', }, }, } ``` If ckn-bw uses a direct `files:` entry pattern: Create `~/Projekte/ckn-bw/bundles/left4me/files/etc/sysctl.d/99-left4me-ptrace.conf` with: ``` # Block ptrace except from CAP_SYS_PTRACE holders. Belt-and-braces with # SystemCallFilter=~@debug + PrivateUsers=true in the gameserver unit. # See left4me docs/superpowers/specs/2026-05-15-hardening-defenses-survey.md kernel.yama.ptrace_scope = 2 ``` And add to `~/Projekte/ckn-bw/bundles/left4me/items.py` in the `files = {...}` dict: ```python '/etc/sysctl.d/99-left4me-ptrace.conf': { 'mode': '0644', 'owner': 'root', 'group': 'root', 'triggers': { 'action:sysctl_reload', # adapt to whatever the sysctl reload action is called in ckn-bw }, }, ``` - [ ] **Step 3: Verify with `bw test`** ```bash cd ~/Projekte/ckn-bw bw test ovh.left4me 2>&1 | head -50 ``` - [ ] **Step 4: Commit** ```bash cd ~/Projekte/ckn-bw git add bundles/left4me/ git commit -m "$(cat <<'EOF' bundles/left4me: ship kernel.yama.ptrace_scope=2 sysctl drop-in Belt-and-braces with the gameserver unit's SystemCallFilter=~@debug + PrivateUsers=true. Currently applied by hand on left4.me (left over from the hardening test plan's Test 9); landing in the bundle so it survives bw apply and is reproducible on any future host. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 6: Update reference unit `deploy/files/usr/local/lib/systemd/system/left4me-server@.service` with per-directive comments **Repo:** `~/Projekte/left4me` **Files:** - Modify: `~/Projekte/left4me/deploy/files/usr/local/lib/systemd/system/left4me-server@.service` - [ ] **Step 1: Read the current reference** Run: ```bash cat ~/Projekte/left4me/deploy/files/usr/local/lib/systemd/system/left4me-server@.service ``` - [ ] **Step 2: Replace with the new annotated content** Overwrite the file with: ```ini # 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. # # Threat model: docs/superpowers/specs/2026-05-15-hardening-threat-model.md # Defenses survey: docs/superpowers/specs/2026-05-15-hardening-defenses-survey.md # Test plan + results: docs/superpowers/specs/2026-05-15-hardening-test-plan.md [Unit] Description=left4me server instance %i After=network-online.target Wants=network-online.target # Bound the restart loop. Without these, a persistent ExecStartPre or # ExecStart failure spins indefinitely. StartLimitBurst=5 StartLimitIntervalSec=60s [Service] Type=simple User=left4me Group=left4me EnvironmentFile=/etc/left4me/host.env EnvironmentFile=/var/lib/left4me/instances/%i/instance.env # `-` prefix: chdir failure is non-fatal. The merged dir only exists # 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. 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 # gameinfo + addons lookup. ExecStart=/var/lib/left4me/runtime/%i/merged/srcds_run -game left4dead2 +hostport ${L4D2_PORT} $L4D2_ARGS ExecStopPost=+/usr/bin/nsenter --mount=/proc/1/ns/mnt -- /usr/local/libexec/left4me/left4me-overlay umount %i Restart=on-failure RestartSec=5 # === Resource control baseline === # See docs/superpowers/specs/2026-05-09-l4d2-server-host-perf-baseline-design.md Slice=l4d2-game.slice Nice=-5 IOSchedulingClass=best-effort IOSchedulingPriority=4 OOMScoreAdjust=-200 MemoryHigh=1.5G MemoryMax=2G TasksMax=256 LimitNOFILE=65536 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 ``` - [ ] **Step 3: Verify content visually** Run: ```bash diff <(cat ~/Projekte/left4me/deploy/files/usr/local/lib/systemd/system/left4me-server@.service | grep -vE '^#|^\s*$') \ <(echo "expected directive set" | tr ' ' '\n' | sort) ``` (Or just read the file and confirm each directive matches the reactor's emission.) - [ ] **Step 4: Commit (deferred — commit with Task 7's reference for one logical change)** Don't commit yet; the next task updates the web reference. Commit both together at the end of Task 7. --- ## Task 7: Update reference unit `deploy/files/usr/local/lib/systemd/system/left4me-web.service` with per-directive comments **Repo:** `~/Projekte/left4me` **Files:** - Modify: `~/Projekte/left4me/deploy/files/usr/local/lib/systemd/system/left4me-web.service` - [ ] **Step 1: Read the current reference** ```bash cat ~/Projekte/left4me/deploy/files/usr/local/lib/systemd/system/left4me-web.service ``` - [ ] **Step 2: Replace with the new annotated content** ```ini # 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 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 ``` (Note: the `--workers 4 --threads 4` values are placeholders; the live emission uses metadata-derived values. The reference doc can use a fixed example — operator note in commit message will clarify.) - [ ] **Step 3: Commit both reference units together** ```bash cd ~/Projekte/left4me git add deploy/files/usr/local/lib/systemd/system/left4me-server@.service \ deploy/files/usr/local/lib/systemd/system/left4me-web.service git commit -m "$(cat <<'EOF' deploy/files: annotate reference units with per-directive hardening comments Update the educational reference copies of left4me-server@.service and left4me-web.service to match the new hardening composition from the ckn-bw reactor (HARDENING_COMMON + HARDENING_SERVER / HARDENING_WEB). Per-directive comments explain each defense's purpose and the threat it addresses, so a cold reader of this repo can understand the threat model from the unit file alone. Top-of-file note in each reference points at the ckn-bw reactor as the live source; reference is hand-synced. gunicorn ExecStart in the web reference uses placeholder '--workers 4 --threads 4' values; live emission interpolates from metadata. This is the documented divergence between the reference and the deployed unit. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 8: Fix four spec bugs in the hardening test plan **Repo:** `~/Projekte/left4me` **Files:** - Modify: `~/Projekte/left4me/docs/superpowers/specs/2026-05-15-hardening-test-plan.md` The four bugs (per the test plan's existing "Spec bugs surfaced" / "Output" section, recorded by the test executor in commit `461b8d0`): A. **PID-lookup race.** `pgrep -f 'srcds_linux.*left4dead2' | head -1` picks whichever instance's PID is lowest — often `@2` not `@1`. Replace with `systemctl show -p MainPID --value left4me-server@1.service`. B. **gdb-from-host ptrace verification flaw.** `sudo nsenter --target $PID --mount -- gdb -p $TARGET` runs gdb as root with full caps in only the mount namespace; the unit's SECCOMP filter doesn't apply. Replace with a probe that runs *inside the same hardening profile* via `systemd-run` with the same directives, or inspect the compiled `SystemCallFilter` directly. C. **D5 pgrep pattern won't match.** `pgrep -f 'srcds_linux.*\@2'` doesn't match because the `@N` lives in the systemd unit name, not in argv. Use `systemctl show -p MainPID --value left4me-server@2.service` or look up the instance by game port (`27021` for @2 in current deployments). D. **`scmp_sys_resolver` package name.** It's in `seccomp` on Debian 13, not `libseccomp-dev` as the spec said. - [ ] **Step 1: Locate the four references in the test plan** ```bash grep -n "pgrep -f 'srcds_linux" ~/Projekte/left4me/docs/superpowers/specs/2026-05-15-hardening-test-plan.md grep -n "nsenter.*gdb\|gdb.*--batch -p" ~/Projekte/left4me/docs/superpowers/specs/2026-05-15-hardening-test-plan.md grep -n "libseccomp-dev" ~/Projekte/left4me/docs/superpowers/specs/2026-05-15-hardening-test-plan.md ``` Each occurrence is a fix site. - [ ] **Step 2: Apply fix A — PID-lookup race** Find and replace, across the test plan: ``` PID=$(pgrep -f 'srcds_linux.*left4dead2' | head -1) ``` With: ``` PID=$(systemctl show -p MainPID --value left4me-server@1.service) ``` (For tests targeting `@2`, use `@2.service` instead.) - [ ] **Step 3: Apply fix B — gdb verification flaw** Find every block like: ```bash sudo nsenter --target $PID --mount -- /usr/bin/gdb --batch -p $GUNICORN_PID 2>&1 | tail -3 # Expect: ptrace: Operation not permitted ``` Replace with a note that this verification is unreliable, and offer two replacement approaches: ```bash # This nsenter-based gdb runs as root with full caps in only the unit's # mount namespace; the SECCOMP filter doesn't apply, so the result is # not meaningful. Use one of these instead: # # Option A: probe inside the same hardening profile. sudo systemd-run --pty --uid=left4me --gid=left4me \ -p NoNewPrivileges=true \ -p PrivateUsers=true \ -p CapabilityBoundingSet= \ -p AmbientCapabilities= \ -p SystemCallArchitectures='native x86' \ -p SystemCallFilter='@system-service' \ -p SystemCallFilter='~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete @privileged' \ -- /usr/bin/gdb --batch -p $GUNICORN_PID 2>&1 | tail -3 # Expect: ptrace: Operation not permitted (or seccomp-related kill) # # Option B: inspect the compiled SystemCallFilter directly. sudo systemd-analyze syscall-filter left4me-server@1.service 2>&1 | grep -E '^(ptrace|process_vm)' # Expect: empty (these syscalls are not in the allow list) ``` (Note: `systemd-analyze syscall-filter` may not accept a unit name; if not, parse the unit's `SystemCallFilter=` lines and resolve via `systemd-analyze syscall-filter @system-service`.) - [ ] **Step 4: Apply fix C — D5 pgrep pattern** Find Test 8's D5 verification: ```bash PID2=$(pgrep -f 'srcds_linux.*\@2' | head -1) ``` Replace with: ```bash PID2=$(systemctl show -p MainPID --value left4me-server@2.service) ``` - [ ] **Step 5: Apply fix D — package name** Find and replace: ``` scmp_sys_resolver (in libseccomp-dev; install on demand for Test 3/4 if filters need analysis) ``` With: ``` scmp_sys_resolver (in `seccomp` package on Debian 13; install on demand for Test 3/4 if filters need analysis) ``` - [ ] **Step 6: Mark the "Spec bugs surfaced" subsection as resolved** If the test plan has a section titled "Spec bugs surfaced" or "Output", append a resolution line: ```markdown **Resolved 2026-05-15 via the hardening-refactor plan** (commit will be filled in after step 7 of Task 8). The four bugs are fixed in-place in the test commands above. ``` - [ ] **Step 7: Commit** ```bash cd ~/Projekte/left4me git add docs/superpowers/specs/2026-05-15-hardening-test-plan.md git commit -m "$(cat <<'EOF' spec(hardening-test-plan): fix four bugs surfaced by executor Four corrections noted by the test plan's executor in commit 461b8d0: - PID-lookup race: pgrep+head can pick the wrong instance. Replace with systemctl show -p MainPID --value left4me-server@N.service. - gdb-from-host ptrace check: nsenter into only the mount namespace with root caps bypasses the SECCOMP filter, so the test is a false positive. Replace with systemd-run-with-same-directives probe, or syscall-filter inspection. - D5 pgrep pattern: 'srcds_linux.*\@2' doesn't match because @N is in the unit name, not argv. Use systemctl show -p MainPID. - scmp_sys_resolver is in the seccomp package on Debian 13, not libseccomp-dev. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 9: Mark `2026-05-15-user-uid-split-design.md` superseded **Repo:** `~/Projekte/left4me` **Files:** - Modify: `~/Projekte/left4me/docs/superpowers/specs/2026-05-15-user-uid-split-design.md` (front-matter) - [ ] **Step 1: Read the current top of the file** ```bash head -10 ~/Projekte/left4me/docs/superpowers/specs/2026-05-15-user-uid-split-design.md ``` - [ ] **Step 2: Add a "Status: superseded" header** Insert at the top of the file, immediately after the H1 title line: ```markdown **Status: SUPERSEDED 2026-05-15 by the hardening refactor.** The original question — should left4me have 1, 2, or 3 system users — is now answered: **2 users (current state) is correct.** The defenses that motivated a 3-user split (DB readability from srcds, cross-server ptrace, same-uid /proc visibility, web-side reach into gameserver state) are closed by the systemd hardening composition landed in commit ``: - `PrivateUsers=true` blocks cross-uid ptrace at the kernel level. - `PrivatePIDs=true` hides peer processes even when uids match. - `TemporaryFileSystem=` + minimal binds hide the DB and web.env from srcds entirely. - `SystemCallFilter=~@debug` + empty `CapabilityBoundingSet=` block ptrace at the syscall layer. The residual filesystem-ACL surface (DB at `0640 root:left4me`, web.env same) is a separate concern: a uid split would close it via kernel ACLs, but for the current deployment shape it's covered by the systemd-imposed FS view. If the deployment shape changes (multi-tenant host, shell logins as the service uids, additional services running as `left4me` outside these units) the uid split should be revisited. The original content of this spec is preserved below for context. --- ``` (Replace `` with the actual hash once the commits land — Task 10 will sweep this.) - [ ] **Step 3: Commit** ```bash cd ~/Projekte/left4me git add docs/superpowers/specs/2026-05-15-user-uid-split-design.md git commit -m "$(cat <<'EOF' spec(user-uid-split): mark superseded by the hardening refactor The 1/2/3-user question is answered: stay at 2 (left4me + l4d2-sandbox). The defenses that motivated a 3-user split (cross-uid ptrace, cross-server contamination, web-side reach into gameserver state, DB/env exposure to srcds) are closed by the systemd hardening composition: PrivateUsers + PrivatePIDs + TemporaryFileSystem + SystemCallFilter=~@debug + empty CapabilityBoundingSet. The residual filesystem-ACL surface (mode 0640 root:left4me on DB and web.env) is noted as a separate concern — covered for the current deployment shape, revisit if shape changes. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 10: Cross-repo push (operator-gated) **Repos:** both **Pre-condition:** all prior tasks committed locally in both repos. - [ ] **Step 1: Verify clean state in both repos** Run: ```bash cd ~/Projekte/left4me && git status && git log --oneline origin/master..HEAD echo "---" cd ~/Projekte/ckn-bw && git status && git log --oneline origin/master..HEAD ``` Expected: both repos clean working trees; left4me has 4 new commits (Tasks 7, 8, 9, + possibly the Task 1 emitter-doc update), ckn-bw has 4 new commits (Tasks 2, 3, 4, 5). - [ ] **Step 2: Confirm with operator before push** Pause and confirm with the operator that pushing is the right move. Don't push autonomously — the operator's pattern in earlier sessions has been to commit locally and push manually. - [ ] **Step 3: If approved, push both repos** ```bash cd ~/Projekte/left4me && git push origin master cd ~/Projekte/ckn-bw && git push origin master ``` - [ ] **Step 4: Sweep the placeholder `` in `2026-05-15-user-uid-split-design.md`** After the push, look up the actual refactor commit hash (the most recent left4me commit) and replace `` in the uid-split spec's superseded header. ```bash cd ~/Projekte/left4me REFACTOR_HASH=$(git log --oneline -1 --format='%h') sed -i.bak "s//$REFACTOR_HASH/" docs/superpowers/specs/2026-05-15-user-uid-split-design.md rm docs/superpowers/specs/2026-05-15-user-uid-split-design.md.bak git add docs/superpowers/specs/2026-05-15-user-uid-split-design.md git commit --amend --no-edit git push --force-with-lease origin master # only if not yet pulled elsewhere ``` (Force-push is OK here because the placeholder fill is part of the same commit; only acceptable if no other operator has pulled.) Alternative: leave `` as a follow-up sweep; the spec is human-readable either way. --- ## Task 11: `bw apply ovh.left4me` and verify on the host **Repo:** `~/Projekte/ckn-bw` (executes against `left4.me`) **Pre-condition:** Task 10 completed. - [ ] **Step 1: Apply** ```bash cd ~/Projekte/ckn-bw bw apply ovh.left4me 2>&1 | tee /tmp/bw-apply.log ``` Watch for errors. Expected outcome: the systemd units `left4me-server@1`, `left4me-server@2`, `left4me-web` are reloaded and restarted by `bw`. The sysctl drop-in is installed and reloaded. - [ ] **Step 2: Verify directives present on the host** SSH to `left4.me`: ```bash ssh root@left4.me # or whatever the operator's ssh alias is sudo systemctl cat left4me-server@1.service # Expect: contains every directive from HARDENING_SERVER. Specifically # verify PrivatePIDs=true and SystemCallArchitectures=native x86 appear. sudo systemctl cat left4me-web.service # Expect: ProtectSystem=strict (not full), and the new hardening # directives present (no NoNewPrivileges, no PrivateUsers). sysctl kernel.yama.ptrace_scope # Expect: kernel.yama.ptrace_scope = 2 ``` - [ ] **Step 3: Functional smoke** ```bash # All services active sudo systemctl is-active left4me-server@1 left4me-server@2 left4me-web # Expect: active active active # 30 seconds later, still active (catches respawn-loop regressions) sleep 30 sudo systemctl is-active left4me-server@1 left4me-server@2 left4me-web # systemd-analyze score sudo systemd-analyze security left4me-server@1.service | tail -3 # Expect: a low score (≤ 2.0); recall baseline was 7.5, Test 7 was 1.3 sudo systemd-analyze security left4me-web.service | tail -3 # Expect: ≤ 5.0; baseline 8.7, Test 10 was 4.1 ``` - [ ] **Step 4: Attack-vector re-verification (subset of Test 8, with the corrected probes)** ```bash PID1=$(systemctl show -p MainPID --value left4me-server@1.service) GUNICORN_PID=$(systemctl show -p MainPID --value left4me-web.service) PID2=$(systemctl show -p MainPID --value left4me-server@2.service) # D1.a — DB invisible to srcds sudo nsenter --target $PID1 --mount -- cat /var/lib/left4me/left4me.db 2>&1 | head -1 # Expect: No such file or directory # D1.b — web.env invisible sudo nsenter --target $PID1 --mount -- cat /etc/left4me/web.env 2>&1 | head -1 # Expect: No such file or directory # D2.b — gunicorn invisible via /proc sudo nsenter --target $PID1 --mount --pid -- ls /proc/$GUNICORN_PID 2>&1 | head -1 # Expect: No such file or directory (PrivatePIDs makes the host PID not exist in the namespace) # D5 — cross-instance: server@1 cannot see server@2's PID sudo nsenter --target $PID1 --mount --pid -- ls /proc/$PID2 2>&1 | head -1 # Expect: No such file or directory # Syscall filter compiled correctly (option B from Task 8/step 3 fix) sudo systemd-analyze syscall-filter left4me-server@1.service 2>/dev/null \ | grep -E '^\s*(ptrace|process_vm_)' || echo "blocked (not in allow list)" # Expect: "blocked (not in allow list)" ``` - [ ] **Step 5: Smoke against the web UI** Manually: - Open the web UI (whatever URL the operator uses for left4.me). - Log in. - Start/stop a server (exercises the sudo path). - View live logs for a server. - Trigger an overlay rebuild for a script overlay (exercises the sandbox). Confirm everything works. If anything breaks, the fix is to identify which directive caused it via journalctl, narrow the filter, and iterate. - [ ] **Step 6: Record on-host state** If something breaks: file a follow-up note in the test plan's Results section. If clean: just proceed. - [ ] **Step 7: No commit needed for this task** (host verification only) --- ## Task 12: Apt-remove test tooling from `left4.me` **Repo:** none (host change) **Pre-condition:** Task 11 verified. - [ ] **Step 1: Confirm tooling is unused** SSH to `left4.me` and confirm no live processes need gdb/libseccomp/seccomp: ```bash which gdb # Expect: /usr/bin/gdb (still installed) sudo apt list --installed 2>/dev/null | grep -E '^(gdb|libseccomp-dev|seccomp)/' # Expect: list of those packages ``` - [ ] **Step 2: Remove** ```bash sudo apt remove --purge -y gdb libseccomp-dev seccomp sudo apt autoremove -y ``` - [ ] **Step 3: Verify clean** ```bash which gdb 2>&1 # Expect: empty (gdb not in path) sudo apt list --installed 2>/dev/null | grep -E '^(gdb|libseccomp-dev|seccomp)/' # Expect: empty ``` - [ ] **Step 4: Verify services still happy** ```bash sudo systemctl is-active left4me-server@1 left4me-server@2 left4me-web # Expect: active active active ``` - [ ] **Step 5: No commit needed** (host change only; if future operator wants reproducibility, add the removal to ckn-bw's `pkg_apt` exclusion list — but defer that decision) --- ## Plan complete After Task 12, write a final session-handoff at `docs/superpowers/specs/2026-05-15-session-handoff.md` (overwrite the existing executor handoff) summarizing: - Refactor landed (commit hashes both repos) - All attack vectors still blocked post-deploy - uid-split spec marked superseded - Next session: build-overlay-unit refactor or the deferred drop-in reshape, operator's call Commit + push the handoff. ## Self-review checklist After completing all tasks, verify against the design spec: - [ ] All directives from HARDENING_SERVER appear in the deployed `left4me-server@.service` (per Task 11/Step 2). - [ ] All directives from HARDENING_WEB appear in the deployed `left4me-web.service`. - [ ] PrivatePIDs=true and SystemCallArchitectures=native x86 present on server@. (Test amendments.) - [ ] MemoryDenyWriteExecute=true absent everywhere. - [ ] SocketBindAllow=udp:27000-27999 + tcp:27000-27999 on server@. - [ ] `kernel.yama.ptrace_scope=2` on the host. - [ ] Reference units in deploy/files annotated with per-directive comments. - [ ] Test plan's four bugs fixed. - [ ] uid-split spec marked superseded. - [ ] gdb + seccomp + libseccomp-dev removed from left4.me. - [ ] All three units active and stable. - [ ] Test 8 D1.a, D1.b, D2.b, D5 vectors all blocked.