Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/RAF/components/04-helper.md
Fimeg ff2f30f47a v0.2.9.3: device classification + ARM support — Pixel 3 lands
DEVICE-002: ARM machine-ID fallback — device-tree model + /etc/machine-id
combo, then /proc/cpuinfo Serial (all-zero rejected), before the weak
hostname fallback. Hardware-bound IDs on DMI-less devices.

DEVICE-001: agent detects device_type (server/desktop/phone/tablet) from
/sys signals — system battery (scope=Device peripherals excluded, UPS
excluded), DRM connector state, framebuffer min-dimension for phone/tablet
split. Reports device_type/device_model/os_distro in registration and
system-info paths.

SERVER-001: migration 061 — device_type, device_type_manual (operator
override, never agent-written), device_model, os_distro on agents.
effective_device_type computed into every serialized agent.

SERVER-002: PUT /admin/agents/:id/device-type — set/clear override,
enum-validated, journaled.

WEB-001: device-type icons + fleet filter, device model in list, detail
header badge with reclassify dropdown, os_distro surfaced.

INSTALL-003: arm64 install path unblocked — helper (required manifest
component) now cross-built aarch64-unknown-linux-musl via rust-lld in the
server image, signed at boot (helperArches += arm64), listed in the release
manifest. Install template already handled uname -m and pacman.

Plus in-flight: desktop tray wiring, enrollment page polish, CI workflow
updates, RAF session-broker/pacman-scanner docs, native installer scaffold.
2026-07-06 18:21:23 -04:00

6.1 KiB

Helper Component

A privileged, network-less Rust executor that trusts nothing it didn't verify itself — the last gate before 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.

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

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.

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 world:

  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 hashclosure_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).
  4. Signatureverify_signature() checks the token's Ed25519 signature over the signed message (which embeds the closure hash) against the keyring, by key_id.
  5. Artifact hashesverify_artifacts() SHA-256s every artifact the token authorizes. A mismatch anywhere is a denial.
  6. Replay checkreplay_check_and_record(): token IDs are recorded in local state; a token executes once.
  7. Plan + executebuild_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. Receiptemit_result() writes a PolicyResult the agent reports back; the server reconciles it into 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, 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.
  • Small audit surface: one file, explicit pipeline, typed denials. The binary is meant to be read.

Last reviewed: 2026-06-14