docs: bind the supply-chain claims to the helper
This commit is contained in:
parent
14d4730c76
commit
c3037655cd
10 changed files with 194 additions and 128 deletions
|
|
@ -1,12 +1,14 @@
|
|||
# Helper Component
|
||||
|
||||
**A privileged, network-less Rust executor that trusts nothing it didn't verify itself — the last gate before mutation.**
|
||||
**A privileged, short-lived Rust executor that independently validates authority before APT/DNF mutation.**
|
||||
|
||||
---
|
||||
|
||||
## Doctrine
|
||||
|
||||
The helper (`helper/src/main.rs`, a single ~2,000-line binary) is the only thing on a gated host allowed to mutate packages, and it earns that privilege by being structurally incapable of being talked into anything. It has no network stack in play, reads its trust inputs from root-owned pinned files, and performs exactly one operation per invocation — the one described by a validly signed capability token. Everything else is a typed denial.
|
||||
The helper (`helper/src/main.rs`, a single ~2,000-line binary) is the only RedFlag path allowed to mutate packages on gated ecosystems. It reads trust inputs from root-owned pinned files and performs exactly one operation per invocation — the one described by a validly signed capability token. It accepts no shell text and inherits no environment. Everything else is a typed denial.
|
||||
|
||||
The current Linux transient unit is **not network-isolated**. APT and DNF can reach their configured registries while the helper runs. Networkless execution remains the design target after RedFlag can stage and re-verify every required artifact locally.
|
||||
|
||||
Deny-by-default is the architecture, not a configuration: every failure path returns a `Denial` with a distinct exit code and a `log_security` entry. There is no flag that weakens verification. See [security/05-supply-chain-gate](../security/05-supply-chain-gate.md) for the token contract this enforces.
|
||||
|
||||
|
|
@ -14,7 +16,7 @@ Deny-by-default is the architecture, not a configuration: every failure path ret
|
|||
|
||||
## Invocation
|
||||
|
||||
The agent invokes it via `sudo systemd-run --pipe --property=ProtectSystem=no` — a transient unit with full filesystem access, separate from the agent's own locked-down unit. The token arrives on stdin (or a file path for self-update flows). The agent holds zero install sudo; the sudoers file grants discovery commands plus this one invocation line. See [components/02-agent](02-agent.md).
|
||||
On Linux the agent invokes it through `sudo systemd-run --wait --property=ProtectSystem=no`, passing root-readable token and result file paths. This avoids fd passing through dbus while escaping the agent service's own `ProtectSystem=strict` mount sandbox. No `PrivateNetwork` or equivalent property is set. The agent holds zero direct install sudo; its privileged route is this helper invocation. See [components/02-agent](02-agent.md).
|
||||
|
||||
### Windows Invocation (SEC-030, decided 2026-07-01)
|
||||
|
||||
|
|
@ -32,16 +34,17 @@ The ACL lockdown described here is the v1 cut, not the final word — Casey's ca
|
|||
|
||||
## The Verification Pipeline
|
||||
|
||||
`run()` executes, in order — any failure stops the world:
|
||||
`run()` executes, in order — any failure stops the operation:
|
||||
|
||||
1. **Trust-input self-validation (SEC-021)** — `validate_trusted_path`: every file the helper relies on (keyring dir, agent-id file, state) must be root-owned, not a symlink, and not group/other-writable. The helper defends its own inputs instead of trusting that the installer set permissions correctly.
|
||||
2. **Keyring load** — pinned Ed25519 public keys from `/etc/redflag/trusted-keys`. Verify keys, not servers (load-bearing constraint #4).
|
||||
3. **Closure hash** — `closure_hash()` recomputes the canonical hash over the token's resolved closure. This must be **byte-identical** to the Go implementation; cross-language tests pin the contract ([testing/01-test-pyramid](../testing/01-test-pyramid.md)).
|
||||
4. **Signature** — `verify_signature()` checks the token's Ed25519 signature over the signed message (which embeds the closure hash) against the keyring, by `key_id`.
|
||||
5. **Artifact hashes** — `verify_artifacts()` SHA-256s every artifact the token authorizes. A mismatch anywhere is a denial.
|
||||
6. **Replay check** — `replay_check_and_record()`: token IDs are recorded in local state; a token executes once.
|
||||
7. **Plan + execute** — `build_plan()` translates the token into the exact package-manager commands; `execute_plan()` runs them. No interpretation, no substitution. A POSIX `--` (end-of-options) separator precedes all user-derived values (package names, versions) to prevent option injection (GATE-005).
|
||||
8. **Receipt** — `emit_result()` writes a `PolicyResult` the agent reports back; the server reconciles it into lifecycle state.
|
||||
1. **Token shape and time** — reject an unsupported token version, a not-yet-valid token, or an expired token.
|
||||
2. **Host binding** — compare the token's `agent_id` with an independently read local identity.
|
||||
3. **Trust-input validation and keyring load (SEC-021)** — root-owned, non-symlinked, non-writable trust paths; pinned Ed25519 public keys from `/etc/redflag/trusted-keys`.
|
||||
4. **Closure hash and signature** — recompute the canonical closure hash and verify the signed message against the selected pinned key. Go and Rust tests pin the byte contract.
|
||||
5. **Local artifact hashes** — `verify_artifacts()` rehashes entries that name a local file. A mirror entry must name a readable matching file. A normal registry entry with no local file remains signed but is not rehashed here.
|
||||
6. **Fixed plan** — `build_plan()` maps the signed package type and operation to fixed package-manager argv. APT/DNF insert POSIX `--` before package values; unsupported pairs deny.
|
||||
7. **Replay record** — `replay_check_and_record()` records the token ID before execution; a token runs at most once even across a crash.
|
||||
8. **Execute** — `execute_plan()` invokes the fixed argv directly, without a shell, after `env_clear()` and a fixed `PATH`.
|
||||
9. **Receipt** — the helper writes a structured `PolicyResult`; the agent reports it and the server reconciles lifecycle state.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -59,10 +62,11 @@ The helper also carries a local-authority minting path (`MintRequest` / `MintedT
|
|||
|
||||
## Why a Separate Binary, Why Rust
|
||||
|
||||
- **Privilege separation:** the long-running, network-facing agent stays unprivileged; the privileged thing is short-lived, single-purpose, and offline.
|
||||
- **Can't be redirected:** no outbound capability means a compromised server can lie in a token — and the signature/hash checks catch that — but nothing can make the helper fetch from somewhere else.
|
||||
- **Privilege separation:** the long-running, network-facing agent stays unprivileged; the privileged thing is short-lived and single-purpose.
|
||||
- **Narrow execution surface:** token fields select from fixed argv templates. They are not interpreted as shell input, and the child gets a cleared environment.
|
||||
- **Isolation still to land:** registry-backed APT/DNF operations currently retain network access. The intended end state stages every authorized artifact locally, verifies it, and runs the helper without a network namespace.
|
||||
- **Small audit surface:** one file, explicit pipeline, typed denials. The binary is meant to be read.
|
||||
|
||||
---
|
||||
|
||||
*Last reviewed: 2026-06-14*
|
||||
*Last reviewed: 2026-08-25*
|
||||
|
|
|
|||
Loading…
Reference in a new issue