Watch
1
0
Fork
You've already forked RedFlag
0

raf: record the standalone pacman boundary

Pacman now has one local envelope path from Desktop intent to a joined helper receipt.

Name the same-host authority limit, missing fleet delivery, and unsupported Arch advisory mapping without painting them green.
This commit is contained in:
Fimeg 2026-09-01 14:14:00 -04:00
commit 213918f1a2
9 changed files with 543 additions and 369 deletions

View file

@ -4,7 +4,7 @@ I'm **Casey Tunturi** — Fimeg in the community. Systems architect, 25+ years o
## Why RedFlag is free ## Why RedFlag is free
RedFlag is my resume piece. It will never be monetized no pro tier, no cloud edition, no per-agent pricing. The update manager is part of your attack surface, and everyone deserves one that treats it that way — not just organizations with an RMM budget. The code is MIT, the architecture is documented in the open, and that's the whole business model: there isn't one. RedFlag is my resume piece. It will never be monetized: no pro tier, no cloud edition, no per-agent pricing. The update manager is part of your attack surface. Everyone deserves one that treats it that way, not just organizations with an RMM budget. The code is AGPL-3.0, the architecture is documented in the open, and that's the whole business model: there isn't one.
If you're evaluating whether I can build the thing you need built, this repository is the interview. Read the code, read the architecture docs, run it against your fleet. If you're evaluating whether I can build the thing you need built, this repository is the interview. Read the code, read the architecture docs, run it against your fleet.

View file

@ -9,20 +9,23 @@ understand the shape of the system, then follow the links into the detailed docs
## What RedFlag Is ## What RedFlag Is
A self-hosted update management platform for homelabs and small MSPs. Centralized A machine knowledge and operations system with a self-hosted fleet surface. Agents
visibility and control over software updates across Linux, Windows, and Docker — with report operating-system, process, socket, service, container, software, update, and
a cryptographic supply chain gate that most commercial RMM tools don't attempt. security state. RedFlag connects those facts to human approval, signed authority,
privileged execution, and durable lifecycle history instead of treating inspection and
mutation as separate products.
--- ---
## The Two Capability Tiers ## Operating Surfaces and Authority Tiers
### Tier 1: Update Management ### Tier 1: Machine Observation and Fleet Lifecycle
Agents register with a one-time token and a hardware fingerprint (TOFU). The server Agents register with a one-time token and a hardware fingerprint (TOFU). The server
issues Ed25519-signed commands; agents verify signatures, check nonces, reject replays. issues Ed25519-signed commands; agents verify signatures, check nonces, reject replays.
Pull-based polling (5 min default, rapid mode available). Subsystem scanning across Pull-based polling (5 min default, rapid mode available). Subsystem scanning across
apt, dnf, winget, WUA, and Docker. apt, dnf, pacman, winget, WUA, and Docker sits beside on-demand process/socket
inspection and local machine telemetry.
Packages move through a server-owned state machine (`pending` through `installed` or Packages move through a server-owned state machine (`pending` through `installed` or
`failed`) with typed transitions and guarded UPDATEs — no free-form string jumps. A `failed`) with typed transitions and guarded UPDATEs — no free-form string jumps. A
@ -86,12 +89,32 @@ role-gated minting is just "anyone can get a root shell."
- [components/06-session-broker](components/06-session-broker.md) — design of record: grant format, - [components/06-session-broker](components/06-session-broker.md) — design of record: grant format,
trust chain, audit trail, sequence diagram, scope variants trust chain, audit trail, sequence diagram, scope variants
### Process Explorer ### RedFlag Desktop
The native Qt/QML Desktop is the local-machine surface. It holds no credential or
package-manager authority and speaks only to the Agent's local socket. It presents live
resources, processes, connections, storage, services, containers, installed software,
updates, security posture, and history, and may ask a standalone Agent to run the same
gate-and-helper authority chain.
Fleet-enrolled Agents refuse local minting. Standalone mode owns a stable local identity,
local scans, APT/DNF capability approval, and pacman `MutationEnvelope` approval. It does
not recreate an off-host authority boundary; its exact limits are published in
[security/06-standalone-authority](security/06-standalone-authority.md).
**Architecture docs:**
- [components/05-desktop](components/05-desktop.md) — native structure, Agent IPC, and local approval
- [security/06-standalone-authority](security/06-standalone-authority.md) — same-host trust boundary
### Process Explorer and Software Ownership
On-demand `/proc` filesystem scanning for process inventory and drill-down detail. On-demand `/proc` filesystem scanning for process inventory and drill-down detail.
Triggered when a user opens the Processes tab — no background broadcasting. Triggered when a user opens the Processes tab — no background broadcasting.
25+ fields per process (osquery parity) plus 7 related data types (open files, 25+ fields per process plus open files, sockets, pipes, environment keys, memory maps,
sockets, pipes, environment keys, memory map, namespaces, listening ports). namespaces, and listening ports. Linux drill-down also reads effective capabilities and
cgroup ownership, attributes a process to a systemd unit or container where the kernel
provides it, and joins the executable back to its installed package when a supported
package manager can prove ownership.
Data collection caps are server-controlled via `ProcessExplorerConfig` (Settings → Data collection caps are server-controlled via `ProcessExplorerConfig` (Settings →
Process Explorer) and delivered to agents on check-in. Listening ports use socket Process Explorer) and delivered to agents on check-in. Listening ports use socket
@ -105,28 +128,34 @@ inode correlation against `/proc/net/tcp` — not system-wide assignment.
## Architectural Boundaries ## Architectural Boundaries
### Agent is a Stateless Executor ### Fleet Lifecycle Is Server-Owned
The agent receives commands, executes them, and reports results. It does not track In fleet mode the Agent receives commands, observes the host, executes authorized work,
lifecycle states. The server owns every state transition. The agent's only autonomous and reports results. It does not own update lifecycle states; the Server owns every
decisions are: verify this signature, check this nonce, reject this replay. transition. Signature, nonce, replay, target, and helper verdicts are local enforcement
decisions. Standalone mode is an explicit exception to server dependence, not to those
checks: it keeps local observations and runs a bounded local scan/approval loop without
inventing a fleet lifecycle.
### Mutation Only Through the Helper ### Gated Mutation Only Through the Helper
On capability-gated ecosystems (dnf, apt), the agent cannot run install commands On capability-gated ecosystems, the Agent cannot run install commands directly. APT and
directly. All mutation flows through `consumer.go``sudo systemd-run --wait` with DNF flow through `consumer.go` and a capability token. Standalone pacman flows through a
token/result files → `redflag-helper`. The agent holds zero sudo for installs. signed `MutationEnvelope` whose exact archives and detached signatures are verified
Discovery (scan, dry-run, hash-resolve) runs unprivileged through `DiscoveryRunner`. before mint and again before execution. Both end at a fixed `redflag-helper` invocation;
the Agent holds no package-manager sudo. Discovery and resolution stay unprivileged.
### Two Execution Paths ### Current Execution Paths
- **Capability gate** (dnf, apt): token minted at approval → agent polls for tokens → - **Capability gate** (dnf, apt): token minted at approval → agent polls for tokens →
helper verifies + executes → agent reports receipt. No install command issued. helper verifies + executes → agent reports receipt. No install command issued.
- **Standalone pacman envelope**: Agent resolves and hashes the official-repository
transaction → local root helper validates custody and signs → execute mode revalidates
and runs one fixed pacman plan → joined receipt returns locally.
- **Legacy command** (docker, winget, windows_update): signed command → agent executes - **Legacy command** (docker, winget, windows_update): signed command → agent executes
directly via type-asserted installer methods → reports via ReportLog. directly via type-asserted installer methods → reports via ReportLog.
The legacy path is a known gap — the gate design covers these ecosystems but Fleet pacman envelope delivery and the remaining legacy ecosystems are known gaps.
implementation is deferred.
### Six Load-Bearing Constraints ### Six Load-Bearing Constraints
@ -163,6 +192,11 @@ From `security/05-supply-chain-gate.md` — do not regress these:
- **Helper network isolation**: the current `systemd-run` unit retains host network access; complete local artifact custody and a private network boundary are not built - **Helper network isolation**: the current `systemd-run` unit retains host network access; complete local artifact custody and a private network boundary are not built
- **Signer in-process**: key encapsulated in SigningService, minter is the only caller — but true process isolation not built - **Signer in-process**: key encapsulated in SigningService, minter is the only caller — but true process isolation not built
- **Legacy ecosystems ungated**: docker, winget, windows_update still direct-mutation - **Legacy ecosystems ungated**: docker, winget, windows_update still direct-mutation
- **Fleet pacman seam**: standalone pacman envelopes execute; the Server does not yet mint and deliver the same envelope to fleet Agents
- **Standalone transition**: local startup and approval work, but standalone-to-fleet join is deliberately refused until local-key retirement and trust replacement are one tested transaction
- **Local attribution**: Desktop's operator label is asserted from the session environment; socket peer identity and fresh step-up are unfinished
- **Local journal surface**: helper decisions are journaled, but Desktop cannot yet browse the root journal and no one-time fleet import exists
- **Desktop platforms**: Linux amd64 is the only native Qt release artifact; Windows transport exists in source without a proved Windows Desktop build
- **Kernel enforcement inert**: eBPF scaffold exists, not wired to the capability model - **Kernel enforcement inert**: eBPF scaffold exists, not wired to the capability model
These are architectural gaps, not bugs. They define where the system's protection These are architectural gaps, not bugs. They define where the system's protection
@ -170,4 +204,4 @@ boundary currently ends. Task tracking for closing them lives in `docs/tasks/`.
--- ---
*Last reviewed: 2026-08-25* *Last reviewed: 2026-09-01*

