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:
parent
7472d21c42
commit
213918f1a2
9 changed files with 543 additions and 369 deletions
|
|
@ -4,7 +4,7 @@ I'm **Casey Tunturi** — Fimeg in the community. Systems architect, 25+ years o
|
|||
|
||||
## 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,20 +9,23 @@ understand the shape of the system, then follow the links into the detailed docs
|
|||
|
||||
## What RedFlag Is
|
||||
|
||||
A self-hosted update management platform for homelabs and small MSPs. Centralized
|
||||
visibility and control over software updates across Linux, Windows, and Docker — with
|
||||
a cryptographic supply chain gate that most commercial RMM tools don't attempt.
|
||||
A machine knowledge and operations system with a self-hosted fleet surface. Agents
|
||||
report operating-system, process, socket, service, container, software, update, and
|
||||
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
|
||||
issues Ed25519-signed commands; agents verify signatures, check nonces, reject replays.
|
||||
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
|
||||
`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,
|
||||
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.
|
||||
Triggered when a user opens the Processes tab — no background broadcasting.
|
||||
25+ fields per process (osquery parity) plus 7 related data types (open files,
|
||||
sockets, pipes, environment keys, memory map, namespaces, listening ports).
|
||||
25+ fields per process plus open files, sockets, pipes, environment keys, memory maps,
|
||||
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 →
|
||||
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
|
||||
|
||||
### Agent is a Stateless Executor
|
||||
### Fleet Lifecycle Is Server-Owned
|
||||
|
||||
The agent receives commands, executes them, and reports results. It does not track
|
||||
lifecycle states. The server owns every state transition. The agent's only autonomous
|
||||
decisions are: verify this signature, check this nonce, reject this replay.
|
||||
In fleet mode the Agent receives commands, observes the host, executes authorized work,
|
||||
and reports results. It does not own update lifecycle states; the Server owns every
|
||||
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
|
||||
directly. All mutation flows through `consumer.go` → `sudo systemd-run --wait` with
|
||||
token/result files → `redflag-helper`. The agent holds zero sudo for installs.
|
||||
Discovery (scan, dry-run, hash-resolve) runs unprivileged through `DiscoveryRunner`.
|
||||
On capability-gated ecosystems, the Agent cannot run install commands directly. APT and
|
||||
DNF flow through `consumer.go` and a capability token. Standalone pacman flows through a
|
||||
signed `MutationEnvelope` whose exact archives and detached signatures are verified
|
||||
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 →
|
||||
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
|
||||
directly via type-asserted installer methods → reports via ReportLog.
|
||||
|
||||
The legacy path is a known gap — the gate design covers these ecosystems but
|
||||
implementation is deferred.
|
||||
Fleet pacman envelope delivery and the remaining legacy ecosystems are known gaps.
|
||||
|
||||
### 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
|
||||
- **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
|
||||
- **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
|
||||
|
||||
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*
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ It describes a system under active development. Some of it will be wrong by the
|
|||
|
||||
| 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 |
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [deployment](deployment/) | Docker stack, native agent services, CI/CD, release gate, operations runbook pointers |
|
||||
| [testing](testing/) | Test pyramid, structural tests, live testing, honest gaps |
|
||||
|
|
@ -56,6 +56,7 @@ architecture docs."
|
|||
|
||||
| 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.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 |
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# 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
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ Deny-by-default is the architecture, not a configuration: every failure path ret
|
|||
|
||||
## 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)
|
||||
|
||||
|
|
@ -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
|
||||
pipeline shape as execute mode — parse, host binding against the independently read
|
||||
`agent_id`, SEC-021 trusted keyring, Ed25519 verification, time window, authorization
|
||||
lifetime ceiling, backend payload shape — and then refuses every envelope with
|
||||
`backend_not_migrated`, emitting a `MutationReceipt`.
|
||||
`verify-envelope` remains inspection-only: it verifies shape, target, trust key,
|
||||
signature, time, lifetime ceiling, and backend payload without consuming replay state or
|
||||
executing.
|
||||
|
||||
Two absences are deliberate. It **records no replay state**: execute mode records a token
|
||||
only after a plan is built, so an operation that cannot run never burns its slot, and
|
||||
nothing here can run. And it enforces **no artifact custody**: when a backend carries a
|
||||
local cached artifact as executor input the helper must rehash it, but no backend does
|
||||
yet, and remote repository execution keeps the limits stated above.
|
||||
`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 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*
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
and never talks to the fleet server. Its entire world is the agent's local API
|
||||
socket, and its authorization is its operating-system identity: the user running it
|
||||
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.
|
||||
RedFlag Desktop is a Qt 6 / QML system monitor and local operations console for one
|
||||
machine. It works in standalone mode and keeps the same domain when the machine joins a
|
||||
fleet. RedFlag Web asks across machines; Desktop asks here, now, on this body.
|
||||
|
||||
The credential-less surface is a design decision, not an inevitability — the
|
||||
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.
|
||||
The application currently carries eleven native surfaces:
|
||||
|
||||
Fleet-server context, when it comes, joins this same app rather than spawning a
|
||||
second local-only application.
|
||||
- Overview and Performance;
|
||||
- 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
|
||||
|
||||
```
|
||||
desktop/
|
||||
├── src/main.rs # Tauri shell: tray, window, localapi client, health reporter
|
||||
├── tauri.conf.json # Window config, dev/build frontend wiring
|
||||
└── icons/icon.png # 8-bit RGBA — tray icon constructor rejects 16-bit PNGs
|
||||
|
||||
web/
|
||||
├── index.desktop.html # Desktop entry (separate from the fleet SPA's index.html)
|
||||
├── src/desktop/ # LocalAgentApp — status dashboard, no auth surface
|
||||
└── vite.desktop.config.ts# Builds to dist-desktop/, emits the entry as index.html
|
||||
```text
|
||||
RedFlag Desktop (Qt / QML)
|
||||
observation + operator intent
|
||||
|
|
||||
v
|
||||
RedFlag Agent local source of truth, resolution, gates
|
||||
|
|
||||
v
|
||||
RedFlag Helper privileged verifier / bounded executor
|
||||
|
|
||||
v
|
||||
platform backend pacman first; other migrated backends follow
|
||||
```
|
||||
|
||||
One Rust binary, one React entry. The frontend is embedded at build time
|
||||
(`frontendDist: ../web/dist-desktop`); release builds are self-contained — no dev
|
||||
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
|
||||
Desktop never shells out to pacman, Docker, or systemd and never reads a signing key. It
|
||||
speaks HTTP/1.1 over the Agent local socket
|
||||
(`/var/lib/redflag/agent/localapi/redflag-agent.sock`; named pipe
|
||||
`\\.\pipe\RedFlagAgentLocal` on Windows). The frontend reaches it only through
|
||||
Tauri `invoke` commands — the webview itself has no socket access.
|
||||
`\\.\pipe\RedFlagAgentLocal` on Windows). The user must be admitted to the
|
||||
`redflag-local` OS group or the kernel refuses the connection.
|
||||
|
||||
- `local_status` → `GET /v1/identity` + `GET /v1/status` — the dashboard snapshot,
|
||||
polled every 5s.
|
||||
- Health reporter thread → `POST /v1/desktop` every 30s with version, uptime, and
|
||||
window state. This is how the agent (and through it, the fleet) knows a tray is
|
||||
alive on the host.
|
||||
The current socket still lacks `SO_PEERCRED` attribution and fresh StepUp. Desktop sends
|
||||
the session username as an assertion, not an attestation. A same-user process able to
|
||||
reach the socket can express the same intent. The helper constrains what bytes and
|
||||
operations can execute; it does not prove which human clicked the button.
|
||||
|
||||
On `EACCES` the shell diagnoses rather than reports: it distinguishes "user not in
|
||||
`redflag-local`" (gives the usermod command) from "membership added but the login
|
||||
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.
|
||||
Fleet enrollment removes local mint authority. Desktop may still display this machine,
|
||||
but approval belongs to RedFlag Server and the Agent refuses local authorization.
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle
|
||||
## Native structure
|
||||
|
||||
Installed and provisioned by the agent install script (binary, autostart entry,
|
||||
group enrollment — `linux.sh.tmpl` step 7c). Updated through the capability gate
|
||||
like every other binary: `desktop-self` tokens, hash-verified and atomically
|
||||
swapped by the helper ([components/04-helper](04-helper.md)). Server-side minting
|
||||
and pre-tray host healing are in progress — `docs/tasks/UPDATE-002`. The component
|
||||
is not yet in release version lockstep; closing that is `docs/tasks/INSTALL-001`.
|
||||
```text
|
||||
desktop/
|
||||
├── Cargo.toml / build.rs
|
||||
├── src/
|
||||
│ ├── main.rs
|
||||
│ └── 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
|
||||
- [components/02-agent](02-agent.md) — the localapi server side
|
||||
- [components/04-helper](04-helper.md) — how the desktop binary itself updates
|
||||
`POST /v1/actions/approve-update` is the only Desktop package-approval call. APT and DNF
|
||||
use the closure-capability path. Pacman now uses the mutation-envelope path:
|
||||
|
||||
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*
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|----------|-------|
|
||||
| Method | `checkupdates --color=never` |
|
||||
| Platform | Linux (Arch, Manjaro, EndeavourOS) |
|
||||
| Execution time | ~5-15 seconds per scan |
|
||||
| Output format | `pkgname oldver -> newver` (one per line) |
|
||||
| Failure modes | pacman-contrib not installed, network timeout, database lock |
|
||||
| Root required | No (checkupdates uses a private db copy) |
|
||||
| Platform | Arch Linux and pacman derivatives |
|
||||
| Output | `pkgname oldver -> newver` |
|
||||
| Root required | No |
|
||||
| Requirements | `pacman`, `pacman-contrib`, `fakeroot` |
|
||||
|
||||
`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
|
||||
func (s *PacmanScanner) IsAvailable() bool {
|
||||
// Requires both pacman and checkupdates (from pacman-contrib)
|
||||
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`).
|
||||
The resolver returns an error rather than guessing when the requested root is absent, the
|
||||
repository join is missing, an archive or signature is not regular, identity output is
|
||||
ambiguous, or the transaction is empty.
|
||||
|
||||
---
|
||||
|
||||
## Sudoers
|
||||
## Privileged path
|
||||
|
||||
The agent user needs `checkupdates` in sudoers. This is an unprivileged
|
||||
discovery command (no mutation). Example:
|
||||
The Agent encodes each package as `name@version` plus an exact JSON payload containing:
|
||||
|
||||
- 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
|
||||
`checkupdates` binary is in the `pacman-contrib` package, which may not
|
||||
be installed by default.
|
||||
- **AUR packages not scanned** — `checkupdates` only checks official repos.
|
||||
AUR helpers (yay, paru) have their own update checkers but are not
|
||||
integrated.
|
||||
- **No dry-run for mutations** — pacman's `--print` flag exists but is not
|
||||
used for hash resolution. Artifact hashing goes through the capability
|
||||
gate path when it lands for pacman.
|
||||
- AUR packages are not scanned or resolved.
|
||||
- Requested-root identity lives in the signed pacman payload rather than the common
|
||||
manifest, so other backends do not inherit pacman semantics accidentally.
|
||||
- RedFlag currently has no OSV ecosystem mapping for Arch. Local approval records
|
||||
`unsupported` and requires an explicit override reason.
|
||||
- Package signature verification proves the artifact against the local Arch keyring. It
|
||||
does not add reproducible-build or transparency-log evidence.
|
||||
|
||||
---
|
||||
|
||||
## 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) — capability token path (future for pacman)
|
||||
- `agent/internal/scanner/detect.go` — scanner registration
|
||||
- [security/05-supply-chain-gate](../security/05-supply-chain-gate.md)
|
||||
- [security/06-standalone-authority](../security/06-standalone-authority.md)
|
||||
- [components/04-helper](../components/04-helper.md)
|
||||
- `protocol/README.md`
|
||||
|
||||
---
|
||||
|
||||
*Last reviewed: 2026-09-01*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
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**.
|
||||
|
||||
**Current enforced scope:** APT and DNF mutation through RedFlag requires a signed
|
||||
authorization token. Docker, Winget, and Windows Update still use the default-strict
|
||||
signed-command path, and kernel enforcement against out-of-band root mutation is not wired.
|
||||
**Current source scope:** APT and DNF mutation through RedFlag uses a signed capability
|
||||
token. Standalone pacman approval uses a signed `MutationEnvelope`, exact closure archives,
|
||||
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 gate authorizes APT/DNF installs with **signed capability tokens**, not with a runtime
|
||||
decision daemon. The server is the authority: it evaluates policy and mints an
|
||||
Ed25519-signed token describing exactly one operation over the artifact entries the agent
|
||||
resolved and reported. The top-level hash is required. Dependency hashes are best-effort;
|
||||
an unresolved dependency is logged and omitted today rather than making the report fail.
|
||||
The gate authorizes APT/DNF installs with **signed capability tokens** and pacman with a
|
||||
**signed mutation envelope**, not with a runtime decision daemon. In fleet mode the Server
|
||||
is the authority: it evaluates policy and mints an Ed25519-signed capability over the
|
||||
artifact entries the Agent resolved and reported. In standalone mode the short-lived root
|
||||
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
|
||||
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
|
||||
the current transient unit retains host network access.
|
||||
|
||||
The intended boundary is stronger: resolve and stage the complete transitive closure,
|
||||
rehash every byte at the privileged edge, then run the executor in a network-isolated unit.
|
||||
That remains explicit design work, not a present guarantee.
|
||||
Pacman now resolves and stages the transaction and rehashes every byte at the privileged
|
||||
edge. APT/DNF full-closure custody and a network-isolated helper unit remain explicit design
|
||||
work, not present guarantees.
|
||||
|
||||
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
|
||||
|
|
@ -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 |
|
||||
| **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
|
||||
|
||||
|
|
@ -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
|
||||
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
|
||||
pins a dormant `MutationEnvelope` contract in Go and Rust so new backends can converge without
|
||||
extending the closure metaphor or preserving its unsigned `source`/`artifact_path` ambiguity.
|
||||
No current operation uses this envelope yet.
|
||||
The closure token remains live for APT and DNF. Pacman is the first runtime backend migrated
|
||||
to `MutationEnvelope`, so new backends can converge without extending the closure metaphor
|
||||
or preserving its unsigned `source`/`artifact_path` ambiguity.
|
||||
|
||||
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
|
||||
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
|
||||
unreached. The defect becomes executable the first time a backend puts a real cached path
|
||||
in front of the executor — which is why the manifest lands before pacman, not after.
|
||||
unreached. Pacman's envelope avoids that defect: repository, requested-root marker, cache
|
||||
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:
|
||||
|
||||
|
|
@ -203,11 +214,13 @@ provenance, execution location, target, backend, resolved action, duplicates, or
|
|||
authorization metadata, validity, decision, identifier shape, lifetime ceiling, and unknown
|
||||
formats.
|
||||
|
||||
`redflag-helper verify-envelope` runs an envelope through the helper's real pipeline —
|
||||
parse, host binding, trusted keyring, signature, time, lifetime, backend payload shape —
|
||||
and refuses with `backend_not_migrated`. It records no replay state and enforces no
|
||||
artifact custody: nothing executes through it yet, so it must not consume a replay slot,
|
||||
and cached-byte enforcement arrives with the first backend that carries one.
|
||||
`redflag-helper verify-envelope` remains an inspection-only compatibility path: it proves
|
||||
the common envelope and refuses without consuming replay state. `mint-envelope` is the
|
||||
standalone pacman authority. It requires exactly one requested root, detached signatures,
|
||||
strict operation semantics (`upgrade` moves that root forward; `install` introduces it),
|
||||
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
|
||||
|
||||
|
|
@ -223,19 +236,22 @@ The token extends the existing Ed25519 infrastructure rather than introducing ne
|
|||
|
||||
## 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
|
||||
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.
|
||||
- **Agent consumer (unprivileged).** Run discovery and best-effort hash resolution, receive
|
||||
the token, confirm `agent_id` is this host, and hand it to the executor. It holds no signing
|
||||
key and has no direct APT/DNF mutation method.
|
||||
- **Agent consumer (unprivileged).** Run discovery and resolution, receive or request the
|
||||
capability, confirm `agent_id` is this host, and hand it to the executor. It holds no
|
||||
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
|
||||
`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
|
||||
→ 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
|
||||
is not network-isolated.
|
||||
result + exit code. For pacman it also stages archives and signatures into root custody,
|
||||
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
|
||||
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.
|
||||
|
|
@ -419,9 +435,14 @@ override: flagged updates come back in
|
|||
Auto-confirm shares the `ClosureCleared` predicate with manual approval — the two paths cannot
|
||||
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
|
||||
`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.
|
||||
7. Kernel adapters wire the executor as the only permitted caller.
|
||||
|
||||
Steps 1–2 are the contract. Everything else hangs off them.
|
||||
Steps 1–5 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 1–2 are the contract. Everything else hangs off them.
|
|||
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
|
||||
environment; the agent that hands it tokens is unprivileged and holds no signing key. The
|
||||
transient unit still has host network access.
|
||||
environment; the Agent that hands it capabilities is unprivileged and holds no signing key.
|
||||
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
|
||||
the helper unit. Neither property should be claimed until the invocation and artifact path
|
||||
make it true.
|
||||
**Target:** Bring APT/DNF to the same full-custody envelope and enforce network isolation on
|
||||
the helper unit. Neither property applies globally until each migrated backend proves it.
|
||||
|
||||
**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*
|
||||
|
|
|
|||
|
|
@ -1,161 +1,188 @@
|
|||
# Standalone Authority (Local Mode)
|
||||
|
||||
Drafted 2026-06-10. Status: APPROVED 2026-06-10 (Casey signed off on all three open
|
||||
questions; resolutions inlined below).
|
||||
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`.
|
||||
Status: implemented for local Agent startup, scanning, APT/DNF capability approval,
|
||||
and pacman envelope approval. Fleet join remains fail-closed and unfinished.
|
||||
|
||||
## 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
|
||||
boundary: a fully compromised agent host still cannot mint a capability token.
|
||||
Standalone has only a privilege boundary:
|
||||
|
||||
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** —
|
||||
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.
|
||||
```
|
||||
Desktop user → local Agent → fixed sudoers invocation → root helper and local key
|
||||
```
|
||||
|
||||
## 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 |
|
||||
| Registry artifact with no local path | not helper-rehashed today | **not helper-rehashed today** |
|
||||
| Known-vuln package (OSV) | yes, over reported resolved entries | **yes, over reported resolved entries** |
|
||||
| Too-new package (age/soak gates) | fleet policy | **not yet applied** — local evidence records `not_applicable` |
|
||||
| Unprivileged local malware minting installs | yes | **yes** — mint key is root-owned |
|
||||
| Compromised agent process | yes (server refuses) | **yes** — agent user cannot read mint key |
|
||||
| Local root attacker | yes (authority off-host) | **no — out of scope, say so in docs** |
|
||||
| Host-bound signed authorization | yes | yes |
|
||||
| Replay protection | yes | yes |
|
||||
| Fixed argv, cleared environment, no shell | yes | yes |
|
||||
| Pacman archive hash, identity, and detached signature verified by root helper | yes when fleet envelopes land | yes |
|
||||
| APT/DNF local artifacts rehashed when a closure entry names a path | yes | yes |
|
||||
| Human authority survives Agent compromise | yes, authority is off-host | **no** |
|
||||
| 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
|
||||
compromised agent in fleet mode can request approval — and the same gates refuse it.
|
||||
What standalone loses is only the root-compromise case, which on a single sovereign
|
||||
host is game over for every other tool too.
|
||||
Standalone still materially narrows mutation. It does not turn same-host signing into
|
||||
proof that the host was uncompromised.
|
||||
|
||||
## Design
|
||||
---
|
||||
|
||||
### Authority placement: privileged mint, root-owned key
|
||||
## Provisioned State
|
||||
|
||||
Standalone mint runs as a **separate privileged invocation of the helper**
|
||||
(`redflag-helper --mint`), the same `sudo systemd-run` pattern the executor already
|
||||
uses. Rationale:
|
||||
`scripts/provision-standalone-authority.sh` runs as root after the base Agent, helper,
|
||||
`redflag-agent` user, and `redflag-local` group exist. It is idempotent and must never run
|
||||
on a fleet-enrolled host.
|
||||
|
||||
- Keeps the artifact set at three (`agent`, `server`, `helper`) — no fourth binary.
|
||||
- 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`).
|
||||
Provisioning creates or verifies:
|
||||
|
||||
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
|
||||
that creates the `redflag-local` group / helper sudoers). Root-owned, `0600`,
|
||||
outside the agent's readable tree. Never leaves the host.
|
||||
- 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.
|
||||
`Config.IsStandalone()` is true only when a stable Agent ID exists and registration,
|
||||
access, and refresh tokens are all absent. Partial fleet material is neither standalone
|
||||
nor registered and startup refuses it.
|
||||
|
||||
### 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)
|
||||
→ POST /v1/updates/:id/approve (agent local API, group ACL boundary)
|
||||
→ agent: resolve closure via DiscoveryRunner (dry-run, hash-resolve — read-only)
|
||||
→ agent: run the current local predicate (OSV.dev query over resolved entries)
|
||||
any vuln in the checked set = full stop — no silent waiver;
|
||||
override requires explicit reason, journaled locally
|
||||
→ agent: write mint request file (closure + gate evidence + operator + reason)
|
||||
→ sudo systemd-run redflag-helper --mint <request> (narrowed sudoers entry)
|
||||
helper re-derives closure_hash, re-checks gate evidence freshness window,
|
||||
signs with authority_local.key, emits token, journals the mint
|
||||
→ token → normal consumer path → helper verify + execute (unchanged)
|
||||
Desktop
|
||||
→ POST /v1/actions/approve-update
|
||||
→ Agent dry-run and closure hash resolution
|
||||
→ Agent OSV query over the reported closure
|
||||
non-clear verdict requires a recorded reason
|
||||
→ Agent writes MintRequest
|
||||
→ root helper mint
|
||||
validates host, operation, closure shape, evidence freshness, and reason
|
||||
signs a short-lived capability with authority_local.key
|
||||
→ normal Agent consumer
|
||||
→ 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
|
||||
contract permits: closure entry shape, evidence timestamps, host binding, operation, and
|
||||
the presence of an explicit override reason when OSV was vulnerable or unreachable. It
|
||||
does not re-run OSV; the unprivileged agent performs that query and the privileged mint
|
||||
path journals the supplied verdict. Network reachability is not the enforcement boundary
|
||||
here: the current helper unit retains host network access.
|
||||
Age and soak fields are recorded as `not_applicable`; standalone has no local registry
|
||||
history for those policies yet. Registry artifacts without local paths remain signed in
|
||||
the closure but are not rehashed helper-side, and the transient 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
|
||||
path, no doctrinal knobs.
|
||||
## Pacman Flow
|
||||
|
||||
### 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
|
||||
read-only local API endpoint. When the host later joins a fleet, the journal is
|
||||
uploaded once during re-provisioning so history survives the mode switch.
|
||||
```
|
||||
Desktop + required reason for unavailable OSV coverage
|
||||
→ Agent resolves exact official-repository transaction in a private database/cache
|
||||
→ 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
|
||||
is **replaced**: server authority key(s) in, local authority key retired and its private
|
||||
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).
|
||||
OSV currently has no usable Arch package mapping. The verdict is `unsupported`, never
|
||||
`clear`, and the operator reason cannot waive any cryptographic or package-identity check.
|
||||
|
||||
**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
|
||||
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.
|
||||
## Audit State
|
||||
|
||||
## 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).
|
||||
- No "lite" trust mode — gates are not weakened because the host is standalone.
|
||||
- No network listener for the mint path; the local group ACL + sudoers narrowing is
|
||||
the entire request surface.
|
||||
- Tray never touches keys, tokens, or the mint path directly — it only calls the
|
||||
agent's local API.
|
||||
The Desktop-provided operator name comes from the session environment and is asserted,
|
||||
not peer-attested. `SO_PEERCRED`, polkit, or another fresh step-up mechanism remains an
|
||||
open authority improvement.
|
||||
|
||||
## Resolved questions (Casey, 2026-06-10)
|
||||
---
|
||||
|
||||
1. **Mint placement: `redflag-helper --mint`.** Artifact set stays at three. Two keys
|
||||
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.
|
||||
## Fleet Join Is Not Implemented
|
||||
|
||||
## 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).
|
||||
- `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.
|
||||
The required future transition is one-way and idempotent:
|
||||
|
||||
*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*
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# Mutation manifest wire contract
|
||||
|
||||
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;
|
||||
the mutation envelope is pinned and tested but not wired to a backend yet.
|
||||
helper. APT and DNF still use the closure-based capability token. Pacman is the first live
|
||||
`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
|
||||
|
||||
|
|
@ -70,10 +72,9 @@ vocabulary ahead of the thing it names would weaken a binding that already works
|
|||
## Authorization discipline
|
||||
|
||||
`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
|
||||
applies to `request_id`. It is the identifier an executor will eventually record as a
|
||||
replay key, and a newline-delimited ledger matched line-by-line has no defence against an
|
||||
identifier that contains a newline. The shape is checked before that day arrives.
|
||||
`4`, variant nibble `8`/`9`/`a`/`b`) — the same discipline the standalone mint applies to
|
||||
`request_id`. The executor atomically records it as a one-shot replay claim before pacman
|
||||
runs. The UUID shape prevents path or record injection into that claim store.
|
||||
|
||||
`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
|
||||
|
|
@ -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
|
||||
replay state.
|
||||
|
||||
`redflag-helper execute-envelope --envelope-file <path> [--receipt-file <path>]` admits the
|
||||
first migrated backend: pacman `install` and `upgrade` over exact cached package archives.
|
||||
The helper opens every signed cache path without following a final symlink, copies it into
|
||||
a root-owned `0700` operation directory, and verifies the signed SHA-256 and archive
|
||||
`name@version`. It then queries installed state with a cleared environment and pacman's
|
||||
`vercmp`; any resolved action older than its installed version fails closed. Absent and
|
||||
equal packages remain valid closure members because format 1 does not distinguish requested
|
||||
roots from resolved dependencies. Only then does it atomically consume
|
||||
`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.
|
||||
`redflag-helper mint-envelope --request-file <path> --envelope-out <path>` is the standalone
|
||||
authority for the first migrated backend. A pacman payload marks exactly one requested root
|
||||
and binds repository, cache locations, archive hash, detached-signature hash, and the exact
|
||||
`name@version` identity. Before signing, the helper copies every archive and signature into
|
||||
a root-owned `0700` operation directory, verifies hashes, archive identity, the Arch package
|
||||
signature, and installed state through pacman's `vercmp`. An `upgrade` requires the requested
|
||||
root to be strictly newer; an `install` requires it to be absent; dependencies may be absent,
|
||||
equal, or newer but never older.
|
||||
|
||||
Every other backend still fails closed with `backend_not_migrated`. The Agent request,
|
||||
resolver, authorization mint, and Desktop handoff remain separate cuts; this executor does
|
||||
not infer or create any of them.
|
||||
`redflag-helper execute-envelope --envelope-file <path> [--receipt-file <path>]` repeats those
|
||||
checks over a fresh root-owned stage, atomically consumes `authorization_id`, and invokes one
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue