Watch
1
0
Fork
You've already forked RedFlag
0

raf: desktop component + local trust boundary + fleet-join maintainability

- components/05-desktop.md: the tray as credential-less localapi client; no-token surface marked as open decision (peer identity unanswered), not doctrine
- trust-boundaries: Local boundary section — the kernel is the middleware, group stamping at login is the sharp edge
- standalone-authority: fleet join must stay idempotent, single-source gated, and tested in both directions
This commit is contained in:
Fimeg 2026-06-12 13:26:54 -04:00
commit 8d54fec8eb
3 changed files with 142 additions and 0 deletions

View file

@ -0,0 +1,91 @@
# Desktop Component
**A credential-less tray window onto the local agent — it can see, it can ask, it cannot mutate.**
---
## Doctrine
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.
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.
Fleet-server context, when it comes, joins this same app rather than spawning a
second local-only application.
---
## Structure
```
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
```
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
(`/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.
- `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.
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.
---
## Lifecycle
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`.
---
## Cross-References
- [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

View file

@ -129,6 +129,40 @@ Ties to SEC-012 (renewal atomicity): the server side is now fully transactional;
---
### Local Trust Boundary (agent localapi)
Not a server HTTP boundary — this one lives on the **agent host**. The agent exposes a
local-only API for the desktop tray app over a Unix socket
(`/var/lib/redflag/agent/localapi/redflag-agent.sock`) on Linux and a named pipe
(`\\.\pipe\RedFlagAgentLocal`) on Windows.
**Endpoints** (`agent/internal/localapi/server.go`): `/v1/identity`, `/v1/status`,
`/v1/scans/latest`, `/v1/packages`, `/v1/tokens/active`, `/v1/desktop` (tray health
report), `/v1/actions/trigger-scan`, `/v1/actions/approve-update`.
**Authentication is the operating system, not credentials.** There are no tokens on
this surface by design — access is gated by filesystem permissions:
- Socket directory `0750`, socket `0660`, both group-owned by `redflag-local`
(`agent/internal/localapi/listener_unix.go`).
- The installer creates the group, enrolls the agent user, and enrolls the detected
desktop user (`linux.sh.tmpl` step 7c).
- A user not in `redflag-local` gets `EACCES` at connect — the kernel is the
middleware.
**Known sharp edge:** group membership is stamped onto a login session at login.
Adding a user to `redflag-local` does not grant running sessions access; the user must
log out and back in. The desktop app diagnoses this case explicitly (in-group-on-disk
vs in-group-in-session, `desktop/src/main.rs`) instead of surfacing a raw permission
error. Installs that predate the desktop-user enrollment step never ran it; the
upgrade path re-asserts socket-chain ownership but does not re-check membership
(tracked: INSTALL-001 post-install healthcheck).
**Cross-references:**
- [components/05-desktop](components/05-desktop.md) (the only intended client)
---
## Anti-Pattern: BUG-013
**Problem:** `DELETE /api/v1/agents/:id` was an admin operation registered under the agent-auth group with `MachineBindingMiddleware`.

View file

@ -107,6 +107,23 @@ fallback authority in fleet mode. A fleet host that loses its server does what i
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 stays single-source.** Standalone and fleet share the same gate
code (vuln full-stop, soak, age, hash verification). When a gate gains a
fleet-side capability (e.g. DB-backed policy config), the standalone resolution
path must be extended in the same change — a gate that behaves differently per
mode is drift, not configuration.
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).