spike: narrow-grant controller-swap mechanics — GO (stdin tee + sudoers * spans spaces, grants stay scoped)

Q1 stdin->pct exec->tee writes byte-identical image\n (PASS); Q2 all 5 narrow grants match,
* spans spaces, negatives denied (PASS); Q3 deployed bootstrap reads via $(cat), tee-written
file consumed identically (PASS). Scratch drop-in + file removed, /etc/sudoers re-validated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
This commit is contained in:
2026-06-29 19:27:15 +02:00
parent eea3cd4040
commit 1141d74532
@@ -0,0 +1,120 @@
# SPIKE — Narrow-grant controller-swap mechanics (stdin `tee` write + sudoers wildcard matching)
- **Date:** 2026-06-29
- **Class:** Spike / empirical, read-only / non-destructive (scratch sudoers drop-in + `/tmp` scratch
file only; both removed in teardown). No production code, no change to the real
`/etc/sudoers.d/felhom-agent`, `/etc/felhom-controller-image`, `controllerswap.go`, or the bootstrap
unit.
- **Host:** felhom-pve (`demo-felhom`, 192.168.0.162), guest LXC 9201; agent runs non-root as
`felhom-agent`. Live controller image at spike time: `…/felhom-controller:0.89.0`.
- **Verdict: GO** — Option A (narrow-grant controller-swap) is mechanically sound as specced. All
three questions PASS; no vector needs reshaping.
---
## Q1 — stdin round-trip through `sudo -n → pct exec → tee` — **PASS**
Run as `felhom-agent`:
```
printf '%s\n' 'gitea.dooplex.hu/admin/felhom-controller:0.89.0' \
| sudo -u felhom-agent sudo -n pct exec 9201 -- tee /tmp/felhom-swap-spike
→ tee-exit=0
```
The bytes written into the guest, `od -c` tail:
```
… c o n t r o l l e r : 0 . 8 9 . 0 \n
```
Exact image ref **plus the trailing `\n`** — byte-identical to the golden's `printf '%s\n'` write.
The `$(cat)` parse the bootstrap uses:
```
IMG=$(cat /tmp/felhom-swap-spike) → parsed=[gitea.dooplex.hu/admin/felhom-controller:0.89.0]
```
stdin forwards cleanly through `sudo -n` and `pct exec` into the in-guest `tee`; the result is what
the reader expects. **The `bash -c "printf … >"` vector can be replaced by stdin-fed `tee`.**
---
## Q2 — sudoers wildcard matching — **PASS (all 5 grants)**
Scratch grants installed (validated `visudo -cf`):
```
/usr/sbin/pct exec [0-9]* -- cat /etc/felhom-controller-image
/usr/sbin/pct exec [0-9]* -- docker image inspect *
/usr/sbin/pct exec [0-9]* -- docker inspect -f *
/usr/sbin/pct exec [0-9]* -- systemctl restart felhom-controller-bootstrap.service
/usr/sbin/pct exec [0-9]* -- tee /tmp/felhom-swap-spike (scratch path; real grant → /etc/felhom-controller-image)
```
`sudo -n -l -- <real invocation>` exit codes (0 = matched/permitted):
| Real invocation | exit |
|---|---|
| `pct exec 9201 -- docker inspect -f '{{json .State}}' felhom-controller` | **0** |
| `pct exec 9201 -- docker inspect -f '{{.State.Running}}|{{if .State.Health}}…{{end}}|{{.Config.Image}}' felhom-controller` (the REAL `controllerHealthy` template) | **0** |
| `pct exec 9201 -- docker image inspect gitea.dooplex.hu/admin/felhom-controller:0.89.0` | **0** |
| `pct exec 9201 -- cat /etc/felhom-controller-image` | **0** |
| `pct exec 9201 -- systemctl restart felhom-controller-bootstrap.service` | **0** |
| `pct exec 9201 -- tee /tmp/felhom-swap-spike` | **0** |
**The critical question — does `*` span spaces? YES.** `docker inspect -f *` matches the full
`-f '<template-with-spaces-and-braces>' <container>` line, the `*` spanning the format string AND the
container across the space. Same for `docker image inspect *`. (sudo matches the trailing argument
string with `fnmatch`, and an unanchored `*` covers whitespace.) **No grant needs reshaping** — the
`-f` template can stay the rich multi-field string; it does not need to be flattened to a fixed
template.
### Negative controls — the grants stay SCOPED (not a blanket `pct exec`) — **PASS**
| Non-granted invocation | exit (want 1) |
|---|---|
| `pct exec 9201 -- docker rm -f felhom-controller` | **1** |
| `pct exec 9201 -- rm -rf /etc` | **1** |
| `pct exec 9201 -- bash -c 'echo pwned'` | **1** |
| `pct exec 9201 -- tee /etc/felhom-controller-image` (real file; scratch only granted `/tmp`) | **1** |
Arbitrary in-guest execution (`bash -c`, `rm`, `docker rm`) and writing the real image file are all
**denied**. The five narrow grants do NOT widen back into the general `pct exec * ` surface that this
work exists to avoid. (In the real grant, `tee /etc/felhom-controller-image` is a FIXED path — no
wildcard — so it is even tighter than the scratch `/tmp` form tested here.)
---
## Q3 — read compatibility on the live guest — **PASS**
Deployed unit `felhom-controller-bootstrap.service`
`ExecStart=/usr/local/sbin/felhom-controller-bootstrap.sh`. Its read line (verbatim):
```
9: IMAGE=$(cat /etc/felhom-controller-image 2>/dev/null || true)
10: [ -n "$IMAGE" ] || { echo "[ctrl-bootstrap] FATAL: …image missing"; exit 1; }
```
`$(cat)` strips trailing newlines, so the read is newline-agnostic. The currently-deployed file is
48 bytes ending `…0.89.0\n` (od-confirmed) — i.e. the live golden already stores image+`\n`, exactly
what a `tee`-written `image\n` produces. **A `tee`-written file is consumed identically to today's
`printf` write** — no bootstrap change needed.
---
## Go / No-go for Option A — **GO**
All three unproven mechanisms hold on the live host:
1. stdin → `sudo -n``pct exec``tee` writes exact bytes incl. the trailing newline (Q1).
2. The 5 narrow sudoers grants match every real invocation, `*` spans spaces, and they remain scoped
— arbitrary exec stays denied (Q2 + negatives).
3. The deployed bootstrap's `$(cat)` read consumes a `tee`-written file identically (Q3).
**No vector needs reshaping before implementation.** Implementation notes for the impl task (not this
spike):
- `GuestExec` (`guestbind.go:119`) has no stdin path today — the impl needs a stdin-carrying variant
(e.g. `GuestExecStdin(ctx, vmid, stdin io.Reader, args…)`) that sets `cmd.Stdin`; `writeImage`
switches from `bash -c "printf…>"` to that variant piping the image into `tee <file>`.
- The 5 grants go into the real `/etc/sudoers.d/felhom-agent` (a new `FELHOM_CONTROLLERSWAP` alias) —
with `tee /etc/felhom-controller-image` as a FIXED path (no wildcard). Add them to the v0.44.0
capability manifest so the build-test covers them.
- The image ref is already strict-validated (`controllerImageRe`) before write — the stdin/`tee` path
removes the shell entirely, so even that is now defence-in-depth rather than the only guard.
## Teardown — confirmed clean
`/etc/sudoers.d/felhom-agent-spike` removed; `/tmp/felhom-swap-spike` removed; `visudo -cf
/etc/sudoers` parses OK; remaining drop-ins `felhom-agent`, `README`, `zfs`; the real
`felhom-agent` grant is untouched (lxc-info grant still present). No secrets recorded (the image ref
is not a secret; no tokens were handled).