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:
parent
71fe075f65
commit
8d54fec8eb
3 changed files with 142 additions and 0 deletions
91
RAF/components/05-desktop.md
Normal file
91
RAF/components/05-desktop.md
Normal 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
|
||||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in a new issue