diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-11 08:40:06 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-11 08:40:40 +0300 |
| commit | 1604107b8ee96dcc4fa7009c23397a2afc94164e (patch) | |
| tree | 27bd68f009379fc839e2af23e6dece297947470e /docs/sudo-hardening-plan.md | |
| parent | 9dac4b33948f441ec645a8ec491878085483aeb6 (diff) | |
build: harden Magefile.go to use sudo -n for discrete commands onlydevelop
Implement the sudo hardening plan so mage itself never runs as root.
Changes:
- Remove implicit sudo wrapping for go test (buildGoTestCmd, progress
ticker, drainTestEvents helpers removed).
- Add compileIntegrationTestBinary() + runIntegrationTestBinary() helpers.
The integration test binary is compiled unprivileged, then executed
under sudo -n -E from the integrationtests/ directory so relative paths
(../ior, ../ioworkload) resolve correctly.
- Harden sudoOutput() to prefix with sudo -n.
- Harden sudoRunWithEnv() to prefix with sudo -n env ...
- Update docs/sudo-hardening-plan.md and docs/sudo-rules-for-ior.txt
to document the working approach (sudo -n -E with SETENV flag).
Diffstat (limited to 'docs/sudo-hardening-plan.md')
| -rw-r--r-- | docs/sudo-hardening-plan.md | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/docs/sudo-hardening-plan.md b/docs/sudo-hardening-plan.md new file mode 100644 index 0000000..9610d01 --- /dev/null +++ b/docs/sudo-hardening-plan.md @@ -0,0 +1,108 @@ +# Sudo Hardening Plan for I/O Riot NG (ior) + +## Problem Statement + +The current build system (`Magefile.go`) silently wraps entire `go test` invocations—and several generation subcommands—with `sudo` when the invoking user is not root. This means: + +1. **You must trust `mage`** (an arbitrary-code build tool) with root privileges. +2. **An attacker who can modify `Magefile.go`** (or any helper it imports) gets immediate privilege escalation. +3. **It is impossible to audit what actually ran as root**, because the `sudo` command is assembled dynamically inside Go code. + +## Goal + +Run `mage` **entirely as an unprivileged user**. Only the **minimal set of specific sub-processes** that genuinely require elevated privileges (`CAP_BPF` / root) should ever run under `sudo`, and they should be invoked with `sudo -n` so no interactive password prompt can be exploited by a malicious build script. + +Granular `/etc/sudoers.d/` rules can then grant **password-less, command-locked** access to exactly those subprocesses. + +## Threat Model + +| Asset | Risk | Mitigation | +|---|---|---| +| `Magefile.go` | Malicious code injection → arbitrary root | `mage` never runs as root; `sudo` is only used for bounded, hard-coded subcommands | +| `go test` | `init()`, `_testmain.go`, or imported packages run arbitrary code at startup | Integration tests are compiled to a static binary first; only the final binary runs under `sudo` | +| `/sys/kernel/tracing` | Leak of kernel tracepoint data | Read-only access to public tracepoint format files (already world-readable on many systems, but the path traversal needs root on locked-down kernels) | +| `bpftool` | Arbitrary BPF object loading | Restricted to the single read-only invocation: `bpftool btf dump file /sys/kernel/btf/vmlinux format c` | +| `ior` binary | Full `CAP_BPF` access | Acceptable because it is the product being tested; access is via a compiled test binary with a deterministic path | + +## What Needs Root (and Why) + +| Operation | Needs root? | Reason | +|---|---|---| +| Compiling Go / BPF object | **No** | Pure toolchain work | +| `mage test` (unit tests) | **No** | Mocks / stubs; no real BPF attachment | +| `mage generate` / `mage world` | **Partially** | `bpftool btf dump ...` and reading `/sys/kernel/tracing/events/syscalls/*/format` | +| `mage integrationTest` | **Yes** | Spawns `./ior`, which attaches BPF tracepoints | +| `mage integrationTestSerial` | **Yes** | Same | +| `mage demo` / `mage demoOne` | **Yes** | Tapes launch `./ior` (BPF) and `pkill` | +| `mage installDemoTools` | **Yes** | `dnf install` system-wide package | + +## Design Principles + +1. **Compile as user, run binary as root.** + - Build the integration-test binary (`go test -c`) while fully unprivileged. + - Only the resulting `integrationtests.test` binary runs under `sudo`. +2. **Explicit `sudo -n` for every elevated command.** + - No silent wrapping. + - If `sudo -n` fails, the build fails with a clear error telling the admin which sudo rule is missing. +3. **Hard-code elevated commands; no dynamic construction.** + - The exact command line passed to `sudo` must be easily auditable in `Magefile.go`. +4. **Sudoers rules are command-granular.** + - Each rule locks the user to a single command with exact arguments (or wildcards only where strictly necessary). + +## Changes to `Magefile.go` + +### 1. Remove automatic `sudo` wrapping from `buildGoTestCmd` + +Current behavior: when `os.Geteuid() != 0`, the function manufactures a `sudo env … go test …` command. **Delete this logic.** Going forward `buildGoTestCmd` returns a plain `go` command with no privilege elevation. + +### 2. Add compiled-binary helper for integration tests + +Two new helpers are introduced: + +- `compileIntegrationTestBinary(env)` – runs `go test -c ./integrationtests/...` with the required `CGO_*` environment. +- `runIntegrationTestBinary(env, args…)` – elevates **only** the compiled binary via `sudo -n -E ./integrationtests.test …`, running it from the `integrationtests/` directory so that relative path resolution (`../ior`, `../ioworkload`) matches `go test` behaviour. + +`IntegrationTest`, `IntegrationTestSerial`, and `testWithName` (when the target is an integration test) are updated to: + +1. build `ior`, `ioworkload`, and compile the test binary as an unprivileged user; +2. invoke `runIntegrationTestBinary` to execute the tests under `sudo`. + +### 3. Harden existing `sudo*` helpers + +- `sudoOutput` is updated to prefix every elevated call with `sudo -n`. +- `sudoRunWithEnv` is updated to prefix every elevated call with `sudo -n env …`. +- `ensureSudoTimestamp` and `startSudoKeepalive` are **retained but restricted** to the `Demo` targets (they are harmless there, and the demo still needs a warm sudo timestamp). + +### 4. What stays the same + +- `mage build`, `mage test`, `mage testRace`, `mage generateTracepointsGo`, `mage generateTypesGo` – no root required. +- `ensureVMLINUX` and `readSyscallFormats` already call `sudoOutput` for discrete commands; they remain elevated, but now via `sudo -n`. + +## Impact on Existing Workflows + +| Before | After | +|---|---| +| `mage integrationTest` as non-root → auto `sudo env … go test …` | `mage integrationTest` as non-root → compiles as user, then `sudo -n -E ./integrationtests.test …` | +| Password prompt may appear mid-build | `sudo -n` fails fast if rule missing; no surprise prompts | +| JSON progress ticker from `go test -json` | Dropped for integration-test path; plain `-test.v` style output is printed directly. (The compiled test binary does not support the `go test` JSON protocol.) | +| `mage testWithName TEST_NAME=TestAioSetup` | Same compiled-binary dance when target is an integration test | +| `mage generate` | Unchanged UX; `sudo -n bpftool …` and `sudo -n sh -c 'cat /sys/kernel/tracing/…'` run automatically if sudoers rules exist | + +## Files Modified + +- `Magefile.go` – removes implicit sudo wrapping for `go test`; adds compiled-binary helpers. +- `docs/sudo-rules-for-ior.txt` – copy-paste rules for `/etc/sudoers.d/`. + +## Deployment Steps for the Admin + +1. As **root**, copy `docs/sudo-rules-for-ior.txt` into `/etc/sudoers.d/ior`. +2. Replace `%developers` with the actual Unix group or usernames that need to run tests. +3. Validate syntax: `visudo -c -f /etc/sudoers.d/ior`. +4. As **unprivileged user**, verify: `sudo -n bpftool btf dump file /sys/kernel/btf/vmlinux format c | head -3`. +5. Run `mage integrationTest` as the unprivileged user. + +## Known Limitations + +- **`mage demo`** is **not covered** by the granular rules. The demo tapes run a helper script (`run-tape.sh`) that performs multiple privileged actions (`pkill`, launching `./ior`). Running the demo still requires a broad `NOPASSWD` rule for that script, or running `mage demo` as root. +- **`mage installDemoTools`** requires `dnf` access and remains outside the scope of automated test rules. +- The compiled integration-test binary is invoked from the **repository root**. Sudoers rules using a wildcard path (`…/ior/integrationtests.test`) assume the repository is checked out somewhere predictable (e.g., `/home/<user>/git/ior/`). If the checkout lives in arbitrary locations, use a wrapper script at a fixed path. |
