Source-Sha: 194a59adfd6c901635da7a9c18253bb2210e2426 Policy-Sha: b9b0be0ee74e55f561a803b8aef6be3c2134f5a83b46816a069a372f2eb04f4d Tree-Digest: 4697f2e281c1c6ecafff17cf6eeb391a0a1ac0fa6e5fc429f5b473f4579c252c
180 lines
14 KiB
Markdown
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*
|