controllerswap: stdin tee write + narrow FELHOM_CONTROLLERSWAP grants (non-root, v0.45.0)

writeImage drops bash -c/printf for GuestExecStdin(img+\n -> tee /etc/felhom-controller-image);
new Runner.RunStdin/GuestExecStdin route stdin through the fenced sudo -n runner. 5 narrow,
auditable sudoers grants (no general pct exec, no bash -c) + capability manifest entries (Critical)
so the self-probe watches them and the build-test asserts coverage (companion red-proof). No
controller change; swap orchestration/rollback/state unchanged. Spike GO.

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:42:30 +02:00
parent 61c89a7efa
commit 8a4ccab3e6
15 changed files with 230 additions and 23 deletions
+12
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os/exec"
"strconv"
)
@@ -26,6 +27,10 @@ import (
// production implementation; tests inject a mock to assert which commands ran.
type Runner interface {
Run(ctx context.Context, name string, args ...string) (stdout, stderr []byte, err error)
// RunStdin is Run with the command's stdin fed from r (nil = no stdin). Used by the
// controller-swap write (image piped into an in-guest `tee`), so the swap needs no shell
// vector. Keeps the sudo-prefix/mode handling in the same place as Run.
RunStdin(ctx context.Context, stdin io.Reader, name string, args ...string) (stdout, stderr []byte, err error)
}
// RunnerMode selects how privileged commands are executed.
@@ -48,6 +53,12 @@ type ExecRunner struct {
// Run implements Runner.
func (r *ExecRunner) Run(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
return r.RunStdin(ctx, nil, name, args...)
}
// RunStdin is Run with the process stdin fed from stdin (nil = no stdin). The sudo-prefix/mode
// handling is identical to Run — kept here so both paths share one place.
func (r *ExecRunner) RunStdin(ctx context.Context, stdin io.Reader, name string, args ...string) ([]byte, []byte, error) {
var cmd *exec.Cmd
if r.Mode == RunnerSudo {
sudo := r.SudoPath
@@ -59,6 +70,7 @@ func (r *ExecRunner) Run(ctx context.Context, name string, args ...string) ([]by
cmd = exec.CommandContext(ctx, name, args...)
}
var stdout, stderr capBuf
cmd.Stdin = stdin
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()