View file

@ -12,12 +12,12 @@ It describes a system under active development. Some of it will be wrong by the
| Section | Description | | Section | Description |
|---------|-------------| |---------|-------------|
| [OVERVIEW](OVERVIEW.md) | **START HERE**architecture overview: what RedFlag is, the two capability tiers, architectural boundaries, honest gaps | | [OVERVIEW](OVERVIEW.md) | **START HERE**machine and fleet shape, authority tiers, architectural boundaries, honest gaps |
| [core](core/) | ETHOS principles, the foundational architectural decisions | | [core](core/) | ETHOS principles, the foundational architectural decisions |
| [components](components/) | Server, agent, web, helper — component breakdowns | | [components](components/) | Server, Agent, Web, native Desktop, helper — component breakdowns |
| [security](security/) | Trust boundaries, auth stack, refresh-token lifecycle, machine binding, supply chain gate, standalone authority | | [security](security/) | Trust boundaries, auth stack, refresh-token lifecycle, machine binding, supply chain gate, standalone authority |
| [verification](verification/) | Ed25519 signing pipeline, agent verification, key rotation, replay protection | | [verification](verification/) | Ed25519 signing pipeline, agent verification, key rotation, replay protection |
| [scanners](scanners/) | Every scanner (APT, DNF, Winget, WUA, Docker, process explorer) with interaction analysis | | [scanners](scanners/) | Every scanner and resolver (APT, DNF, pacman, Winget, WUA, Docker, process explorer) with interaction analysis |
| [flows](flows/) | Data flows — registration, command execution, upgrade, heartbeat, capability advertisement, update lifecycle | | [flows](flows/) | Data flows — registration, command execution, upgrade, heartbeat, capability advertisement, update lifecycle |
| [deployment](deployment/) | Docker stack, native agent services, CI/CD, release gate, operations runbook pointers | | [deployment](deployment/) | Docker stack, native agent services, CI/CD, release gate, operations runbook pointers |
| [testing](testing/) | Test pyramid, structural tests, live testing, honest gaps | | [testing](testing/) | Test pyramid, structural tests, live testing, honest gaps |
@ -56,6 +56,7 @@ architecture docs."
| Version | Date | Changes | | Version | Date | Changes |
|---------|------|---------| |---------|------|---------|
| 2.3 | 2026-09-01 | Native Desktop machine console, standalone Agent mode, and the first live pacman mutation envelope with signed artifact custody. |
| 2.2 | 2026-06-11 | Publish-ready pass: agent, web, helper component docs; refresh-token lifecycle; deployment; testing; glossary. Public framing. | | 2.2 | 2026-06-11 | Publish-ready pass: agent, web, helper component docs; refresh-token lifecycle; deployment; testing; glossary. Public framing. |
| 2.1 | 2026-06-01 | Updated for v0.2.3.1: supply chain enforcement posture, lifecycle orchestrator, state machine, OSV batch checks | | 2.1 | 2026-06-01 | Updated for v0.2.3.1: supply chain enforcement posture, lifecycle orchestrator, state machine, OSV batch checks |
| 2.0 | 2026-05-26 | Restructured for single-source-of-truth organization | | 2.0 | 2026-05-26 | Restructured for single-source-of-truth organization |

View file

@ -1,12 +1,12 @@
# Helper Component # Helper Component
**A privileged, short-lived Rust executor that independently validates authority before APT/DNF mutation.** **A privileged, short-lived Rust verifier, local minter, and executor for capability-gated mutation.**
--- ---
## Doctrine ## 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 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. 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.
@ -16,7 +16,7 @@ Deny-by-default is the architecture, not a configuration: every failure path ret
## Invocation ## 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](02-agent.md). 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) ### Windows Invocation (SEC-030, decided 2026-07-01)
@ -54,23 +54,47 @@ Agent, helper, and desktop binaries update through the same gate as packages: `s
--- ---
## Dormant Envelope Verification ## Mutation Envelopes and Pacman
`verify-envelope` is the mutation-envelope path (ARCH-002 cut 2). It runs the same `verify-envelope` remains inspection-only: it verifies shape, target, trust key,
pipeline shape as execute mode — parse, host binding against the independently read signature, time, lifetime ceiling, and backend payload without consuming replay state or
`agent_id`, SEC-021 trusted keyring, Ed25519 verification, time window, authorization executing.
lifetime ceiling, backend payload shape — and then refuses every envelope with
`backend_not_migrated`, emitting a `MutationReceipt`.
Two absences are deliberate. It **records no replay state**: execute mode records a token `execute-envelope` is live for the pacman backend. Before the local standalone authority
only after a plan is built, so an operation that cannot run never burns its slot, and signs, `mint-envelope` requires every action to carry an exact official-repository
nothing here can run. And it enforces **no artifact custody**: when a backend carries a archive and detached signature. The helper copies those files into root custody and
local cached artifact as executor input the helper must rehash it, but no backend does checks:
yet, and remote repository execution keeps the limits stated above.
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 ## 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](../security/06-standalone-authority.md). 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).
--- ---
@ -83,4 +107,4 @@ The helper also carries a local-authority minting path (`MintRequest` / `MintedT
--- ---
*Last reviewed: 2026-08-26* *Last reviewed: 2026-09-01*

View file

@ -1,95 +1,139 @@
# Desktop Component # RedFlag Desktop
**A credential-less tray window onto the local agent — it can see, it can ask, it cannot mutate.** **The native local-machine observation and operations surface. QML is the glass; the Agent and helper own the machine.**
--- ---
## Doctrine ## Product role
The desktop app (`desktop/`, Tauri 2) holds no tokens, reads no protected config, RedFlag Desktop is a Qt 6 / QML system monitor and local operations console for one
and never talks to the fleet server. Its entire world is the agent's local API machine. It works in standalone mode and keeps the same domain when the machine joins a
socket, and its authorization is its operating-system identity: the user running it fleet. RedFlag Web asks across machines; Desktop asks here, now, on this body.
is either in the `redflag-local` group or the kernel refuses the connection. See
[security/01-trust-boundaries](../security/01-trust-boundaries.md), Local Trust
Boundary.
The credential-less surface is a design decision, not an inevitability — the The application currently carries eleven native surfaces:
reasoning lives in [security/06-standalone-authority](../security/06-standalone-authority.md):
group membership only gates the *request*; the judgment stays in the gates and the
root-owned mint key, and same-host token schemes don't reproduce an off-host
boundary. The decision is held open in one respect: the socket does not capture
peer identity (`SO_PEERCRED`), so on a multi-user host an approval journals as an
event, not as a person. Whether that needs per-user attribution (peer creds, or a
polkit prompt) is an open call, not settled doctrine.
Fleet-server context, when it comes, joins this same app rather than spawning a - Overview and Performance;
second local-only application. - Processes, Network, Storage, Services, and Containers;
- installed Software and available Updates;
- Security and History.
The useful unit is the join between those surfaces. A process can carry its systemd unit,
container identity, sockets, namespaces, capabilities, and owning package. Package and
update details lead toward dependency closure, advisory evidence, authorization, and
mutation history instead of remaining a separate updater product.
Souveraine Updater no longer owns a product boundary here. Useful closure and package UX
may be absorbed, but its direct `pkexec pacman` authority must not survive. The Tauri /
React Desktop runtime has also been removed; the fleet React application remains RedFlag
Web.
--- ---
## Structure ## Authority boundary
``` ```text
desktop/ RedFlag Desktop (Qt / QML)
├── src/main.rs # Tauri shell: tray, window, localapi client, health reporter observation + operator intent
├── tauri.conf.json # Window config, dev/build frontend wiring |
└── icons/icon.png # 8-bit RGBA — tray icon constructor rejects 16-bit PNGs v
RedFlag Agent local source of truth, resolution, gates
web/ |
├── index.desktop.html # Desktop entry (separate from the fleet SPA's index.html) v
├── src/desktop/ # LocalAgentApp — status dashboard, no auth surface RedFlag Helper privileged verifier / bounded executor
└── vite.desktop.config.ts# Builds to dist-desktop/, emits the entry as index.html |
v
platform backend pacman first; other migrated backends follow
``` ```
One Rust binary, one React entry. The frontend is embedded at build time Desktop never shells out to pacman, Docker, or systemd and never reads a signing key. It
(`frontendDist: ../web/dist-desktop`); release builds are self-contained — no dev speaks HTTP/1.1 over the Agent local socket
server, no localhost dependency. Dev builds (`cargo build` without `tauri build`)
compile in the Vite `devUrl` and require `npm run dev:desktop` running.
The fleet SPA and the desktop entry share the `web/` tree but are distinct
applications: the desktop entry never renders the sign-in screen because it has
nothing to sign in to.
---
## Agent IPC
The Rust shell speaks plain HTTP/1.1 over the local socket
(`/var/lib/redflag/agent/localapi/redflag-agent.sock`; named pipe (`/var/lib/redflag/agent/localapi/redflag-agent.sock`; named pipe
`\\.\pipe\RedFlagAgentLocal` on Windows). The frontend reaches it only through `\\.\pipe\RedFlagAgentLocal` on Windows). The user must be admitted to the
Tauri `invoke` commands — the webview itself has no socket access. `redflag-local` OS group or the kernel refuses the connection.
- `local_status``GET /v1/identity` + `GET /v1/status` — the dashboard snapshot, The current socket still lacks `SO_PEERCRED` attribution and fresh StepUp. Desktop sends
polled every 5s. the session username as an assertion, not an attestation. A same-user process able to
- Health reporter thread → `POST /v1/desktop` every 30s with version, uptime, and reach the socket can express the same intent. The helper constrains what bytes and
window state. This is how the agent (and through it, the fleet) knows a tray is operations can execute; it does not prove which human clicked the button.
alive on the host.
On `EACCES` the shell diagnoses rather than reports: it distinguishes "user not in Fleet enrollment removes local mint authority. Desktop may still display this machine,
`redflag-local`" (gives the usermod command) from "membership added but the login but approval belongs to RedFlag Server and the Agent refuses local authorization.
session predates it" (tells the user to re-log) by comparing `/etc/group` against
the process's live groups. Group membership is stamped at login; this is the
component's most common support question, so the error message carries the answer.
--- ---
## Lifecycle ## Native structure
Installed and provisioned by the agent install script (binary, autostart entry, ```text
group enrollment — `linux.sh.tmpl` step 7c). Updated through the capability gate desktop/
like every other binary: `desktop-self` tokens, hash-verified and atomically ├── Cargo.toml / build.rs
swapped by the helper ([components/04-helper](04-helper.md)). Server-side minting ├── src/
and pre-tray host healing are in progress — `docs/tasks/UPDATE-002`. The component │ ├── main.rs
is not yet in release version lockstep; closing that is `docs/tasks/INSTALL-001`. │ └── bridge/
│ ├── local_api.rs HTTP over Unix socket / named pipe
│ └── machine.rs CXX-Qt state and invokable intent
├── qml/
│ ├── Main.qml
│ ├── Theme.qml
│ ├── MetricCard.qml
│ ├── LineGraph.qml
│ └── NavItem.qml
└── icons/icon.png
```
The Rust bridge polls bounded Agent projections. Live resource telemetry runs at one
second with a 300-point in-memory history. Larger inventory and detail reads are explicit
and asynchronous so the QML thread does not become a second scanner.
If the Agent is reachable but lacks newer routes, Desktop names the missing surface as an
Agent version gap rather than presenting an empty healthy machine. Unknown and unsupported
remain distinct from zero.
--- ---
## Cross-References ## Local update intent
- [security/01-trust-boundaries](../security/01-trust-boundaries.md) — the Local trust boundary this client lives behind `POST /v1/actions/approve-update` is the only Desktop package-approval call. APT and DNF
- [components/02-agent](02-agent.md) — the localapi server side use the closure-capability path. Pacman now uses the mutation-envelope path:
- [components/04-helper](04-helper.md) — how the desktop binary itself updates
1. the Agent refreshes a private pacman database and resolves one exact dependency
transaction without touching live package state;
2. it downloads every archive and detached signature into a private cache and records
exact identity, repository, locations, and hashes in the manifest;
3. the standalone helper stages those bytes under root custody, verifies identity and
Arch signatures, enforces non-decreasing versions, checks gate evidence, and signs the
envelope with the root-owned local authority;
4. execution repeats custody, hash, identity, signature, and version checks, atomically
consumes `authorization_id`, runs one fixed pacman argv, and returns a joined receipt.
OSV has no Arch ecosystem mapping in the current RedFlag gate. Desktop displays
`unsupported` and requires a recorded override reason; it never turns missing advisory
coverage into a green check.
Override uses this same path. It is intent plus a reason inside the same authority and
journal, not a second privileged button.
--- ---
*Last reviewed: 2026-06-14* ## Lifecycle and proof
The native Linux Desktop is built by Gitea Actions with Qt 6 and CXX-Qt. Windows still has
Agent support but no native Qt/MSVC Desktop artifact until a suitable runner and packaging
path exist. Source presence is not an installed or runtime proof.
Desktop self-update remains a capability-gated helper swap. Installation must also provide
the local socket group, autostart/background presence, and exact helper sudo protocols;
QML does not compensate for incomplete provisioning.
---
## Cross-references
- [security/01-trust-boundaries](../security/01-trust-boundaries.md)
- [security/06-standalone-authority](../security/06-standalone-authority.md)
- [components/02-agent](02-agent.md)
- [components/04-helper](04-helper.md)
- [scanners/06-pacman-scanner](../scanners/06-pacman-scanner.md)
---
*Last reviewed: 2026-09-01*

View file

@ -1,92 +1,98 @@
# Pacman Scanner # Pacman Scanner and Resolver
**Arch Linux package manager scanning via pacman-contrib's checkupdates.** **Arch update discovery stays unprivileged; approved mutation crosses the signed helper envelope.**
--- ---
## Component Details ## Discovery
| Property | Value | | Property | Value |
|----------|-------| |----------|-------|
| Method | `checkupdates --color=never` | | Method | `checkupdates --color=never` |
| Platform | Linux (Arch, Manjaro, EndeavourOS) | | Platform | Arch Linux and pacman derivatives |
| Execution time | ~5-15 seconds per scan | | Output | `pkgname oldver -> newver` |
| Output format | `pkgname oldver -> newver` (one per line) | | Root required | No |
| Failure modes | pacman-contrib not installed, network timeout, database lock | | Requirements | `pacman`, `pacman-contrib`, `fakeroot` |
| Root required | No (checkupdates uses a private db copy) |
`agent/internal/scanner/pacman.go` runs pacman-contrib's `checkupdates` through
`DiscoveryRunner`. `checkupdates` synchronizes a private database and compares it with the
installed local database. The Agent has no `pacman -Sy` sudo grant and discovery never
refreshes live sync state.
Epoch-bearing versions remain opaque pacman versions. The parser preserves a line such as
`fakeroot 1:1.37.2-1 -> 1:1.37.2-2`; it does not split or reinterpret the epoch.
--- ---
## Implementation ## Standalone closure resolution
**File:** `agent/internal/scanner/pacman.go` `agent/internal/installer/pacman_resolver_linux.go` owns the approval-time resolver:
### Detection 1. create an Agent-private operation directory, sync database, and cache;
2. symlink only the installed `local` database into that private database;
3. run `fakeroot pacman -Sy` against the private database with a cleared, C locale;
4. resolve the requested exact `name=version` transaction and repository identities;
5. download the transaction with `pacman -Sw` into the private cache;
6. inspect each archive with locale-pinned `pacman -Qp`, require its adjacent detached
signature, and hash both files;
7. keep the cache alive through helper mint and execution, then remove it.
```go The resolver returns an error rather than guessing when the requested root is absent, the
func (s *PacmanScanner) IsAvailable() bool { repository join is missing, an archive or signature is not regular, identity output is
// Requires both pacman and checkupdates (from pacman-contrib) ambiguous, or the transaction is empty.
if _, err := exec.LookPath("checkupdates"); err != nil {
return false
}
_, err := exec.LookPath("pacman")
return err == nil
}
```
### Scanning
Uses `DiscoveryRunner` (same as apt/dnf) for sandboxed execution:
```go
func (s *PacmanScanner) Scan() ([]client.UpdateReportItem, error) {
runner, err := installer.NewDiscoveryRunner("pacman")
// checkupdates --color=never
result, err := runner.Run(context.Background(), "--color=never")
// Parse "pkgname oldver -> newver" lines
}
```
### Output Parsing
`checkupdates` output format:
```
fakeroot 1:1.37.2-1 -> 1:1.37.2-2
linux 6.14.6.arch1-1 -> 6.15.1.arch1-1
```
The current (installed) version is embedded in the output — no secondary
query needed (unlike dnf which calls `rpm -q`).
--- ---
## Sudoers ## Privileged path
The agent user needs `checkupdates` in sudoers. This is an unprivileged The Agent encodes each package as `name@version` plus an exact JSON payload containing:
discovery command (no mutation). Example:
- repository;
- whether this action is the one requested root;
- archive cache path and SHA-256;
- detached-signature cache path and SHA-256.
The standalone helper will not sign a pacman envelope without every detached signature.
Before mint it copies Agent paths into a root-owned `0700` operation directory, verifies
both hashes, checks archive identity with `pacman -Qp`, verifies the detached signature
through the Arch keyring, and uses pacman's `vercmp` against installed state. Execution
repeats those checks, atomically consumes the authorization, and invokes:
```text
/usr/bin/pacman -U --noconfirm -- <staged archives...>
``` ```
redflag-agent ALL=(ALL) NOPASSWD: /usr/bin/checkupdates
``` Source and exchange files are opened without following the final symlink. Helper exchange
directories are walked component-by-component with `openat(..., O_NOFOLLOW)` and result
files are create-new, so an Agent-controlled path cannot redirect a root write through a
swapped directory or pre-existing link.
There is no `--needed`: a successful receipt cannot conceal a package-manager skip. The
helper requires exactly one requested root. `upgrade` requires that root to be installed at
a strictly older version; `install` requires it to be absent. Equal versions remain valid
dependencies, while every older target is refused.
--- ---
## Known Limitations ## Limits
- **Requires pacman-contrib**`pacman` alone is not sufficient. The - AUR packages are not scanned or resolved.
`checkupdates` binary is in the `pacman-contrib` package, which may not - Requested-root identity lives in the signed pacman payload rather than the common
be installed by default. manifest, so other backends do not inherit pacman semantics accidentally.
- **AUR packages not scanned**`checkupdates` only checks official repos. - RedFlag currently has no OSV ecosystem mapping for Arch. Local approval records
AUR helpers (yay, paru) have their own update checkers but are not `unsupported` and requires an explicit override reason.
integrated. - Package signature verification proves the artifact against the local Arch keyring. It
- **No dry-run for mutations** — pacman's `--print` flag exists but is not does not add reproducible-build or transparency-log evidence.
used for hash resolution. Artifact hashing goes through the capability
gate path when it lands for pacman.
--- ---
## Cross-References ## Cross-references
- [core/02-architecture-decisions](../core/02-architecture-decisions.md) — DiscoveryRunner pattern - [security/05-supply-chain-gate](../security/05-supply-chain-gate.md)
- [security/05-supply-chain-gate](../security/05-supply-chain-gate.md) — capability token path (future for pacman) - [security/06-standalone-authority](../security/06-standalone-authority.md)
- `agent/internal/scanner/detect.go` — scanner registration - [components/04-helper](../components/04-helper.md)
- `protocol/README.md`
---
*Last reviewed: 2026-09-01*

View file

@ -1,6 +1,6 @@
# Supply Chain Gate # Supply Chain Gate
**Package-manager authorization with signed capability tokens; full closure custody, network isolation, and kernel enforcement remain design work.** **Package-manager authorization with signed capabilities and mutation envelopes; pacman has full local artifact custody, while network isolation and kernel enforcement remain design work.**
This is the design of record for the gate — the capability model, the wire contract all This is the design of record for the gate — the capability model, the wire contract all
three components agree on, the load-bearing constraints, and component responsibilities. three components agree on, the load-bearing constraints, and component responsibilities.
@ -12,19 +12,28 @@ Build status and per-step implementation tracking live in `docs/tasks/GATE-000-s
RedFlag's supply chain gate inverts the traditional defense model: instead of **allow all installs and detect bad ones**, it **denies all state changes and requires explicit human authorization**. RedFlag's supply chain gate inverts the traditional defense model: instead of **allow all installs and detect bad ones**, it **denies all state changes and requires explicit human authorization**.
**Current enforced scope:** APT and DNF mutation through RedFlag requires a signed **Current source scope:** APT and DNF mutation through RedFlag uses a signed capability
authorization token. Docker, Winget, and Windows Update still use the default-strict token. Standalone pacman approval uses a signed `MutationEnvelope`, exact closure archives,
signed-command path, and kernel enforcement against out-of-band root mutation is not wired. detached package signatures, helper custody, and a joined receipt. Docker, Winget, and
Windows Update still use the signed-command path. Fleet pacman envelope delivery and kernel
enforcement against out-of-band root mutation are not wired.
--- ---
## The decision: capability tokens, not a decision daemon ## The decision: capability tokens, not a decision daemon
The gate authorizes APT/DNF installs with **signed capability tokens**, not with a runtime The gate authorizes APT/DNF installs with **signed capability tokens** and pacman with a
decision daemon. The server is the authority: it evaluates policy and mints an **signed mutation envelope**, not with a runtime decision daemon. In fleet mode the Server
Ed25519-signed token describing exactly one operation over the artifact entries the agent is the authority: it evaluates policy and mints an Ed25519-signed capability over the
resolved and reported. The top-level hash is required. Dependency hashes are best-effort; artifact entries the Agent resolved and reported. In standalone mode the short-lived root
an unresolved dependency is logged and omitted today rather than making the report fail. helper mints locally after validating the bounded request; this preserves a privilege
boundary, not the fleet's off-host authority boundary.
APT/DNF still require the top-level hash while dependency hashes are best-effort: an
unresolved dependency is logged and omitted rather than making the report fail. Pacman is
stricter. The Agent resolves and downloads the transaction from signed repository metadata,
and the helper requires an exact archive and detached signature for every action before it
will sign or execute.
A small, privileged **executor** (`helper/`, Rust) validates token version and time, host A small, privileged **executor** (`helper/`, Rust) validates token version and time, host
binding, signature, and replay state, then runs a fixed argv plan without a shell or inherited binding, signature, and replay state, then runs a fixed argv plan without a shell or inherited
@ -32,9 +41,9 @@ environment. It rehashes local artifact files and requires a readable matching f
mirror entry. A normal registry entry without a local path is not rehashed helper-side, and mirror entry. A normal registry entry without a local path is not rehashed helper-side, and
the current transient unit retains host network access. the current transient unit retains host network access.
The intended boundary is stronger: resolve and stage the complete transitive closure, Pacman now resolves and stages the transaction and rehashes every byte at the privileged
rehash every byte at the privileged edge, then run the executor in a network-isolated unit. edge. APT/DNF full-closure custody and a network-isolated helper unit remain explicit design
That remains explicit design work, not a present guarantee. work, not present guarantees.
This replaced the earlier `rs-helper` socket-decision daemon. `rs-helper`'s reusable parts This replaced the earlier `rs-helper` socket-decision daemon. `rs-helper`'s reusable parts
(the eBPF `InterceptEvent` struct, the package-manager allowlist, the hash cache) moved into (the eBPF `InterceptEvent` struct, the package-manager allowlist, the hash cache) moved into
@ -70,7 +79,9 @@ The two tests converge on the same answer, which is the signal it's right.
| **Windows** | WDAC (Windows Defender Application Control) | winget, npm.cmd, pip.exe, choco, scoop | | **Windows** | WDAC (Windows Defender Application Control) | winget, npm.cmd, pip.exe, choco, scoop |
| **macOS** | Endpoint Security Framework (ESF) | brew, pip, npm, bun, cargo | | **macOS** | Endpoint Security Framework (ESF) | brew, pip, npm, bun, cargo |
**Crucial distinction:** Enforcement sits **below** userspace wrappers. A prompt-injected Claude Code session cannot bypass it with absolute paths or environment manipulation. **Target distinction:** kernel enforcement sits below userspace wrappers. The current eBPF
scaffold is not connected to the capability model, so RedFlag does not yet claim that an
out-of-band package-manager invocation is blocked.
### Trust Chain ### Trust Chain
@ -150,19 +161,19 @@ signature = ed25519_sign(authority_priv, signed_message)
The closure is sorted before hashing so ordering can't change the digest. Tampering with any The closure is sorted before hashing so ordering can't change the digest. Tampering with any
artifact, version, or hash changes `closure_hash` and breaks verification. artifact, version, or hash changes `closure_hash` and breaks verification.
### Mutation manifest contract (pinned, not wired) ### Mutation manifest contract (live for standalone pacman)
The closure token above remains the only format the runtime mints and executes. RedFlag also The closure token remains live for APT and DNF. Pacman is the first runtime backend migrated
pins a dormant `MutationEnvelope` contract in Go and Rust so new backends can converge without to `MutationEnvelope`, so new backends can converge without extending the closure metaphor
extending the closure metaphor or preserving its unsigned `source`/`artifact_path` ambiguity. or preserving its unsigned `source`/`artifact_path` ambiguity.
No current operation uses this envelope yet.
Those two unsigned fields steer helper *verification* today, not helper *execution*: Those two legacy unsigned fields steer capability-token *verification* today, not helper
execution:
`build_plan` reads only name, version, package type, and operation, and the self-update `build_plan` reads only name, version, package type, and operation, and the self-update
branches take their source path from a helper constant. The server never populates branches take their source path from a helper constant. The server never populates
`artifact_path` and the agent reports `source=registry`, so the mirror branch is currently `artifact_path` and the agent reports `source=registry`, so the mirror branch is currently
unreached. The defect becomes executable the first time a backend puts a real cached path unreached. Pacman's envelope avoids that defect: repository, requested-root marker, cache
in front of the executor — which is why the manifest lands before pacman, not after. locations, archive hash, and signature hash all live inside each signed backend payload.
The envelope carries two objects, and the executor answers with a third: The envelope carries two objects, and the executor answers with a third:
@ -203,11 +214,13 @@ provenance, execution location, target, backend, resolved action, duplicates, or
authorization metadata, validity, decision, identifier shape, lifetime ceiling, and unknown authorization metadata, validity, decision, identifier shape, lifetime ceiling, and unknown
formats. formats.
`redflag-helper verify-envelope` runs an envelope through the helper's real pipeline — `redflag-helper verify-envelope` remains an inspection-only compatibility path: it proves
parse, host binding, trusted keyring, signature, time, lifetime, backend payload shape — the common envelope and refuses without consuming replay state. `mint-envelope` is the
and refuses with `backend_not_migrated`. It records no replay state and enforces no standalone pacman authority. It requires exactly one requested root, detached signatures,
artifact custody: nothing executes through it yet, so it must not consume a replay slot, strict operation semantics (`upgrade` moves that root forward; `install` introduces it),
and cached-byte enforcement arrives with the first backend that carries one. and non-decreasing dependencies. `execute-envelope` repeats identity, hash, signature, and
version checks over a fresh root stage, atomically claims the authorization, and runs one
fixed pacman plan without `--needed`.
### Reuse, don't reinvent ### Reuse, don't reinvent
@ -223,19 +236,22 @@ The token extends the existing Ed25519 infrastructure rather than introducing ne
## Component responsibilities ## Component responsibilities
- **Server (authority).** For dnf/apt, persist the agent-reported resolved entries, run OSV - **Server (fleet authority).** For dnf/apt, persist the agent-reported resolved entries, run OSV
over that set, and mint a host-bound token after the approval boundary. The top-level hash over that set, and mint a host-bound token after the approval boundary. The top-level hash
is required, but the current agent may omit a dependency whose hash did not resolve. Full is required, but the current agent may omit a dependency whose hash did not resolve. Full
transitive resolution, the mirror tier, and signer process isolation remain target work. transitive resolution, the mirror tier, and signer process isolation remain target work.
- **Agent consumer (unprivileged).** Run discovery and best-effort hash resolution, receive - **Agent consumer (unprivileged).** Run discovery and resolution, receive or request the
the token, confirm `agent_id` is this host, and hand it to the executor. It holds no signing capability, confirm `agent_id` is this host, and hand it to the executor. It holds no
key and has no direct APT/DNF mutation method. signing key and has no direct package-manager mutation method. For pacman it owns a private
sync database/cache and retains those files until mint and execution finish.
- **Executor (`helper/`, privileged, Rust).** Verify validity window → resolve trusted key by - **Executor (`helper/`, privileged, Rust).** Verify validity window → resolve trusted key by
`key_id` from a local pinned keyring → reconstruct `signed_message` → Ed25519 verify → `key_id` from a local pinned keyring → reconstruct `signed_message` → Ed25519 verify →
rehash each locally available artifact (and require mirror paths) → build a fixed argv plan rehash each locally available artifact (and require mirror paths) → build a fixed argv plan
→ replay-guard on `token_id` → exec without a shell or inherited environment → structured → replay-guard on `token_id` → exec without a shell or inherited environment → structured
result + exit code. Registry entries without local paths are not rehashed. The current unit result + exit code. For pacman it also stages archives and signatures into root custody,
is not network-isolated. verifies the local Arch keyring, and refuses downgrades or operation-name lies. Legacy
registry entries without local paths are not rehashed. The current unit is not
network-isolated.
- **Kernel layer (where applicable).** Linux eBPF / Windows WDAC / macOS ESF deny - **Kernel layer (where applicable).** Linux eBPF / Windows WDAC / macOS ESF deny
package-manager execution except via the trusted executor. This is defense-in-depth design; package-manager execution except via the trusted executor. This is defense-in-depth design;
the present eBPF scaffold is not wired to the capability model. the present eBPF scaffold is not wired to the capability model.
@ -419,9 +435,14 @@ override: flagged updates come back in
Auto-confirm shares the `ClosureCleared` predicate with manual approval — the two paths cannot Auto-confirm shares the `ClosureCleared` predicate with manual approval — the two paths cannot
drift on what counts as a clean closure. drift on what counts as a clean closure.
Standalone pacman does not claim that predicate. RedFlag has no Arch OSV ecosystem mapping
in this path, so the local gate records `unsupported` and requires an explicit operator
reason. Archive hashes and Arch package signatures still remain mandatory; the reason waives
only the missing advisory coverage.
--- ---
## Build sequence (design intent) ## Build sequence (lineage)
The order the gate is built in. Per-step *status* is tracked in The order the gate is built in. Per-step *status* is tracked in
`docs/tasks/GATE-000-supply-chain-gate-plan.md`; this records the intended dependency order. `docs/tasks/GATE-000-supply-chain-gate-plan.md`; this records the intended dependency order.
@ -436,7 +457,10 @@ The order the gate is built in. Per-step *status* is tracked in
6. Mirror tier (optional): pull+hash closure at approval; verified-cache fallback. 6. Mirror tier (optional): pull+hash closure at approval; verified-cache fallback.
7. Kernel adapters wire the executor as the only permitted caller. 7. Kernel adapters wire the executor as the only permitted caller.
Steps 12 are the contract. Everything else hangs off them. Steps 15 produced the capability-token path. Pacman then exercised the envelope migration:
private resolution, local root mint, detached signatures, custody, strict forward semantics,
execution, and receipt are wired in source. Fleet envelope mint/delivery, the mirror tier,
and kernel adapters remain.
--- ---
@ -456,12 +480,13 @@ Steps 12 are the contract. Everything else hangs off them.
are defense-in-depth on top, not a prerequisite. Userspace wrappers alone are bypassable. are defense-in-depth on top, not a prerequisite. Userspace wrappers alone are bypassable.
**Current:** The privileged executor has a narrow argv-only API, no shell, and a stripped **Current:** The privileged executor has a narrow argv-only API, no shell, and a stripped
environment; the agent that hands it tokens is unprivileged and holds no signing key. The environment; the Agent that hands it capabilities is unprivileged and holds no signing key.
transient unit still has host network access. Pacman archives and detached signatures cross root custody before a fixed offline `pacman
-U`; legacy APT/DNF registry entries may still require network. The transient unit retains
host network access.
**Target:** Stage and re-verify the full artifact closure, then enforce network isolation on **Target:** Bring APT/DNF to the same full-custody envelope and enforce network isolation on
the helper unit. Neither property should be claimed until the invocation and artifact path the helper unit. Neither property applies globally until each migrated backend proves it.
make it true.
**Connection:** [security/01-trust-boundaries](01-trust-boundaries.md) (kernel enforcement as trust boundary) **Connection:** [security/01-trust-boundaries](01-trust-boundaries.md) (kernel enforcement as trust boundary)
@ -469,4 +494,6 @@ make it true.
--- ---
*Last reviewed: 2026-09-01*
*Last reviewed: 2026-08-26* *Last reviewed: 2026-08-26*

View file

@ -1,161 +1,188 @@
# Standalone Authority (Local Mode) # Standalone Authority (Local Mode)
Drafted 2026-06-10. Status: APPROVED 2026-06-10 (Casey signed off on all three open Status: implemented for local Agent startup, scanning, APT/DNF capability approval,
questions; resolutions inlined below). and pacman envelope approval. Fleet join remains fail-closed and unfinished.
Companion to `05-supply-chain-gate.md` (the gate contract is unchanged by this doc).
Build tracking: `docs/tasks/FEAT-003-standalone-local-authority.md`.
## The problem Companion to [05-supply-chain-gate](05-supply-chain-gate.md). Standalone changes where
authority lives; it does not create a pretend off-host boundary on one machine.
The tray app needs a local approval flow ("approve update → it installs") on a host with ---
no fleet server. In fleet mode the trust chain is:
## The Boundary That Exists
Fleet mode has a real host boundary:
``` ```
operator → server (authority, off-host) → agent (consumer) → helper (enforcer) operator → Server authority → Agent consumer → root helper
``` ```
The authority lives on a different machine than the thing being updated. That is the real Standalone has only a privilege boundary:
boundary: a fully compromised agent host still cannot mint a capability token.
In standalone mode the server does not exist. Something on the same host must mint ```
tokens. **No arrangement of processes on one host reproduces the off-host boundary** Desktop user → local Agent → fixed sudoers invocation → root helper and local key
local root can always reach the key. Pretending otherwise (e.g. "the agent signs its own ```
tokens, the helper checks them") turns the token into self-attestation theater. This doc
is honest about what survives and designs for that.
## What the gate still buys on one host The local Ed25519 private key is root-owned and unreadable by the Agent. That stops the
long-running Agent from copying or directly using key material, and the helper still
constrains every privileged action to a typed protocol. It does **not** make a compromised
Agent independent of the signer: the Agent is intentionally allowed to invoke the fixed
mint command and supplies the gate evidence the helper validates. The helper does not
re-run OSV or cryptographically prove that a human supplied the operator label.
The boundary that *is* preservable locally is the OS privilege boundary: The honest threat table is therefore:
| Defended against | Fleet mode | Standalone | | Property | Fleet | Standalone |
|---|---|---| |---|---|---|
| Tampered local/mirror artifact (hash mismatch) | yes | **yes** — helper rehashes a supplied local path | | Host-bound signed authorization | yes | yes |
| Registry artifact with no local path | not helper-rehashed today | **not helper-rehashed today** | | Replay protection | yes | yes |
| Known-vuln package (OSV) | yes, over reported resolved entries | **yes, over reported resolved entries** | | Fixed argv, cleared environment, no shell | yes | yes |
| Too-new package (age/soak gates) | fleet policy | **not yet applied** — local evidence records `not_applicable` | | Pacman archive hash, identity, and detached signature verified by root helper | yes when fleet envelopes land | yes |
| Unprivileged local malware minting installs | yes | **yes** — mint key is root-owned | | APT/DNF local artifacts rehashed when a closure entry names a path | yes | yes |
| Compromised agent process | yes (server refuses) | **yes** — agent user cannot read mint key | | Human authority survives Agent compromise | yes, authority is off-host | **no** |
| Local root attacker | yes (authority off-host) | **no — out of scope, say so in docs** | | Signing key survives managed-host compromise | yes, authority is off-host | **no** |
| Out-of-band mutation by local root prevented | **no, kernel enforcement is not wired** | **no** |
| OSV judgment independently reproduced by helper | no | no |
A compromised *agent* (unprivileged) in standalone can request a mint, exactly as a Standalone still materially narrows mutation. It does not turn same-host signing into
compromised agent in fleet mode can request approval — and the same gates refuse it. proof that the host was uncompromised.
What standalone loses is only the root-compromise case, which on a single sovereign
host is game over for every other tool too.
## Design ---
### Authority placement: privileged mint, root-owned key ## Provisioned State
Standalone mint runs as a **separate privileged invocation of the helper** `scripts/provision-standalone-authority.sh` runs as root after the base Agent, helper,
(`redflag-helper --mint`), the same `sudo systemd-run` pattern the executor already `redflag-agent` user, and `redflag-local` group exist. It is idempotent and must never run
uses. Rationale: on a fleet-enrolled host.
- Keeps the artifact set at three (`agent`, `server`, `helper`) — no fourth binary. Provisioning creates or verifies:
- The helper is already the audited, privileged, short-lived Rust component
("auditable in one sitting"). Mint is ~the token struct it already parses, signed
instead of verified.
- Minter-equals-verifier is acceptable *here only* because both already run as root on
the same host; the boundary being enforced is root-vs-unprivileged, not
minter-vs-verifier. In fleet mode this collapse stays forbidden (constraint #2 of
`05`).
Keys: - a stable UUIDv4 in the Agent config via `redflag-agent --init-standalone`;
- the same UUID in root-owned `/etc/redflag/agent_id` for independent target binding;
- `/etc/redflag/authority_local.key`, root-owned `0600`;
- the public half in the helper's root-owned trusted keyring;
- Agent-owned exchange directories for mint requests, tokens, mutation requests,
envelopes, and receipts; and
- exact sudoers command shapes for legacy token mint, envelope mint, and envelope
execution.
- `authority_local.key` — Ed25519, generated at install time (same provisioning step `Config.IsStandalone()` is true only when a stable Agent ID exists and registration,
that creates the `redflag-local` group / helper sudoers). Root-owned, `0600`, access, and refresh tokens are all absent. Partial fleet material is neither standalone
outside the agent's readable tree. Never leaves the host. nor registered and startup refuses it.
- The helper's pinned keyring gets the corresponding public key with its `key_id`
fingerprint, exactly like a server key. Verify path is byte-identical to fleet mode.
### Flow The standalone Agent starts the local API and kernel monitor, scans on startup, and then
scans on its bounded local interval without contacting a Server. Pacman is included in
that local scanner set.
---
## APT and DNF Flow
``` ```
tray (redflag-local member) Desktop
→ POST /v1/updates/:id/approve (agent local API, group ACL boundary) → POST /v1/actions/approve-update
→ agent: resolve closure via DiscoveryRunner (dry-run, hash-resolve — read-only) → Agent dry-run and closure hash resolution
→ agent: run the current local predicate (OSV.dev query over resolved entries) → Agent OSV query over the reported closure
any vuln in the checked set = full stop — no silent waiver; non-clear verdict requires a recorded reason
override requires explicit reason, journaled locally → Agent writes MintRequest
→ agent: write mint request file (closure + gate evidence + operator + reason) → root helper mint
→ sudo systemd-run redflag-helper --mint <request> (narrowed sudoers entry) validates host, operation, closure shape, evidence freshness, and reason
helper re-derives closure_hash, re-checks gate evidence freshness window, signs a short-lived capability with authority_local.key
signs with authority_local.key, emits token, journals the mint → normal Agent consumer
→ token → normal consumer path → helper verify + execute (unchanged) → root helper verifies, replay-claims, and executes one fixed APT/DNF plan
→ PolicyResult returns to Desktop
``` ```
The mint step **re-validates rather than trusts** the agent's evidence where the current Age and soak fields are recorded as `not_applicable`; standalone has no local registry
contract permits: closure entry shape, evidence timestamps, host binding, operation, and history for those policies yet. Registry artifacts without local paths remain signed in
the presence of an explicit override reason when OSV was vulnerable or unreachable. It the closure but are not rehashed helper-side, and the transient helper unit retains host
does not re-run OSV; the unprivileged agent performs that query and the privileged mint network access.
path journals the supplied verdict. Network reachability is not the enforcement boundary
here: the current helper unit retains host network access.
Age and soak evidence fields exist, but the local approval handler currently records both ---
as `not_applicable`. Bringing the fleet age/soak policies into standalone mode remains work.
Doctrine carried over unchanged: signing required, forward-only, no skip-verification ## Pacman Flow
path, no doctrinal knobs.
### Audit Pacman does not use the legacy capability payload:
Every standalone approval/mint/execute writes to a local append-only journal owned by ```
root (mirror of the server's security-event journal). The tray reads it through a Desktop + required reason for unavailable OSV coverage
read-only local API endpoint. When the host later joins a fleet, the journal is → Agent resolves exact official-repository transaction in a private database/cache
uploaded once during re-provisioning so history survives the mode switch. → Agent hashes every archive and detached signature
→ Agent writes EnvelopeMintRequest + MutationManifest
→ root helper mint-envelope
secure openat read from the fixed exchange directory
copies artifacts into root custody
verifies exactly one requested root, hashes, package identity, repository, detached signatures, and versions
requires an upgrade root to move strictly forward or an install root to be absent
signs one host-bound MutationEnvelope
→ Agent writes the envelope into the fixed execution exchange
→ root helper execute-envelope
verifies signature, time, target, replay, custody, identity, signatures, and versions again
executes one fixed pacman -U plan without --needed
→ joined MutationReceipt returns to Desktop
```
### Fleet join ("join fleet later") Exchange files use exact UUID filenames, no-follow directory traversal, create-new
outputs, and filename joins between request and response. Stale unprivileged outputs are
removed before the helper runs; the helper refuses symlinked or out-of-directory paths.
Registration code + one-time 2FA → agent registers against the server → helper keyring OSV currently has no usable Arch package mapping. The verdict is `unsupported`, never
is **replaced**: server authority key(s) in, local authority key retired and its private `clear`, and the operator reason cannot waive any cryptographic or package-identity check.
half destroyed (journaled). Single authority per mode — no dual-mint window, no local
fallback authority in fleet mode. A fleet host that loses its server does what it does
today: nothing installs until the server returns (constraint #3's verified-cache
fallback applies only to already-minted operations).
**Transition maintainability (Casey, 2026-06-12).** Fleet join is a supported ---
lifecycle path, not a one-off migration script — it must hold to the same standard
as install/upgrade: idempotent, re-runnable, verified by the post-join healthcheck
rather than assumed (`docs/tasks/INSTALL-001` is the enforcement pattern). Two
standing rules keep it from rotting:
1. **Gate logic should converge.** Fleet and standalone share capability and helper ## Audit State
verification primitives, while standalone currently omits the age/soak policies.
Closing that drift is required; future gate changes must extend both resolution
paths in the same change.
2. **The join flow is exercised, not trusted.** Keyring replacement, key
destruction, and journal upload need test coverage that runs both directions of
the matrix (fresh-fleet install vs standalone-then-join must converge on
identical end state). If the two end states can diverge, the transition has
already broken — it just hasn't been noticed yet.
## Non-goals Mint and execution decisions write the helper's local security journal. The local API
does not yet expose that root journal as a dedicated approval-history endpoint, and no
fleet upload exists. Desktop can show the joined result returned by the current request;
durable local browsing remains unfinished.
- No local approval authority in fleet mode (server remains sole authority). The Desktop-provided operator name comes from the session environment and is asserted,
- No "lite" trust mode — gates are not weakened because the host is standalone. not peer-attested. `SO_PEERCRED`, polkit, or another fresh step-up mechanism remains an
- No network listener for the mint path; the local group ACL + sudoers narrowing is open authority improvement.
the entire request surface.
- Tray never touches keys, tokens, or the mint path directly — it only calls the
agent's local API.
## Resolved questions (Casey, 2026-06-10) ---
1. **Mint placement: `redflag-helper --mint`.** Artifact set stays at three. Two keys ## Fleet Join Is Not Implemented
keep roles distinct (mint key root-owned `0600`, verify key in pinned keyring);
SEC-022 attests both invocation modes.
2. **OSV in standalone: best-effort with honest verdict.** Vuln found in a reported
resolved entry = full stop.
OSV unreachable = explicit operator acceptance of "closure unverified" with reason,
journaled — mirrors the fleet `unverified` hold from v0.2.3.1. Age/soak gates and
complete execute-time registry artifact rehashing are not current standalone guarantees.
3. **Gate-evidence freshness window: 15 minutes, hard-coded.** Not configurable
(no doctrinal knobs); expired evidence means the agent re-resolves and re-checks.
## Cross-refs The helper has an explicit local-key retirement primitive, but there is no complete,
tested transition that retires local authority, destroys its private key, installs the
Server keyring, registers the Agent, and proves the converged end state. Registration
from a standalone config therefore refuses with an error. Operators must not add fleet
credentials beside the local authority by hand.
- `05-supply-chain-gate.md` — token contract, constraints (esp. #2, #4, #6). The required future transition is one-way and idempotent:
- `docs/tasks/FEAT-002-local-agent-api.md` — local API surface this builds on.
- `docs/tasks/SEC-023` — package-mutation boundary (mint path must not weaken it).
- `docs/tasks/SEC-022` — binary mutual attestation (helper trust file).
- `docs/tasks/THREAT-001` — must document the standalone trust model table above.
*Last reviewed: 2026-08-25* 1. stop local mutation intake;
2. retire and destroy the local private authority;
3. replace the helper trust set with Server authority keys;
4. register and persist complete fleet credentials;
5. prove that local mint sudoers and key material are absent; and
6. resume as a fleet Agent with no local fallback.
Until that workflow and its matrix tests exist, “join this standalone machine to a
fleet” is a named gap, not a supported lifecycle.
---
## Non-Goals
- No local mint authority beside fleet credentials.
- No doctrinal switch that disables signing, replay, hashes, identity, signatures, or
forward-only enforcement.
- No package-manager command in Desktop or Agent sudoers.
- No network listener for minting.
- No claim that same-host Ed25519 recreates the fleet trust boundary.
---
## Cross-References
- [05-supply-chain-gate](05-supply-chain-gate.md) — capability and envelope contracts
- [../components/04-helper](../components/04-helper.md) — root validation and execution
- [../components/05-desktop](../components/05-desktop.md) — credential-less request surface
- [../scanners/06-pacman-scanner](../scanners/06-pacman-scanner.md) — discovery and exact transaction resolution
---
*Last reviewed: 2026-09-01*

View file

@ -1,8 +1,10 @@
# Mutation manifest wire contract # Mutation manifest wire contract
This directory owns the language-independent bytes shared by RedFlag Server, Agent, and This directory owns the language-independent bytes shared by RedFlag Server, Agent, and
helper. The current closure-based capability token remains the only runtime execution path; helper. APT and DNF still use the closure-based capability token. Pacman is the first live
the mutation envelope is pinned and tested but not wired to a backend yet. `MutationEnvelope` backend: standalone Desktop intent now crosses Agent resolution, local
root mint, helper verification, execution, and a joined receipt without a second package
manager path in QML.
## Envelope ## Envelope
@ -70,10 +72,9 @@ vocabulary ahead of the thing it names would weaken a binding that already works
## Authorization discipline ## Authorization discipline
`authorization_id` MUST be a canonical UUID v4 (8-4-4-4-12 lowercase hex, version nibble `authorization_id` MUST be a canonical UUID v4 (8-4-4-4-12 lowercase hex, version nibble
`4`, variant nibble `8`/`9`/`a`/`b`) — the same discipline the standalone mint already `4`, variant nibble `8`/`9`/`a`/`b`) — the same discipline the standalone mint applies to
applies to `request_id`. It is the identifier an executor will eventually record as a `request_id`. The executor atomically records it as a one-shot replay claim before pacman
replay key, and a newline-delimited ledger matched line-by-line has no defence against an runs. The UUID shape prevents path or record injection into that claim store.
identifier that contains a newline. The shape is checked before that day arrives.
`expires_at - not_before` MUST NOT exceed **3600 seconds**. That ceiling is derived, not `expires_at - not_before` MUST NOT exceed **3600 seconds**. That ceiling is derived, not
chosen: it is `DefaultTokenTTL`, the fleet minter's own window. Standalone mint is tighter chosen: it is `DefaultTokenTTL`, the fleet minter's own window. Standalone mint is tighter
@ -220,18 +221,28 @@ signature, time, lifetime ceiling, backend payload shape — and then refuses wi
`backend_not_migrated`. This remains the inspection-only compatibility path and consumes no `backend_not_migrated`. This remains the inspection-only compatibility path and consumes no
replay state. replay state.
`redflag-helper execute-envelope --envelope-file <path> [--receipt-file <path>]` admits the `redflag-helper mint-envelope --request-file <path> --envelope-out <path>` is the standalone
first migrated backend: pacman `install` and `upgrade` over exact cached package archives. authority for the first migrated backend. A pacman payload marks exactly one requested root
The helper opens every signed cache path without following a final symlink, copies it into and binds repository, cache locations, archive hash, detached-signature hash, and the exact
a root-owned `0700` operation directory, and verifies the signed SHA-256 and archive `name@version` identity. Before signing, the helper copies every archive and signature into
`name@version`. It then queries installed state with a cleared environment and pacman's a root-owned `0700` operation directory, verifies hashes, archive identity, the Arch package
`vercmp`; any resolved action older than its installed version fails closed. Absent and signature, and installed state through pacman's `vercmp`. An `upgrade` requires the requested
equal packages remain valid closure members because format 1 does not distinguish requested root to be strictly newer; an `install` requires it to be absent; dependencies may be absent,
roots from resolved dependencies. Only then does it atomically consume equal, or newer but never older.
`authorization_id` before one fixed `pacman -U` invocation without `--needed`, so exit zero
cannot conceal an already-satisfied no-op. A receipt records the joined operation, manifest,
authorization, verified-action count, and outcome.
Every other backend still fails closed with `backend_not_migrated`. The Agent request, `redflag-helper execute-envelope --envelope-file <path> [--receipt-file <path>]` repeats those
resolver, authorization mint, and Desktop handoff remain separate cuts; this executor does checks over a fresh root-owned stage, atomically consumes `authorization_id`, and invokes one
not infer or create any of them. fixed `pacman -U --noconfirm -- <archives...>` plan. There is no `--needed`, so exit zero
cannot collapse a skipped transaction and an applied one into the same receipt. The receipt
records the joined operation, manifest, authorization, fully verified-action count, and
executor outcome.
The Agent-side resolver and Desktop handoff are wired for standalone pacman in the active
source. Exchange reads walk fixed directories with `openat` and no-follow semantics; helper
writes are create-new and joined by UUID filename. RedFlag has no Arch OSV mapping yet, so
local pacman approval records `unsupported` and requires an explicit reason instead of
inventing a clear advisory verdict.
Every other backend still fails closed with `backend_not_migrated`. Server-side fleet
envelope mint and delivery remain separate work. This document describes source behavior;
CI, packaging, installation, and runtime proof remain distinct states.