6.4 KiB
Helper Component
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 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 for the token contract this enforces.
Invocation
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.
Windows Invocation (SEC-030, decided 2026-07-01)
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.
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:
- Token shape and time — reject an unsupported token version, a not-yet-valid token, or an expired token.
- Host binding — compare the token's
agent_idwith an independently read local identity. - 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. - 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.
- 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. - 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. - Replay record —
replay_check_and_record()records the token ID before execution; a token runs at most once even across a crash. - Execute —
execute_plan()invokes the fixed argv directly, without a shell, afterenv_clear()and a fixedPATH. - 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 (deployment/01-docker-stack).
Standalone Mint Mode
The helper also carries a local-authority minting path (MintRequest / MintedToken / load_mint_key) for deployments where the signing authority runs beside the host rather than on a central server. Design of record: security/06-standalone-authority.
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-08-25