Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/RAF/security/06-standalone-authority.md

161 lines
8.4 KiB
Markdown

# 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 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** |
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, 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:
- `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 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)
```
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 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.
### 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).
**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.
## 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 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
- `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.
*Last reviewed: 2026-08-25*