Files
felhom.eu/documentation/audits/SPIKE-controllerswap-narrow-grants-2026-06-29.md
T
admin 1141d74532 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
2026-06-29 19:27:15 +02:00

6.3 KiB

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 → teePASS

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}}
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.serviceExecStart=/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 -npct exectee 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).