RAF: full docs pass — components, flows, security, scanners, reference, testing, verification, overview
This commit is contained in:
parent
565baa0819
commit
a19dcf4f14
34 changed files with 5085 additions and 28 deletions
137
RAF/security/06-standalone-authority.md
Normal file
137
RAF/security/06-standalone-authority.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# 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`.
|
||||
|
||||
## The problem
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
operator → server (authority, off-host) → agent (consumer) → helper (enforcer)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## What the gate still buys on one host
|
||||
|
||||
The boundary that *is* preservable locally is the OS privilege boundary:
|
||||
|
||||
| Defended against | Fleet mode | Standalone |
|
||||
|---|---|---|
|
||||
| Tampered artifact (hash mismatch) | yes | **yes** — helper verification unchanged |
|
||||
| Known-vuln package (OSV) | yes | **yes** — gates run before mint |
|
||||
| Too-new package (age/soak gates) | yes | **yes** |
|
||||
| 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** |
|
||||
|
||||
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.
|
||||
|
||||
## Design
|
||||
|
||||
### Authority placement: privileged mint, root-owned key
|
||||
|
||||
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:
|
||||
|
||||
- Keeps the artifact set at three (`agent`, `server`, `helper`) — no fourth binary.
|
||||
- The helper is already the audited, privileged, network-less 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:
|
||||
|
||||
- `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.
|
||||
|
||||
### 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 gate predicates locally (OSV.dev query, age gate, soak gate)
|
||||
any vuln in closure = full stop, same as ApproveUpdate's 409 — 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)
|
||||
```
|
||||
|
||||
The mint step **re-validates rather than trusts** the agent's gate verdicts where it
|
||||
can do so without network (closure hash shape, evidence timestamps, forward-only
|
||||
version check). It cannot re-run OSV (network-less) — the OSV verdict is part of the
|
||||
journaled evidence, so a lying agent user leaves a tamper-evident trail and still
|
||||
cannot bypass artifact-hash verification at execute time.
|
||||
|
||||
Doctrine carried over unchanged: signing required, forward-only, no skip-verification
|
||||
path, no doctrinal knobs.
|
||||
|
||||
### Audit
|
||||
|
||||
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.
|
||||
|
||||
### Fleet join ("join fleet later")
|
||||
|
||||
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).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- 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.
|
||||
|
||||
## 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 = 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
|
||||
execute-time hash verification never relax.
|
||||
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
|
||||
|
||||
- `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.
|
||||
Loading…
Reference in a new issue