RedFlag/RAF/components/04-helper.md
Source publisher ffdccba10f source: advance the checked projection
Source-Sha: 194a59adfd6c901635da7a9c18253bb2210e2426
Policy-Sha: b9b0be0ee74e55f561a803b8aef6be3c2134f5a83b46816a069a372f2eb04f4d
Tree-Digest: 4697f2e281c1c6ecafff17cf6eeb391a0a1ac0fa6e5fc429f5b473f4579c252c
2026-09-21 14:39:08 +00:00

180 lines
14 KiB
Markdown

# Helper Component
**A privileged, short-lived Rust verifier, local minter, and executor for capability-gated mutation.**
---
## Doctrine
The helper (`helper/src/main.rs`) 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 operation described by a valid capability token or `MutationEnvelope`. 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.
---
## Invocation
On Linux the agent invokes it through `sudo systemd-run --wait --property=ProtectSystem=no`, passing fixed exchange-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 an exact helper invocation. Fleet installers grant only the live capability-token execution shape. Standalone provisioning adds local mint and pacman-envelope command shapes and must not coexist with fleet credentials. See [components/02-agent](02-agent.md).
### Windows Invocation (SEC-030, decided 2026-07-01)
**Current source boundary:** the native Windows payload now builds the Rust Helper. It
answers its version and supports verification-only binary and `MutationEnvelope` checks.
Windows-only source now opens each trust-path component without following reparse points,
checks SYSTEM/Administrators ownership and denies write grants to other principals, rejects
hard-linked trust files, and creates write-through, one-shot authorization claims. The fixed
task dispatcher reads an envelope from its protected exchange, pins the installed authority
placement, and uses the shared verifier. The wrapper carrying that envelope is decoded
before the envelope itself, so a request the Helper could read but whose envelope it could
not is refused under its own validated request id instead of leaving the caller an unknown
outcome; a wrapper too malformed to establish an identity still writes no result, because
the Helper does not invent one. A shared Go/Rust fixture pins that wrapper on both sides.
The Agent reaches this path read-only through `redflag-agent -verify-envelope`, which mints
nothing, admits no backend, and passes no command or path to the privileged process.
Windows mint and backend execution still return typed refusals.
Native acceptance of the *installed* rail — ACL enforcement, service identity, task
definition, and exchange roots — remains open: the transport is covered by tests, not by a
witness on a provisioned host. Replay refusal is likewise open and deliberately unexercised,
because verification must not consume an authorization; the claim belongs at an execution
boundary that no admitted backend has yet. Low-privilege service migration and WUA / Winget
executors remain required before mutation authority is enabled.
Windows has no `systemd-run` equivalent for spinning up an ad-hoc transient privileged unit, so the helper is invoked through a **Scheduled Task, configured to run once as SYSTEM**. The agent's own service account has no standing right to mutate anything — it only holds a delegated "AllowedToRun" ACE on this one task definition (`schtasks /run /tn RedFlagHelper`), the direct Windows analogue of the Linux sudoers line that grants exactly one `systemd-run` invocation and nothing else.
Two elevation-model alternatives were considered and rejected:
- **A persistent, always-running elevated service watching a staging path.** Rejected outright — a standing elevated process is strictly more attack surface than what it replaces, failing "no standing daemon with broad rights" on its face.
- **A Windows Service the agent starts/stops per-operation.** Viable in principle (mirrors `systemd-run`'s transient-unit shape more closely) but means authoring and auditing a custom SCM service dispatcher/control handler in Rust — real new lifecycle code in a security-critical component. Scheduled Task reuses a well-understood, heavily-audited OS primitive instead of building one.
The task definition itself is provisioned **at agent-install time** by the (already-elevated) install script, the same moment Linux drops its sudoers entry — not self-provisioned by the agent on first gated operation, which would just relocate the "who grants the first elevation" problem rather than solve it.
`installer/windows/provision-helper.ps1` now prepares that installer-owned boundary. It
creates one triggerless `\RedFlag\Helper` task with a fixed Helper path and `run-request`
argument, grants the Agent service SID read/run rather than task mutation, and places the
Agent-writable request child beneath a non-Agent-writable Helper root beside SYSTEM-owned
trust, replay, and result directories. The staged
payload validates this description but does not run the provisioning script.
`run-request` accepts no command-line arguments. Its machine exchange is fixed at
`C:\ProgramData\RedFlag\helper`; the installer refuses alternate data roots for this
transport. It opens each component relative to a retained directory handle, rejects
reparse points and hard-linked files, bounds input size, and denies concurrent write/delete
sharing while reading. The RedFlag and Helper roots, authority file, and result directory
must pass owner/DACL checks. A handle lock serializes dispatcher instances.
The Agent writes `requests\pending.json`: `version`, a fresh UUID `request_id`, an action
(`verify-envelope` or `execute-envelope`), and the unchanged `envelope`. Unknown envelope
fields and duplicate struct fields are refused. Installer-owned `authority.json` contains
`version`, `target_id`, `authority_kind` (`standalone` or `fleet-server`), `authority_id`, and
`public_keys` (Ed25519 hex). This is local trust configuration. Provisioning accepts it
through `-AuthorityFile`, but refuses to replace an existing different authority; enrollment
and rotation need their own explicit transaction.
The result is a create-new `results\<request_id>.json` containing `version`, `request_id`,
`envelope_verified`, and the common joined `receipt`. Read-only verification reports
`decision=verified`, `executed=false`, and zero verified backend actions. Backend execution
currently reports `backend_not_migrated`; neither path consumes replay authorization.
Protected results are flushed and closed before the Agent can read them. Results stay as
local evidence; retention and timed-out request reconciliation remain follow-up work.
The Agent's Windows envelope executor triggers only `\RedFlag\Helper`, serializes its one
pending slot across processes, validates the full receipt join, and leaves an unresolved
slot intact after task-start failure, timeout, cancellation, or an invalid response. Task
start never counts as execution success. A later call first reads any pending request and
its protected result. A valid joined result retires the slot and is returned when the caller
retried the same action, authorization, and manifest; a different call may proceed while the
old result stays as evidence. Missing, malformed, or mismatched results keep the slot blocked
with its request ID for reconciliation. Windows standalone mint explicitly refuses until
fresh StepUp exists. New Agent service installs select the virtual
`NT SERVICE\RedFlagAgent` account and enable its service SID. Helper provisioning grants
that SID modify access to Agent state/logs, modify access to the untrusted request directory,
read access to results, and no access to authority/replay state. Existing SYSTEM services
use the explicit `-MigrateAgentService` transaction: enable the service SID, prepare ACL/task
ownership while LocalSystem is still available, stop the service, change its account, verify
SCM state, and restore its prior running state. Only an observed LocalSystem service is
migrated; another operator-owned identity is refused. If the new identity cannot start, the
script restores LocalSystem and its prior running state while retaining the prepared narrow
ACLs for a later retry. Native transport acceptance remains open.
The ACL lockdown described here is the v1 cut, not the final word — Casey's call ("that'll do for now"). Revisit if a real gap in the ACE-delegation model surfaces (see `docs/tasks/SEC-030-windows-privileged-mutation-helper.md` Open Questions for what's still unresolved: WUA's COM-driven install path, rollback ownership parity with Linux's `.bak` handling).
---
## The Verification Pipeline
`run()` executes, in order — any failure stops the operation:
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.
---
## Binary Self-Update Path
Agent, helper, and desktop binaries update through the same gate as packages: `stage_and_verify_binary` (hash check before anything moves) → `atomic_replace_binary` (rename, never write-in-place; failed swap leaves `<binary>.bak`). During agent upgrades, `reconcile_agent_unit_dropin()` heals fleet systemd units to the current template — this is how pre-`AmbientCapabilities` units get fixed without manual fleet surgery. The supported deployment surface is the root `docker-compose.yml` and `OPERATIONS.md`.
---
## Mutation Envelopes and Pacman
`verify-envelope` remains inspection-only: it verifies shape, target, trust key,
signature, time, lifetime ceiling, and backend payload without consuming replay state or
executing.
`execute-envelope` is live for the pacman backend. Before the local standalone authority
signs, `mint-envelope` requires every action to carry an exact official-repository
archive and detached signature. The helper copies those files into root custody and
checks:
1. archive and signature SHA-256;
2. package name and version read from the archive with `pacman -Qp`;
3. the detached package signature with `pacman-key --verify`; and
4. forward-only movement against the installed version with `vercmp`.
The signed backend payload marks exactly one requested root. An `upgrade` must move that
root strictly forward; an `install` must introduce an absent root. Dependencies may already
be satisfied, but none may move backward.
Execute mode verifies the signed envelope, replay-claims the authorization, repeats the
custody and package checks, and invokes one fixed `pacman -U --noconfirm -- ...` plan. The
resulting `MutationReceipt` joins authorization, decision,
exit code, and the number of fully verified actions; exit zero cannot mean pacman silently
skipped an already-satisfied transaction.
Fleet pacman envelope delivery is not implemented yet. The executor exists; the Server
does not yet mint and deliver this envelope to a fleet Agent.
## Standalone Mint Mode
The helper carries two local-authority protocols: legacy `MintRequest` / `MintedToken`
for APT and DNF, and `EnvelopeMintRequest` / `MutationEnvelope` for pacman. Both load a
root-owned, owner-and-mode-validated Ed25519 key with no-follow semantics. Exchange-file
reads walk fixed directories with `openat`; writes are create-new and no-follow. Request,
response, envelope, and receipt filenames must join on the same UUID.
The helper validates evidence shape and freshness, but it does not independently re-run
OSV or attest the human operator. Root-owned key material therefore constrains the
privileged protocol without reproducing fleet authority against a compromised Agent.
Design of record: [security/06-standalone-authority](../security/06-standalone-authority.md).
---
## Why a Separate Binary, Why Rust
- **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-09-01*