spec(deployment-responsibility): design — symlink hardening drop-ins, sudoers, sysctl, helpers
Conservative reshape coming out of the brainstorm: application-shape static artifacts move to left4me/deploy/ and are delivered to the target via bw symlink items pointing into /opt/left4me/src/deploy/... (safe because the runtime-state relocation made the checkout root-owned). Per-host shape — base unit bodies, slice CPU pinning, env templates, nginx/timers/nftables metadata — stays bw-managed in ckn-bw. Moves: hardening drop-ins (new), sudoers (dedup mirror), sysctl drop-in (dedup mirror + absorb ptrace_scope metadata entry), privileged scripts (relocate scripts/ to deploy/scripts/, replace install-action with symlinks). Five-step migration with sysctl consolidation as the canary, then hardening drop-ins, sudoers, scripts, cleanup. Lands before the build-overlay-unit refactor so that work can ship its hardening drop-in inline using this pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
434ee20339
commit
c446f6c8eb
1 changed files with 355 additions and 0 deletions
|
|
@ -0,0 +1,355 @@
|
|||
# Deployment responsibility — design
|
||||
|
||||
## Context
|
||||
|
||||
Trace: `2026-05-06-left4me-deployment-design.md` established the original
|
||||
model — left4me's `deploy/files/` mirrors target filesystem paths;
|
||||
ckn-bw integrates. The hardening refactor
|
||||
(`2026-05-15-hardening-refactor-design.md`) landed *inline-in-reactor*
|
||||
as an explicit tradeoff and queued the responsibility question for this
|
||||
brainstorm (handoff: `2026-05-15-handoff-deployment-responsibility.md`).
|
||||
The runtime-state relocation
|
||||
(`2026-05-15-runtime-state-relocation-design.md`) made
|
||||
`/opt/left4me/src` root-owned, which is the prerequisite that makes
|
||||
target-side symlinks into the checkout safe — left4me cannot rewrite
|
||||
its own deployment artifacts at runtime.
|
||||
|
||||
This design picks a narrow, conservative line. Application-shape
|
||||
artifacts that are static across hosts move to left4me's `deploy/`
|
||||
tree and are delivered to the target via **target-side symlinks**.
|
||||
Per-host shape (CPU pinning, gunicorn workers, env file values) stays
|
||||
bw-managed. The base systemd unit bodies stay bw-managed too — they
|
||||
encode per-host values (workers, threads, CPU set) that are awkward to
|
||||
parameterize cleanly, and ckn-bw is already the right place for that
|
||||
computation.
|
||||
|
||||
The wedge between "moves" and "stays" is **threat model knowledge vs.
|
||||
host shape**. The hardening profile is the security knowledge of the
|
||||
application; the base unit body is the operational shape of the host.
|
||||
Different repos.
|
||||
|
||||
## Scope
|
||||
|
||||
### Moves to left4me/deploy/, delivered via target-side symlinks
|
||||
|
||||
| Artifact | Source path | Symlink target |
|
||||
|---|---|---|
|
||||
| Hardening drop-in for `left4me-web` | `deploy/files/etc/systemd/system/left4me-web.service.d/10-hardening.conf` (NEW) | `/etc/systemd/system/left4me-web.service.d/10-hardening.conf` |
|
||||
| Hardening drop-in for `left4me-server@` | `deploy/files/etc/systemd/system/left4me-server@.service.d/10-hardening.conf` (NEW) | same pattern |
|
||||
| Sudoers | `deploy/files/etc/sudoers.d/left4me` (exists) | `/etc/sudoers.d/left4me` |
|
||||
| Sysctl drop-in (absorbs `ptrace_scope`) | `deploy/files/etc/sysctl.d/99-left4me.conf` (exists; one line added) | `/etc/sysctl.d/99-left4me.conf` |
|
||||
| Privileged helpers (`libexec/`) | `deploy/scripts/libexec/*` (relocated from `scripts/libexec/`) | `/usr/local/libexec/left4me/<name>` |
|
||||
| Privileged helpers (`sbin/`) | `deploy/scripts/sbin/*` (relocated from `scripts/sbin/`) | `/usr/local/sbin/<name>` |
|
||||
|
||||
All symlinks are created by bw `symlinks{}` items in
|
||||
`bundles/left4me/items.py`. `git_deploy:/opt/left4me/src` triggers
|
||||
`systemctl daemon-reload` (for unit drop-ins) and `sysctl --system`
|
||||
(for sysctl) so changes to the symlink-target content propagate even
|
||||
though the symlink path itself doesn't change.
|
||||
|
||||
### Stays bw-managed
|
||||
|
||||
- **Base unit bodies** (`left4me-web.service`, `left4me-server@.service`):
|
||||
emitted by the `systemd/units` reactor in
|
||||
`bundles/left4me/metadata.py`. These encode per-host values
|
||||
(gunicorn workers/threads, CPU pinning, instance bind paths). Pulling
|
||||
them into left4me would require either templating or env-var
|
||||
parameterization that doesn't cleanly cover everything (systemd
|
||||
doesn't substitute env vars in non-Exec directives like
|
||||
`SocketBindAllow=`).
|
||||
- **Slice units** (`l4d2-game.slice`, `l4d2-build.slice`) and cpuset
|
||||
drop-ins (`system.slice.d/99-left4me-cpuset.conf`,
|
||||
`user.slice.d/99-left4me-cpuset.conf`): all encode per-host CPU
|
||||
pinning. Reactor stays.
|
||||
- **`host.env.mako`, `web.env.mako`**: per-host secret + scalar
|
||||
templating. Stays.
|
||||
- **`nginx/vhosts`, `nftables/input`, `nftables/output`**: bundle
|
||||
abstractions (letsencrypt auto-population, set-merge) add real value
|
||||
over raw files.
|
||||
- **`systemd-timers/left4me-workshop-refresh`**: same — bundle
|
||||
synthesizes the `.timer` + `.service` from the metadata dict.
|
||||
- **Action chains**: `git_deploy`, `pip_install`, `alembic_upgrade`,
|
||||
`seed_overlays`, `create_venv`, `pip_upgrade`, `install_steamcmd`.
|
||||
Stays.
|
||||
- **`directory`, `user`, `group`** items: must exist before
|
||||
`git_deploy` runs.
|
||||
- **`apt/packages`, `backup/paths`** defaults. Stays.
|
||||
|
||||
### Stays in left4me as reference fixtures (no change)
|
||||
|
||||
`deploy/files/usr/local/lib/systemd/system/*.{service,slice}` —
|
||||
reference units matched against the live form by
|
||||
`deploy/tests/test_deploy_artifacts.py`. Base units stay bw-emitted,
|
||||
so reference-vs-live assertion stays valid. Reference units should
|
||||
**not** include hardening directives once the drop-in extraction
|
||||
lands; the live form's hardening lives in the drop-in, not the base
|
||||
unit.
|
||||
|
||||
## Repo layout (left4me)
|
||||
|
||||
```
|
||||
deploy/
|
||||
files/
|
||||
etc/sudoers.d/left4me
|
||||
etc/sysctl.d/99-left4me.conf
|
||||
etc/systemd/system/left4me-web.service.d/10-hardening.conf # NEW
|
||||
etc/systemd/system/left4me-server@.service.d/10-hardening.conf # NEW
|
||||
etc/left4me/sandbox-resolv.conf # unchanged
|
||||
usr/local/lib/systemd/system/*.{service,slice} # reference (unchanged shape)
|
||||
scripts/ # moves in from scripts/
|
||||
libexec/{left4me-overlay,left4me-systemctl,left4me-journalctl,left4me-script-sandbox}
|
||||
sbin/<wrappers>
|
||||
tests/
|
||||
```
|
||||
|
||||
## Mechanism: target-side symlinks
|
||||
|
||||
bw `symlinks{}` item type. One entry per artifact:
|
||||
|
||||
```python
|
||||
symlinks = {
|
||||
'/etc/sudoers.d/left4me': {
|
||||
'target': '/opt/left4me/src/deploy/files/etc/sudoers.d/left4me',
|
||||
'owner': 'root',
|
||||
'group': 'root',
|
||||
'needs': ['git_deploy:/opt/left4me/src'],
|
||||
},
|
||||
'/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',
|
||||
'needs': [
|
||||
'directory:/etc/systemd/system/left4me-web.service.d',
|
||||
'git_deploy:/opt/left4me/src',
|
||||
],
|
||||
'triggers': ['action:systemd_daemon_reload'],
|
||||
},
|
||||
# …same for left4me-server@.service.d/10-hardening.conf
|
||||
# …same for each script in /usr/local/{libexec/left4me,sbin}/
|
||||
}
|
||||
```
|
||||
|
||||
Drop-in directories (`*.service.d/`) need explicit `directory:` items
|
||||
in `items.py` (the bw systemd bundle does not create them
|
||||
automatically for symlink-only drop-ins). Mode `0755`, owner
|
||||
`root:root`.
|
||||
|
||||
bw fires the symlink's `triggers:` when **the symlink itself
|
||||
changes** (path/target update). It does *not* fire when the symlink's
|
||||
*target content* changes — that's still a `git_deploy:` event. So
|
||||
both wirings are needed: every symlink declares
|
||||
`needs: ['git_deploy:/opt/left4me/src']`, and ckn-bw declares
|
||||
`triggered_by: [git_deploy:/opt/left4me/src]` actions for the global
|
||||
reloads (`daemon-reload`, `sysctl --system`).
|
||||
|
||||
## Per-artifact details
|
||||
|
||||
### Hardening drop-ins
|
||||
|
||||
Extract from `HARDENING_COMMON`, `HARDENING_SERVER`, `HARDENING_WEB`
|
||||
Python dicts in `bundles/left4me/metadata.py` into static `.conf`
|
||||
files:
|
||||
|
||||
```ini
|
||||
# deploy/files/etc/systemd/system/left4me-web.service.d/10-hardening.conf
|
||||
[Service]
|
||||
ProtectProc=invisible
|
||||
ProcSubset=pid
|
||||
ProtectKernelTunables=true
|
||||
…
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@debug @mount @raw-io @reboot @swap @cpu-emulation @obsolete
|
||||
```
|
||||
|
||||
Per-directive comments documenting *why* each directive is set the
|
||||
way it is (sudo-incompatibility carve-outs for web; i386 amendment
|
||||
and `PrivatePIDs` rationale for server@) should live inline as `#`
|
||||
comments. Today those rationale comments live in the Python source;
|
||||
they need to come along.
|
||||
|
||||
After extraction, the reactor's emitted unit bodies drop the
|
||||
`**HARDENING_WEB` / `**HARDENING_SERVER` splat. The reactor still
|
||||
emits the base unit and is responsible for everything except the
|
||||
hardening profile.
|
||||
|
||||
### Sudoers
|
||||
|
||||
Today: identical content in
|
||||
`left4me/deploy/files/etc/sudoers.d/left4me` and
|
||||
`ckn-bw/bundles/left4me/files/etc/sudoers.d/left4me`. The bw item
|
||||
sources from the ckn-bw copy.
|
||||
|
||||
After: bw `symlinks{}` item; delete the ckn-bw copy and the bw
|
||||
`files{}` entry. The `test_with: 'visudo -cf {}'` semantics don't
|
||||
apply to symlinks; tested instead on commit in left4me CI (a
|
||||
`test_sudoers.py` that runs `visudo -cf` against the live file).
|
||||
|
||||
### Sysctl drop-in + ptrace_scope absorption
|
||||
|
||||
Today: same dual-copy story as sudoers, plus `kernel.yama.ptrace_scope`
|
||||
exists as a metadata default (`sysctl/kernel/yama/ptrace_scope: '2'`)
|
||||
that gets deployed via `bundles/sysctl/` into a separate file.
|
||||
|
||||
After: append `kernel.yama.ptrace_scope = 2` to
|
||||
`deploy/files/etc/sysctl.d/99-left4me.conf`. Delete the metadata
|
||||
entry. Delete the bw `files{}` entry + ckn-bw mirror; replace with
|
||||
symlink. `bundles/sysctl/` no longer renders anything for left4me;
|
||||
all left4me sysctl tuning lives in the one drop-in.
|
||||
|
||||
### Privileged scripts
|
||||
|
||||
Today: `scripts/libexec/`, `scripts/sbin/` at left4me repo root.
|
||||
`install_left4me_scripts` action copies them to
|
||||
`/usr/local/libexec/left4me/` and `/usr/local/sbin/` as root on every
|
||||
git_deploy update.
|
||||
|
||||
After:
|
||||
- Move `left4me/scripts/` → `left4me/deploy/scripts/`. Update the few
|
||||
references that point at the old path (search for
|
||||
`/opt/left4me/src/scripts/` and `scripts/{libexec,sbin}/` in both
|
||||
repos).
|
||||
- Replace `install_left4me_scripts` action with one bw `symlinks{}`
|
||||
item per script. Trigger semantics: each symlink declares
|
||||
`triggers: ['action:systemd_daemon_reload']` only if the script is
|
||||
referenced by a systemd unit (e.g. `left4me-overlay` is in
|
||||
`ExecStartPre=` of `left4me-server@.service`; daemon-reload not
|
||||
needed for script changes since systemd reads the script content at
|
||||
exec time, not at unit-load time).
|
||||
|
||||
Sudo follows symlinks. With `/opt/left4me/src` root-owned, the
|
||||
symlink target is root-owned, and sudo's `Cmnd_Alias` path matching
|
||||
sees the original `/usr/local/{libexec,sbin}/<name>` path.
|
||||
|
||||
### Reference units in `deploy/files/`
|
||||
|
||||
No structural change. Remove hardening directives from the reference
|
||||
files in lockstep with extracting them into the drop-ins (otherwise
|
||||
`test_deploy_artifacts.py` sees the reference unit with hardening
|
||||
inline but the live unit without). The reference file then represents
|
||||
"the base unit ckn-bw emits"; the drop-in represents "the hardening
|
||||
profile left4me ships".
|
||||
|
||||
## Migration order
|
||||
|
||||
Each step is an independent landable PR.
|
||||
|
||||
1. **Canary — sysctl consolidation.**
|
||||
- Add `kernel.yama.ptrace_scope = 2` to
|
||||
`deploy/files/etc/sysctl.d/99-left4me.conf`.
|
||||
- Delete `defaults['sysctl']['kernel']['yama']['ptrace_scope']`
|
||||
from `bundles/left4me/metadata.py`.
|
||||
- Delete `bundles/left4me/files/etc/sysctl.d/99-left4me.conf`
|
||||
(the verbatim mirror).
|
||||
- Replace the bw `files{}` entry with a `symlinks{}` entry
|
||||
pointing at the checkout.
|
||||
- Verify: `sysctl kernel.yama.ptrace_scope` reads `2`;
|
||||
`bw apply` idempotent.
|
||||
|
||||
2. **Hardening drop-ins.**
|
||||
- Create `deploy/files/etc/systemd/system/left4me-web.service.d/10-hardening.conf`
|
||||
and `…/left4me-server@.service.d/10-hardening.conf` from the
|
||||
`HARDENING_*` dicts.
|
||||
- Remove `**HARDENING_WEB` / `**HARDENING_SERVER` splats from the
|
||||
reactor; delete the three constants.
|
||||
- Remove hardening directives from the reference units in
|
||||
`deploy/files/usr/local/lib/systemd/system/`.
|
||||
- Add `directory:/etc/systemd/system/left4me-{web,server@}.service.d`
|
||||
items + symlinks for the drop-ins.
|
||||
- Wire `systemctl daemon-reload` to fire on
|
||||
`git_deploy:/opt/left4me/src`.
|
||||
- Verify: `systemctl show -p ProtectSystem,ProtectKernelTunables,PrivateUsers,…
|
||||
left4me-web.service left4me-server@1.service` matches the
|
||||
pre-extraction values (full hardening test plan rerun is the
|
||||
gold standard).
|
||||
|
||||
3. **Sudoers.**
|
||||
- Replace bw `files{}` entry with `symlinks{}`.
|
||||
- Delete `bundles/left4me/files/etc/sudoers.d/left4me`.
|
||||
- Add left4me CI test running `visudo -cf` on the file.
|
||||
- Verify: `sudo -l -U left4me` lists the expected commands;
|
||||
gameserver start via the web app still works.
|
||||
|
||||
4. **Privileged scripts.**
|
||||
- `git mv left4me/scripts left4me/deploy/scripts`.
|
||||
- Update any references (commit hooks, docs).
|
||||
- Replace `actions['install_left4me_scripts']` with `symlinks{}`
|
||||
items, one per script. Drop the action.
|
||||
- Update `git_deploy:` `triggers:` to remove
|
||||
`action:install_left4me_scripts`.
|
||||
- Verify: `sudo /usr/local/libexec/left4me/left4me-overlay status 1`
|
||||
still works; gameserver lifecycle (start/stop) still works.
|
||||
|
||||
5. **Cleanup.**
|
||||
- Prune `gunicorn_workers` / `gunicorn_threads` metadata defaults
|
||||
if they end up referenced only by `web.env.mako` (they do today;
|
||||
keep the metadata, they're real per-host values).
|
||||
- Update `deploy/README.md` to describe the new layout
|
||||
(deploy/files = symlink source-of-truth; deploy/scripts = same
|
||||
for helpers).
|
||||
- Update `bundles/left4me/README.md` to describe the new
|
||||
symlink-based delivery model.
|
||||
|
||||
## Sequence vs. build-overlay-unit refactor
|
||||
|
||||
This design lands **before** the build-overlay-unit refactor
|
||||
(`2026-05-15-build-overlay-unit-design.md`). Reasons:
|
||||
|
||||
- build-overlay-unit introduces a dispatcher unit template; its
|
||||
hardening profile should live as a drop-in alongside the dispatcher
|
||||
from the start, using the pattern this design establishes.
|
||||
- The reactor surgery in step 2 (removing `HARDENING_*` splats) is
|
||||
cleaner against today's reactor than against a reactor that's also
|
||||
being reshaped for the build-overlay-unit work.
|
||||
|
||||
## Verification (end-to-end)
|
||||
|
||||
After all five steps land and `bw apply` is idempotent on
|
||||
`ovh.left4me`:
|
||||
|
||||
1. `systemctl show -p ProtectSystem,PrivateUsers,SystemCallFilter,…
|
||||
left4me-web.service left4me-server@1.service` matches the
|
||||
hardening test plan's reference values (run the relevant tests
|
||||
from `docs/superpowers/specs/2026-05-15-hardening-test-plan.md`).
|
||||
2. `sysctl kernel.yama.ptrace_scope net.core.rmem_max
|
||||
net.ipv4.tcp_congestion_control` returns expected values.
|
||||
3. `sudo -l -U left4me` reports the same allowed commands as before.
|
||||
4. `ls -la /etc/sudoers.d/left4me /etc/sysctl.d/99-left4me.conf
|
||||
/etc/systemd/system/left4me-*.service.d/10-hardening.conf
|
||||
/usr/local/libexec/left4me/* /usr/local/sbin/<wrappers>` shows
|
||||
symlinks into `/opt/left4me/src/deploy/...`.
|
||||
5. A gameserver round-trip (start via web app → cvar inspect → stop)
|
||||
succeeds.
|
||||
6. `bw verify ovh.left4me` reports no drift.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Moving base unit bodies into left4me. Per-host shape stays
|
||||
reactor-emitted.
|
||||
- AppArmor profiles (deferred from the defenses survey).
|
||||
- Reshaping the bw `files{}` items for `host.env.mako` /
|
||||
`web.env.mako` — they need mako templating with metadata context,
|
||||
which ckn-bw is the right place for.
|
||||
- The build-overlay-unit refactor itself. Lands separately on top of
|
||||
this.
|
||||
|
||||
## Pointers
|
||||
|
||||
- Handoff (this brainstorm's framing):
|
||||
`docs/superpowers/specs/2026-05-15-handoff-deployment-responsibility.md`
|
||||
- Prereq (runtime state relocation + non-editable install, shipped):
|
||||
`docs/superpowers/specs/2026-05-15-runtime-state-relocation-design.md`
|
||||
- Original deployment design (the model being reaffirmed for
|
||||
application-shape artifacts):
|
||||
`docs/superpowers/specs/2026-05-06-left4me-deployment-design.md`
|
||||
- Hardening refactor design (the inline-in-reactor approach this
|
||||
design supersedes for hardening):
|
||||
`docs/superpowers/specs/2026-05-15-hardening-refactor-design.md`
|
||||
- Hardening test plan (reference for step-2 verification):
|
||||
`docs/superpowers/specs/2026-05-15-hardening-test-plan.md`
|
||||
- ckn-bw left4me bundle: `~/Projekte/ckn-bw/bundles/left4me/`
|
||||
Loading…
Reference in a new issue