Compare commits
No commits in common. "a95a7e20e207d7ada9d59c2f2367171cbde61f3a" and "f3fe49c60e9d235196bcf892f3a54d77d5f22a67" have entirely different histories.
a95a7e20e2
...
f3fe49c60e
6 changed files with 292 additions and 286 deletions
|
|
@ -36,74 +36,27 @@ from defaults. None of these need to be declared per-node.
|
||||||
|
|
||||||
## What this bundle does
|
## What this bundle does
|
||||||
|
|
||||||
The bundle delivers to `ovh.left4me` a mix of:
|
- Creates system user `left4me` (uid/gid 980, home `/var/lib/left4me`,
|
||||||
|
mode 0755) — same uid hosts the web app, gameservers, and the
|
||||||
### Target-side symlinks into the left4me checkout
|
script-overlay sandbox unit (which drops privileges via systemd-run
|
||||||
|
with a fully hardened transient service).
|
||||||
After `git_deploy:/opt/left4me/src` (root-owned — left4me cannot rewrite
|
- Drops privileged helpers under `/usr/local/libexec/left4me/`
|
||||||
its own deployment artifacts at runtime), ckn-bw creates symlinks from
|
(`left4me-systemctl`, `left4me-journalctl`, `left4me-overlay`,
|
||||||
canonical on-host paths into the checkout:
|
`left4me-script-sandbox`) plus a tight sudoers file (validated with
|
||||||
|
`visudo -cf` before install).
|
||||||
| On-host path | Source in checkout |
|
- `git_deploy`s the left4me repo to `/opt/left4me/src`, builds a venv at
|
||||||
|---|---|
|
`/opt/left4me/.venv`, `pip install -e`s both `l4d2host` and `l4d2web`,
|
||||||
| `/etc/sudoers.d/left4me` | `deploy/files/etc/sudoers.d/left4me` |
|
runs `alembic upgrade head` and `flask seed-script-overlays`, then
|
||||||
| `/etc/sysctl.d/99-left4me.conf` | `deploy/files/etc/sysctl.d/99-left4me.conf` |
|
enables `left4me-web.service`.
|
||||||
| `/etc/systemd/system/left4me-web.service.d/10-hardening.conf` | `deploy/files/etc/systemd/system/left4me-web.service.d/10-hardening.conf` |
|
- Emits four systemd units via `systemd/units` metadata (consumed by
|
||||||
| `/etc/systemd/system/left4me-server@.service.d/10-hardening.conf` | `deploy/files/etc/systemd/system/left4me-server@.service.d/10-hardening.conf` |
|
`bundles/systemd/`):
|
||||||
| `/usr/local/libexec/left4me/{left4me-overlay,left4me-systemctl,left4me-journalctl,left4me-script-sandbox}` | `deploy/scripts/libexec/*` |
|
- `left4me-web.service` — gunicorn on `127.0.0.1:8000` (TLS terminates upstream).
|
||||||
| `/usr/local/sbin/left4me` | `deploy/scripts/sbin/left4me` |
|
- `left4me-server@.service` — per-instance srcds template, started on
|
||||||
|
demand by the web app via the `left4me-systemctl` helper.
|
||||||
The hardening drop-ins and sudoers are the application's own security
|
- `l4d2-game.slice` / `l4d2-build.slice` — cgroup slices for the
|
||||||
knowledge — they live in the left4me repo and are version-controlled there.
|
perf-baseline (CPU/IO weights, memory caps).
|
||||||
The privileged helpers are also application code. The symlink pattern
|
|
||||||
lets bw manage placement without duplicating content.
|
|
||||||
|
|
||||||
Design rationale:
|
|
||||||
`left4me/docs/superpowers/specs/2026-05-15-deployment-responsibility-design.md`.
|
|
||||||
|
|
||||||
### Reactor-emitted units (per-host shape)
|
|
||||||
|
|
||||||
Via `systemd/units` metadata in `metadata.py` (consumed by `bundles/systemd/`):
|
|
||||||
|
|
||||||
- `left4me-web.service` — gunicorn on `127.0.0.1:8000`; worker/thread
|
|
||||||
counts from `web.env.mako`. TLS terminates upstream.
|
|
||||||
- `left4me-server@.service` — per-instance srcds template; `SocketBindAllow=`
|
|
||||||
ranges from metadata.
|
|
||||||
- `l4d2-game.slice` / `l4d2-build.slice` — cgroup slices with per-host
|
|
||||||
`AllowedCPUs=` from `left4me/system_cpus`.
|
|
||||||
- `system.slice.d/99-left4me-cpuset.conf` + `user.slice.d/99-left4me-cpuset.conf`
|
|
||||||
— host CPU-set drop-ins, same source.
|
|
||||||
|
|
||||||
### bw `files{}` — templated env files
|
|
||||||
|
|
||||||
- `host.env.mako` → `/etc/left4me/host.env`
|
|
||||||
- `web.env.mako` → `/etc/left4me/web.env`
|
|
||||||
- `sandbox-resolv.conf` → `/etc/left4me/sandbox-resolv.conf`
|
|
||||||
|
|
||||||
### Action chains — deploy lifecycle
|
|
||||||
|
|
||||||
- `git_deploy` → `pip_install` (non-editable; setuptools writes egg-info to
|
|
||||||
a left4me-writable tempdir) → `alembic_upgrade` → `seed_overlays` + web restart.
|
|
||||||
- Idempotent gates: `chmod-sudoers` (0440 root:root), `chmod-scripts` (0755 root:root).
|
|
||||||
- Post-git-deploy reloads: `systemctl daemon-reload`, `sysctl --system`.
|
|
||||||
- Post-apply self-test: `verify-hardening-dropins` (asserts the drop-ins are
|
|
||||||
loaded by the live units before declaring apply done).
|
|
||||||
|
|
||||||
### System user
|
|
||||||
|
|
||||||
`left4me` (uid/gid 980, home `/var/lib/left4me`, mode 0755) — the same uid
|
|
||||||
hosts the web app, gameservers, and the script-overlay sandbox unit (which
|
|
||||||
drops privileges via systemd-run with a fully hardened transient service).
|
|
||||||
Runtime mutable state lives under `/var/lib/left4me/`; `/opt/left4me/`
|
|
||||||
stays as a root-owned deploy-artifact root.
|
|
||||||
|
|
||||||
### nftables / nginx / monitoring
|
|
||||||
|
|
||||||
- Contributes uid-based DSCP/priority marks for srcds UDP egress to
|
- Contributes uid-based DSCP/priority marks for srcds UDP egress to
|
||||||
`nftables/output` (via `defaults`).
|
`nftables/output` (via `defaults`).
|
||||||
- `derived_from_domain` reactor emits the corresponding `nginx/vhosts`,
|
|
||||||
`letsencrypt/domains`, and `monitoring/services/left4me-web` (HTTPS
|
|
||||||
health check).
|
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
LEFT4ME_ROOT=/var/lib/left4me
|
LEFT4ME_ROOT=/var/lib/left4me
|
||||||
# l4d2host invokes steamcmd by absolute path — bypasses PATH lookup so the
|
# l4d2host invokes steamcmd by absolute path — bypasses PATH lookup so the
|
||||||
# script's `cd "$(dirname "$0")"` resolves next to the real install dir.
|
# script's `cd "$(dirname "$0")"` resolves next to the real install dir.
|
||||||
LEFT4ME_STEAMCMD=/var/lib/left4me/steam/steamcmd.sh
|
LEFT4ME_STEAMCMD=/opt/left4me/steam/steamcmd.sh
|
||||||
|
|
|
||||||
5
bundles/left4me/files/etc/sudoers.d/left4me
Normal file
5
bundles/left4me/files/etc/sudoers.d/left4me
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Defaults:left4me !requiretty
|
||||||
|
left4me ALL=(root) NOPASSWD: /usr/local/libexec/left4me/left4me-systemctl *
|
||||||
|
left4me ALL=(root) NOPASSWD: /usr/local/libexec/left4me/left4me-journalctl *
|
||||||
|
left4me ALL=(root) NOPASSWD: /usr/local/libexec/left4me/left4me-overlay mount *, /usr/local/libexec/left4me/left4me-overlay umount *
|
||||||
|
left4me ALL=(root) NOPASSWD: /usr/local/libexec/left4me/left4me-script-sandbox
|
||||||
36
bundles/left4me/files/etc/sysctl.d/99-left4me.conf
Normal file
36
bundles/left4me/files/etc/sysctl.d/99-left4me.conf
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Host-side perf baseline for left4me — see
|
||||||
|
# docs/superpowers/specs/2026-05-09-l4d2-server-host-perf-baseline-design.md
|
||||||
|
#
|
||||||
|
# UDP socket buffers: distro defaults of ~128 KiB are too small for sustained
|
||||||
|
# Source-engine UDP across multiple instances. 8 MiB matches the standard
|
||||||
|
# 1 Gbit recommendation; rmem_default/wmem_default protect sockets that don't
|
||||||
|
# explicitly enlarge their buffers.
|
||||||
|
net.core.rmem_max = 8388608
|
||||||
|
net.core.wmem_max = 8388608
|
||||||
|
net.core.rmem_default = 524288
|
||||||
|
net.core.wmem_default = 524288
|
||||||
|
|
||||||
|
# Kernel softirq UDP path: the per-CPU backlog queue starts dropping packets
|
||||||
|
# at the default 1000 under multi-instance burst; 5000 absorbs realistic peaks.
|
||||||
|
# netdev_budget = 600 gives softirq more drain headroom per pass.
|
||||||
|
net.core.netdev_max_backlog = 5000
|
||||||
|
net.core.netdev_budget = 600
|
||||||
|
|
||||||
|
# Latency-sensitive default: avoid swap unless the box is really under
|
||||||
|
# pressure. Harmless on swapless hosts.
|
||||||
|
vm.swappiness = 10
|
||||||
|
|
||||||
|
# Per-socket UDP buffer floors: protect game-server sockets that don't bump
|
||||||
|
# their own SO_RCVBUF/SO_SNDBUF when softirq drains lag briefly.
|
||||||
|
net.ipv4.udp_rmem_min = 16384
|
||||||
|
net.ipv4.udp_wmem_min = 16384
|
||||||
|
|
||||||
|
# Default qdisc for ifaces we don't explicitly shape with CAKE. Debian Trixie
|
||||||
|
# already defaults to fq_codel; setting it explicitly is belt-and-suspenders
|
||||||
|
# and survives kernel-default churn.
|
||||||
|
net.core.default_qdisc = fq_codel
|
||||||
|
|
||||||
|
# TCP congestion control: BBR for any bulk TCP egress on the host (admin SSH,
|
||||||
|
# backups, package fetches, web-app responses) so a long flow does not push
|
||||||
|
# the bottleneck queue ahead of game UDP. UDP srcds is unaffected.
|
||||||
|
net.ipv4.tcp_congestion_control = bbr
|
||||||
|
|
@ -6,26 +6,12 @@
|
||||||
|
|
||||||
directories = {
|
directories = {
|
||||||
'/opt/left4me': {
|
'/opt/left4me': {
|
||||||
# Deploy-artifact root. Only /opt/left4me/src lives here; runtime
|
'owner': 'left4me',
|
||||||
# state (.venv, steamcmd) lives under /var/lib/left4me/. Root-owned
|
'group': 'left4me',
|
||||||
# so left4me cannot drop new files alongside src/ (e.g. an attacker
|
|
||||||
# with web-compromise can't plant a 'scripts.d/' loaded by future
|
|
||||||
# deploy logic).
|
|
||||||
'owner': 'root',
|
|
||||||
'group': 'root',
|
|
||||||
'mode': '0755',
|
|
||||||
},
|
},
|
||||||
'/opt/left4me/src': {
|
'/opt/left4me/src': {
|
||||||
# Source checkout. Root-owned because the production install model
|
'owner': 'left4me',
|
||||||
# is non-editable: pip_install copies the source to a left4me-owned
|
'group': 'left4me',
|
||||||
# tempdir before building, so the source tree on disk is never
|
|
||||||
# mutated at runtime and left4me only needs read access (which
|
|
||||||
# world-readable bits provide). Keeps left4me from being able to
|
|
||||||
# rewrite its own future hardening drop-ins / unit files under
|
|
||||||
# /opt/left4me/src/deploy/ (target-side symlink model in the
|
|
||||||
# deployment-responsibility reshape).
|
|
||||||
'owner': 'root',
|
|
||||||
'group': 'root',
|
|
||||||
},
|
},
|
||||||
'/etc/left4me': {
|
'/etc/left4me': {
|
||||||
'owner': 'root',
|
'owner': 'root',
|
||||||
|
|
@ -47,21 +33,12 @@ directories = {
|
||||||
'/var/lib/left4me/runtime': {'owner': 'left4me', 'group': 'left4me'},
|
'/var/lib/left4me/runtime': {'owner': 'left4me', 'group': 'left4me'},
|
||||||
'/var/lib/left4me/workshop_cache': {'owner': 'left4me', 'group': 'left4me'},
|
'/var/lib/left4me/workshop_cache': {'owner': 'left4me', 'group': 'left4me'},
|
||||||
'/var/lib/left4me/tmp': {'owner': 'left4me', 'group': 'left4me'},
|
'/var/lib/left4me/tmp': {'owner': 'left4me', 'group': 'left4me'},
|
||||||
'/var/lib/left4me/steam': {'owner': 'left4me', 'group': 'left4me'},
|
'/opt/left4me/steam': {'owner': 'left4me', 'group': 'left4me'},
|
||||||
# Note: the venv (/var/lib/left4me/.venv) is created by the
|
|
||||||
# left4me_create_venv action; declaring it here too would race with
|
|
||||||
# `python -m venv` which expects to create the directory itself.
|
|
||||||
'/usr/local/libexec/left4me': {
|
'/usr/local/libexec/left4me': {
|
||||||
'owner': 'root',
|
'owner': 'root',
|
||||||
'group': 'root',
|
'group': 'root',
|
||||||
'mode': '0755',
|
'mode': '0755',
|
||||||
},
|
},
|
||||||
'/etc/systemd/system/left4me-web.service.d': {
|
|
||||||
'owner': 'root', 'group': 'root', 'mode': '0755',
|
|
||||||
},
|
|
||||||
'/etc/systemd/system/left4me-server@.service.d': {
|
|
||||||
'owner': 'root', 'group': 'root', 'mode': '0755',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
groups = {
|
groups = {
|
||||||
|
|
@ -82,11 +59,11 @@ users = {
|
||||||
# (981 — formerly l4d2-sandbox — was collapsed into 980 on 2026-05-15;
|
# (981 — formerly l4d2-sandbox — was collapsed into 980 on 2026-05-15;
|
||||||
# see left4me/docs/superpowers/plans/2026-05-15-uid-collapse.md.)
|
# see left4me/docs/superpowers/plans/2026-05-15-uid-collapse.md.)
|
||||||
|
|
||||||
# Privileged helpers are delivered via target-side symlinks (see the
|
# Privileged helpers are installed by the `install_left4me_scripts`
|
||||||
# `symlinks` dict below) pointing into the left4me checkout at
|
# action (below) directly from the left4me git checkout at
|
||||||
# `/opt/left4me/src/deploy/scripts/{libexec,sbin}/`. No verbatim copy
|
# `/opt/left4me/src/scripts/{libexec,sbin}/` — no verbatim copy in this
|
||||||
# in this bundle's files/ tree. Sudoers (further below) lists the
|
# bundle's files/ tree. Sudoers (further below) lists the specific
|
||||||
# specific paths that left4me may invoke as root NOPASSWD.
|
# paths that left4me may invoke as root NOPASSWD.
|
||||||
|
|
||||||
files = {
|
files = {
|
||||||
'/etc/left4me/sandbox-resolv.conf': {
|
'/etc/left4me/sandbox-resolv.conf': {
|
||||||
|
|
@ -95,6 +72,22 @@ files = {
|
||||||
'owner': 'root',
|
'owner': 'root',
|
||||||
'group': 'root',
|
'group': 'root',
|
||||||
},
|
},
|
||||||
|
'/etc/sudoers.d/left4me': {
|
||||||
|
'source': 'etc/sudoers.d/left4me',
|
||||||
|
'mode': '0440',
|
||||||
|
'owner': 'root',
|
||||||
|
'group': 'root',
|
||||||
|
'test_with': 'visudo -cf {}',
|
||||||
|
},
|
||||||
|
'/etc/sysctl.d/99-left4me.conf': {
|
||||||
|
'source': 'etc/sysctl.d/99-left4me.conf',
|
||||||
|
'mode': '0644',
|
||||||
|
'owner': 'root',
|
||||||
|
'group': 'root',
|
||||||
|
'triggers': [
|
||||||
|
'action:left4me_sysctl_reload',
|
||||||
|
],
|
||||||
|
},
|
||||||
'/etc/left4me/host.env': {
|
'/etc/left4me/host.env': {
|
||||||
'source': 'etc/left4me/host.env.mako',
|
'source': 'etc/left4me/host.env.mako',
|
||||||
'content_type': 'mako',
|
'content_type': 'mako',
|
||||||
|
|
@ -120,132 +113,11 @@ files = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
symlinks = {
|
|
||||||
'/etc/sysctl.d/99-left4me.conf': {
|
|
||||||
'target': '/opt/left4me/src/deploy/files/etc/sysctl.d/99-left4me.conf',
|
|
||||||
'owner': 'root',
|
|
||||||
'group': 'root',
|
|
||||||
'needs': [
|
|
||||||
'git_deploy:/opt/left4me/src',
|
|
||||||
],
|
|
||||||
'triggers': [
|
|
||||||
'action:left4me_sysctl_reload',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'/etc/systemd/system/left4me-web.service.d/10-hardening.conf': {
|
|
||||||
'target': '/opt/left4me/src/deploy/files/etc/systemd/system/left4me-web.service.d/10-hardening.conf',
|
|
||||||
'owner': 'root', 'group': 'root',
|
|
||||||
'needs': [
|
|
||||||
'directory:/etc/systemd/system/left4me-web.service.d',
|
|
||||||
'git_deploy:/opt/left4me/src',
|
|
||||||
],
|
|
||||||
'triggers': [
|
|
||||||
'action:left4me_daemon_reload',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'/etc/systemd/system/left4me-server@.service.d/10-hardening.conf': {
|
|
||||||
'target': '/opt/left4me/src/deploy/files/etc/systemd/system/left4me-server@.service.d/10-hardening.conf',
|
|
||||||
'owner': 'root', 'group': 'root',
|
|
||||||
'needs': [
|
|
||||||
'directory:/etc/systemd/system/left4me-server@.service.d',
|
|
||||||
'git_deploy:/opt/left4me/src',
|
|
||||||
],
|
|
||||||
'triggers': [
|
|
||||||
'action:left4me_daemon_reload',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'/etc/sudoers.d/left4me': {
|
|
||||||
'target': '/opt/left4me/src/deploy/files/etc/sudoers.d/left4me',
|
|
||||||
'owner': 'root', 'group': 'root',
|
|
||||||
'needs': [
|
|
||||||
'action:left4me_chmod_sudoers',
|
|
||||||
'git_deploy:/opt/left4me/src',
|
|
||||||
],
|
|
||||||
# sudo follows symlinks; with the target file at root:root 0440
|
|
||||||
# in a root-owned source tree, sudo accepts it. No daemon-reload
|
|
||||||
# equivalent — sudo re-reads /etc/sudoers.d/ on each invocation.
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper script source paths (in left4me's checkout) → deployed-form paths.
|
|
||||||
# Each gets a symlink item merged into the symlinks dict above.
|
|
||||||
_LEFT4ME_LIBEXEC_SCRIPTS = (
|
|
||||||
'left4me-overlay',
|
|
||||||
'left4me-systemctl',
|
|
||||||
'left4me-journalctl',
|
|
||||||
'left4me-script-sandbox',
|
|
||||||
)
|
|
||||||
_LEFT4ME_SBIN_SCRIPTS = (
|
|
||||||
'left4me',
|
|
||||||
)
|
|
||||||
|
|
||||||
for _script in _LEFT4ME_LIBEXEC_SCRIPTS:
|
|
||||||
symlinks[f'/usr/local/libexec/left4me/{_script}'] = {
|
|
||||||
'target': f'/opt/left4me/src/deploy/scripts/libexec/{_script}',
|
|
||||||
'owner': 'root', 'group': 'root',
|
|
||||||
'needs': [
|
|
||||||
'directory:/usr/local/libexec/left4me',
|
|
||||||
'action:left4me_chmod_scripts',
|
|
||||||
'git_deploy:/opt/left4me/src',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
for _script in _LEFT4ME_SBIN_SCRIPTS:
|
|
||||||
symlinks[f'/usr/local/sbin/{_script}'] = {
|
|
||||||
'target': f'/opt/left4me/src/deploy/scripts/sbin/{_script}',
|
|
||||||
'owner': 'root', 'group': 'root',
|
|
||||||
'needs': [
|
|
||||||
'action:left4me_chmod_scripts',
|
|
||||||
'git_deploy:/opt/left4me/src',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
'left4me_sysctl_reload': {
|
'left4me_sysctl_reload': {
|
||||||
'command': 'sysctl --system >/dev/null',
|
'command': 'sysctl --system >/dev/null',
|
||||||
'triggered': True,
|
'triggered': True,
|
||||||
},
|
},
|
||||||
'left4me_daemon_reload': {
|
|
||||||
'command': 'systemctl daemon-reload',
|
|
||||||
'triggered': True,
|
|
||||||
'cascade_skip': False,
|
|
||||||
},
|
|
||||||
'left4me_verify_hardening_dropins_loaded': {
|
|
||||||
# Post-apply self-test: confirm systemd actually picked up the
|
|
||||||
# hardening drop-ins we shipped via symlink. Catches the failure
|
|
||||||
# mode where the symlink lands but daemon-reload didn't take or
|
|
||||||
# someone manually unlinked the drop-in. For the gameserver template
|
|
||||||
# we query an imaginary instance — systemd resolves drop-in paths
|
|
||||||
# for `name@instance.service` against the template (`name@.service.d/`),
|
|
||||||
# so the instance need not exist or ever have run.
|
|
||||||
'command': (
|
|
||||||
'systemctl show left4me-server@verify.service -p DropInPaths --value '
|
|
||||||
'| tr " " "\\n" '
|
|
||||||
'| grep -qx /etc/systemd/system/left4me-server@.service.d/10-hardening.conf '
|
|
||||||
'&& '
|
|
||||||
'systemctl show left4me-web.service -p DropInPaths --value '
|
|
||||||
'| tr " " "\\n" '
|
|
||||||
'| grep -qx /etc/systemd/system/left4me-web.service.d/10-hardening.conf'
|
|
||||||
),
|
|
||||||
'cascade_skip': False,
|
|
||||||
'needs': [
|
|
||||||
'action:left4me_daemon_reload',
|
|
||||||
'symlink:/etc/systemd/system/left4me-web.service.d/10-hardening.conf',
|
|
||||||
'symlink:/etc/systemd/system/left4me-server@.service.d/10-hardening.conf',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'left4me_chmod_sudoers': {
|
|
||||||
# sudo refuses sudoers.d entries that aren't 0440 (or 0400) root:root.
|
|
||||||
# git_deploy extracts as root with the in-repo file mode; this action
|
|
||||||
# is belt-and-braces in case the repo mode drifts. Idempotent via
|
|
||||||
# the `unless` gate.
|
|
||||||
'command': 'chmod 0440 /opt/left4me/src/deploy/files/etc/sudoers.d/left4me',
|
|
||||||
'unless': 'test "$(stat -c %a /opt/left4me/src/deploy/files/etc/sudoers.d/left4me)" = "440"',
|
|
||||||
'cascade_skip': False,
|
|
||||||
'needs': [
|
|
||||||
'git_deploy:/opt/left4me/src',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'left4me_dpkg_add_i386_arch': {
|
'left4me_dpkg_add_i386_arch': {
|
||||||
# steamcmd is 32-bit and pulls libc6:i386 + lib32z1 from the i386 arch.
|
# steamcmd is 32-bit and pulls libc6:i386 + lib32z1 from the i386 arch.
|
||||||
# apt-get update is part of this action because newly-added foreign
|
# apt-get update is part of this action because newly-added foreign
|
||||||
|
|
@ -261,15 +133,15 @@ actions = {
|
||||||
# tarball version from bw isn't useful.
|
# tarball version from bw isn't useful.
|
||||||
'command': (
|
'command': (
|
||||||
'sudo -u left4me sh -c "'
|
'sudo -u left4me sh -c "'
|
||||||
'cd /var/lib/left4me/steam && '
|
'cd /opt/left4me/steam && '
|
||||||
'curl -fsSL https://media.steampowered.com/installer/steamcmd_linux.tar.gz | '
|
'curl -fsSL https://media.steampowered.com/installer/steamcmd_linux.tar.gz | '
|
||||||
'tar -xz'
|
'tar -xz'
|
||||||
'"'
|
'"'
|
||||||
),
|
),
|
||||||
'unless': 'test -x /var/lib/left4me/steam/steamcmd.sh',
|
'unless': 'test -x /opt/left4me/steam/steamcmd.sh',
|
||||||
'cascade_skip': False,
|
'cascade_skip': False,
|
||||||
'needs': [
|
'needs': [
|
||||||
'directory:/var/lib/left4me/steam',
|
'directory:/opt/left4me/steam',
|
||||||
'pkg_apt:curl',
|
'pkg_apt:curl',
|
||||||
'pkg_apt:libc6_i386', # bw pkg_apt convention: _ → :
|
'pkg_apt:libc6_i386', # bw pkg_apt convention: _ → :
|
||||||
'pkg_apt:lib32z1',
|
'pkg_apt:lib32z1',
|
||||||
|
|
@ -287,50 +159,71 @@ git_deploy = {
|
||||||
'repo': node.metadata.get('left4me/git_url'),
|
'repo': node.metadata.get('left4me/git_url'),
|
||||||
'rev': node.metadata.get('left4me/git_branch'),
|
'rev': node.metadata.get('left4me/git_branch'),
|
||||||
'triggers': [
|
'triggers': [
|
||||||
# Rebuild + reinstall the packages whenever the checkout
|
# On a code-update apply, refresh the DB schema. pip_install
|
||||||
# changes. pip_install does its own out-of-tree build (copies
|
# would have triggered alembic in the create_venv path, but on
|
||||||
# source to a left4me-owned tempdir before invoking pip), so
|
# a normal apply pip_install's `unless` skips (packages still
|
||||||
# the source tree itself stays root-owned and untouched.
|
# importable from the previous editable install), and that
|
||||||
# pip_install cascades into alembic_upgrade → web restart.
|
# would leave alembic_upgrade dormant. Wiring git_deploy →
|
||||||
'action:left4me_pip_install',
|
# alembic directly ensures new migrations land whenever new
|
||||||
# alembic upgrade head is idempotent — keeping it as a direct
|
# code lands. alembic upgrade head is idempotent (no-op when
|
||||||
# trigger off git_deploy is belt-and-braces in case the
|
# already at head), so this is safe to fire on every code
|
||||||
# pip_install cascade is ever short-circuited.
|
# update; the seed_overlays + service:restart cascade off
|
||||||
|
# alembic also covers picking up the new code in gunicorn.
|
||||||
'action:left4me_alembic_upgrade',
|
'action:left4me_alembic_upgrade',
|
||||||
# Reload systemd unit definitions whenever the checkout changes;
|
# Privileged-helper scripts: reinstall from the new checkout
|
||||||
# handles updates to hardening drop-in content without requiring
|
# into /usr/local/{libexec,sbin}/ as root-owned. No-op when
|
||||||
# a symlink change.
|
# the checkout didn't actually change (action is triggered).
|
||||||
'action:left4me_daemon_reload',
|
'action:install_left4me_scripts',
|
||||||
],
|
],
|
||||||
|
# chown_src and pip_install are NOT in triggers — they run every
|
||||||
|
# apply gated by their own `unless` guards, which makes the chain
|
||||||
|
# self-healing after a partial failure. (Items in a triggers list
|
||||||
|
# must be triggered:True, which would lose that property.)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
actions['left4me_chmod_scripts'] = {
|
actions['install_left4me_scripts'] = {
|
||||||
# sudo invokes the helpers by absolute path under /usr/local/...;
|
# Copy privileged scripts from the deployed left4me checkout into
|
||||||
# those resolve to the checkout via the symlinks above. The target
|
# /usr/local/{libexec,sbin}/ as root:root 0755. Source of truth for
|
||||||
# files must be executable (mode 0755). git_deploy extracts with
|
# the file content is left4me's scripts/{libexec,sbin}/ tree (these
|
||||||
# the in-repo file modes; this action is belt-and-braces in case
|
# are application code, not deploy artifacts; left4me's deploy/ is
|
||||||
# any helper's repo mode regresses to 0644.
|
# reference material only). The two install globs map source dirs
|
||||||
|
# 1:1 to deploy targets. Triggered only on git_deploy updates so a
|
||||||
|
# no-op apply doesn't re-copy.
|
||||||
'command': (
|
'command': (
|
||||||
'chmod 0755 '
|
'install -m 0755 -o root -g root -t /usr/local/libexec/left4me/ '
|
||||||
'/opt/left4me/src/deploy/scripts/libexec/* '
|
'/opt/left4me/src/scripts/libexec/*; '
|
||||||
'/opt/left4me/src/deploy/scripts/sbin/*'
|
'install -m 0755 -o root -g root -t /usr/local/sbin/ '
|
||||||
),
|
'/opt/left4me/src/scripts/sbin/*'
|
||||||
'unless': (
|
|
||||||
'! find /opt/left4me/src/deploy/scripts -type f \\! -perm 755 -print -quit 2>/dev/null | grep -q .'
|
|
||||||
),
|
),
|
||||||
|
'triggered': True,
|
||||||
'cascade_skip': False,
|
'cascade_skip': False,
|
||||||
'needs': [
|
'needs': [
|
||||||
'git_deploy:/opt/left4me/src',
|
'git_deploy:/opt/left4me/src',
|
||||||
|
'directory:/usr/local/libexec/left4me',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
actions['left4me_chown_src'] = {
|
||||||
|
# Runs every apply (cheap — chown -R on a small tree). Self-heals
|
||||||
|
# whenever git_deploy extracts a new tarball as root-owned files.
|
||||||
|
# Not in any triggers list so doesn't need triggered:True.
|
||||||
|
'command': 'chown -R left4me:left4me /opt/left4me/src',
|
||||||
|
'unless': 'test -z "$(find /opt/left4me/src \\! -user left4me -print -quit 2>/dev/null)"',
|
||||||
|
'cascade_skip': False,
|
||||||
|
'needs': [
|
||||||
|
'git_deploy:/opt/left4me/src',
|
||||||
|
'user:left4me',
|
||||||
|
'group:left4me',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
actions['left4me_create_venv'] = {
|
actions['left4me_create_venv'] = {
|
||||||
'command': 'sudo -u left4me /usr/bin/python3 -m venv /var/lib/left4me/.venv',
|
'command': 'sudo -u left4me /usr/bin/python3 -m venv /opt/left4me/.venv',
|
||||||
'unless': 'test -x /var/lib/left4me/.venv/bin/python',
|
'unless': 'test -x /opt/left4me/.venv/bin/python',
|
||||||
'cascade_skip': False,
|
'cascade_skip': False,
|
||||||
'needs': [
|
'needs': [
|
||||||
'directory:/var/lib/left4me',
|
'directory:/opt/left4me',
|
||||||
'pkg_apt:python3-venv',
|
'pkg_apt:python3-venv',
|
||||||
'user:left4me',
|
'user:left4me',
|
||||||
],
|
],
|
||||||
|
|
@ -340,41 +233,29 @@ actions['left4me_create_venv'] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
actions['left4me_pip_upgrade'] = {
|
actions['left4me_pip_upgrade'] = {
|
||||||
'command': 'sudo -u left4me /var/lib/left4me/.venv/bin/python -m pip install --upgrade pip',
|
'command': 'sudo -u left4me /opt/left4me/.venv/bin/python -m pip install --upgrade pip',
|
||||||
'triggered': True,
|
'triggered': True,
|
||||||
'cascade_skip': False,
|
'cascade_skip': False,
|
||||||
'needs': [
|
'needs': [
|
||||||
'pkg_apt:python3-pip',
|
'pkg_apt:python3-pip',
|
||||||
],
|
],
|
||||||
# No triggers — pip_install is driven by git_deploy on actual code
|
# No triggers — pip_install runs on every apply (gated by `unless`)
|
||||||
# updates, not by venv setup. Keeps pip_upgrade scoped to exactly
|
# rather than being chained from here. Keeps pip_upgrade scoped to
|
||||||
# its purpose.
|
# exactly its purpose.
|
||||||
}
|
}
|
||||||
|
|
||||||
actions['left4me_pip_install'] = {
|
actions['left4me_pip_install'] = {
|
||||||
# Non-editable install of l4d2host + l4d2web into the venv. We have
|
# Single pip invocation installs both editable packages from the same
|
||||||
# to copy the source to a left4me-writable tempdir first because
|
# checkout. Runs on every apply: pip install -e is fast on no-op, and
|
||||||
# setuptools.build_meta writes <pkg>.egg-info/ into the source dir
|
# any gate weaker than "egg-info matches pyproject.toml" can mask
|
||||||
# during `get_requires_for_build_wheel`, and the source tree is
|
# script regeneration — e.g. adding [project.scripts] later wouldn't
|
||||||
# root-owned. cp -r is fast (small tree, world-readable), the build
|
# be picked up if `unless` only checks importability.
|
||||||
# itself happens in $tmpdir, and pip installs the resulting wheel
|
'command': 'sudo -u left4me /opt/left4me/.venv/bin/pip install -e /opt/left4me/src/l4d2host -e /opt/left4me/src/l4d2web',
|
||||||
# into /var/lib/left4me/.venv/site-packages. --force-reinstall
|
|
||||||
# because the version string in pyproject.toml (0.1.0) doesn't
|
|
||||||
# change commit-to-commit; without it pip would skip on no-op.
|
|
||||||
# triggered:True so this only fires on actual git_deploy changes
|
|
||||||
# (the cp + build is too heavy to run on every apply).
|
|
||||||
'command': """sudo -u left4me sh -c '
|
|
||||||
set -e
|
|
||||||
tmpdir=$(mktemp -d -t left4me-build-XXXXXX)
|
|
||||||
trap "rm -rf \\"$tmpdir\\"" EXIT
|
|
||||||
cp -r /opt/left4me/src/l4d2host /opt/left4me/src/l4d2web "$tmpdir/"
|
|
||||||
/var/lib/left4me/.venv/bin/pip install --force-reinstall "$tmpdir/l4d2host" "$tmpdir/l4d2web"
|
|
||||||
'""",
|
|
||||||
'triggered': True,
|
|
||||||
'cascade_skip': False,
|
'cascade_skip': False,
|
||||||
'needs': [
|
'needs': [
|
||||||
'git_deploy:/opt/left4me/src',
|
'git_deploy:/opt/left4me/src',
|
||||||
'action:left4me_create_venv',
|
'action:left4me_create_venv',
|
||||||
|
'action:left4me_chown_src',
|
||||||
],
|
],
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'action:left4me_alembic_upgrade',
|
'action:left4me_alembic_upgrade',
|
||||||
|
|
@ -390,7 +271,7 @@ actions['left4me_alembic_upgrade'] = {
|
||||||
'cd /opt/left4me/src/l4d2web && '
|
'cd /opt/left4me/src/l4d2web && '
|
||||||
'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && '
|
'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && '
|
||||||
'env JOB_WORKER_ENABLED=false PYTHONPATH=/opt/left4me/src '
|
'env JOB_WORKER_ENABLED=false PYTHONPATH=/opt/left4me/src '
|
||||||
'/var/lib/left4me/.venv/bin/alembic -c /opt/left4me/src/l4d2web/alembic.ini upgrade head'
|
'/opt/left4me/.venv/bin/alembic -c /opt/left4me/src/l4d2web/alembic.ini upgrade head'
|
||||||
'"'
|
'"'
|
||||||
),
|
),
|
||||||
'triggered': True,
|
'triggered': True,
|
||||||
|
|
@ -412,7 +293,7 @@ actions['left4me_seed_overlays'] = {
|
||||||
'sudo -u left4me sh -c "'
|
'sudo -u left4me sh -c "'
|
||||||
'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && '
|
'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && '
|
||||||
'env JOB_WORKER_ENABLED=false PYTHONPATH=/opt/left4me/src '
|
'env JOB_WORKER_ENABLED=false PYTHONPATH=/opt/left4me/src '
|
||||||
'/var/lib/left4me/.venv/bin/flask --app l4d2web.app:create_app '
|
'/opt/left4me/.venv/bin/flask --app l4d2web.app:create_app '
|
||||||
'seed-script-overlays /opt/left4me/src/examples/script-overlays'
|
'seed-script-overlays /opt/left4me/src/examples/script-overlays'
|
||||||
'"'
|
'"'
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,17 @@ defaults = {
|
||||||
'/etc/left4me',
|
'/etc/left4me',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'sysctl': {
|
||||||
|
# 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
'systemd-timers': {
|
'systemd-timers': {
|
||||||
# Daily re-fetch of Steam Workshop metadata + .vpk downloads for any
|
# Daily re-fetch of Steam Workshop metadata + .vpk downloads for any
|
||||||
# item whose author published an update. The CLI just inserts a
|
# item whose author published an update. The CLI just inserts a
|
||||||
|
|
@ -90,7 +101,7 @@ defaults = {
|
||||||
# Idempotent — a re-fire while a refresh is already queued/running
|
# Idempotent — a re-fire while a refresh is already queued/running
|
||||||
# is a no-op (see l4d2web/cli.py:workshop_refresh).
|
# is a no-op (see l4d2web/cli.py:workshop_refresh).
|
||||||
'left4me-workshop-refresh': {
|
'left4me-workshop-refresh': {
|
||||||
'command': '/var/lib/left4me/.venv/bin/flask --app l4d2web.app:create_app workshop-refresh',
|
'command': '/opt/left4me/.venv/bin/flask --app l4d2web.app:create_app workshop-refresh',
|
||||||
'when': '*-*-* 04:00:00',
|
'when': '*-*-* 04:00:00',
|
||||||
'persistent': True,
|
'persistent': True,
|
||||||
'user': 'left4me',
|
'user': 'left4me',
|
||||||
|
|
@ -108,6 +119,123 @@ defaults = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# ProcSubset=pid is intentionally NOT in COMMON: it hides
|
||||||
|
# /proc/sys/kernel/random/boot_id which journalctl reads at startup,
|
||||||
|
# and the web unit invokes `sudo -n left4me-journalctl ...` to stream
|
||||||
|
# live server logs into the UI. Server unit adds it back in
|
||||||
|
# HARDENING_SERVER (srcds doesn't read journalctl).
|
||||||
|
HARDENING_COMMON = {
|
||||||
|
'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',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gameserver unit: COMMON + sudo-incompatible flags + filesystem
|
||||||
|
# virtualization + i386 amendment + per-instance PID namespace + bound
|
||||||
|
# socket binds.
|
||||||
|
HARDENING_SERVER = {
|
||||||
|
**HARDENING_COMMON,
|
||||||
|
# ProcSubset=pid was here but had to come out: it hides /proc/cpuinfo
|
||||||
|
# and /proc/sys/*, which breaks Source's tier0/cpu.cpp and (downstream)
|
||||||
|
# SteamAPI_Init's "create pipe" step — server then registers as LAN
|
||||||
|
# and rejects external clients with "LAN servers are restricted to
|
||||||
|
# local clients (class C)". PrivatePIDs=true (kernel-level PID
|
||||||
|
# namespace) remains the load-bearing peer-process isolation, and
|
||||||
|
# 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.
|
||||||
|
'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',
|
||||||
|
# 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.
|
||||||
|
'/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
|
||||||
|
# /opt/left4me/steam, so both paths need to be bound back
|
||||||
|
# through TemporaryFileSystem.
|
||||||
|
'/var/lib/left4me/.steam',
|
||||||
|
'/opt/left4me/steam',
|
||||||
|
'/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',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'nginx/vhosts',
|
'nginx/vhosts',
|
||||||
|
|
@ -202,14 +330,14 @@ def systemd_units(metadata):
|
||||||
'WorkingDirectory': '/opt/left4me/src',
|
'WorkingDirectory': '/opt/left4me/src',
|
||||||
'Environment': {
|
'Environment': {
|
||||||
'HOME=/var/lib/left4me',
|
'HOME=/var/lib/left4me',
|
||||||
'PATH=/var/lib/left4me/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
'PATH=/opt/left4me/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||||
},
|
},
|
||||||
'EnvironmentFile': (
|
'EnvironmentFile': (
|
||||||
'/etc/left4me/host.env',
|
'/etc/left4me/host.env',
|
||||||
'/etc/left4me/web.env',
|
'/etc/left4me/web.env',
|
||||||
),
|
),
|
||||||
'ExecStart': (
|
'ExecStart': (
|
||||||
'/var/lib/left4me/.venv/bin/gunicorn '
|
'/opt/left4me/.venv/bin/gunicorn '
|
||||||
f'--workers {workers} --threads {threads} '
|
f'--workers {workers} --threads {threads} '
|
||||||
"--bind 127.0.0.1:8000 'l4d2web.app:create_app()'"
|
"--bind 127.0.0.1:8000 'l4d2web.app:create_app()'"
|
||||||
),
|
),
|
||||||
|
|
@ -221,9 +349,12 @@ def systemd_units(metadata):
|
||||||
# only its instance dir).
|
# only its instance dir).
|
||||||
'ReadWritePaths': '/var/lib/left4me',
|
'ReadWritePaths': '/var/lib/left4me',
|
||||||
|
|
||||||
# Hardening profile delivered via
|
# Hardening profile — see HARDENING_WEB constant near top of
|
||||||
# /etc/systemd/system/left4me-web.service.d/10-hardening.conf
|
# this file. NoNewPrivileges intentionally NOT set: workers
|
||||||
# (target-side symlink into left4me/deploy/files/, owned by left4me).
|
# 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': {
|
'Install': {
|
||||||
'WantedBy': {'multi-user.target'},
|
'WantedBy': {'multi-user.target'},
|
||||||
|
|
@ -270,9 +401,9 @@ def systemd_units(metadata):
|
||||||
'TimeoutStopSec': '15s',
|
'TimeoutStopSec': '15s',
|
||||||
'LogRateLimitIntervalSec': '0',
|
'LogRateLimitIntervalSec': '0',
|
||||||
|
|
||||||
# Hardening profile delivered via
|
# Hardening profile — see HARDENING_SERVER constant near top of
|
||||||
# /etc/systemd/system/left4me-server@.service.d/10-hardening.conf
|
# this file for per-directive rationale.
|
||||||
# (target-side symlink into left4me/deploy/files/, owned by left4me).
|
**HARDENING_SERVER,
|
||||||
},
|
},
|
||||||
'Install': {
|
'Install': {
|
||||||
'WantedBy': {'multi-user.target'},
|
'WantedBy': {'multi-user.target'},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue