Files
harmony/docs/guides/fleet-device-secrets.md
Jean-Gabriel Gill-Couture 397f266d31 docs(adr-025): groups are the security boundary — rewrite, trail, guide
Rewrite ADR-025 around the group-based design: groups gate (admin-
managed identity), labels place, OpenBao enforces via external groups;
batch tokens with 1h TTL make expiry re-login the membership sync.
Iteration trail preserved under 025-device-secret-access/ (025-1 JWT
claim scoping, 025-2 entity-by-policy as built, 025-3 the scale and
security analysis with verified sources). Add the user-facing guide
and the implementation + branch-consolidation rollout plan.
2026-06-12 00:26:24 -04:00

114 lines
5.1 KiB
Markdown

# Fleet device secrets
How fleet devices read their deployments' secrets — image-pull
credentials, application config, API keys — from OpenBao, and how you
control who reads what. Design reasoning lives in
[ADR-025](../adr/025-fleet-device-secret-access.md); this page is the
operating manual.
## The architecture in one minute
A device proves *who it is* with the Zitadel machine key it already
uses for NATS. *What it may read* is decided by **group membership**,
which only admins can change. *Where it runs* is decided by labels.
The three never mix:
```
Admin Zitadel OpenBao
│ assign device to groups ──▶ device ∈ {edge-a, …} │
│ write secret ──────────────────────────────────────────────▶ secret/<fleet>/<deployment>/…
│ │
Deployment CR │
│ allowedGroups: [edge-a] ──▶ operator attaches policy ──────▶ group edge-a may read
│ targetSelector: labels ──▶ operator schedules within │ deployment-<name> subtree
│ authorized devices │
│ │
Device │
└─ machine key ─▶ Zitadel token (groups: [edge-a]) ─▶ JWT login ─▶ reads its secrets
```
Three rules to remember:
| Concept | Who sets it | What it controls |
|---|---|---|
| **Group** | Admin, in Zitadel | Security boundary: which deployments' secrets a device may read |
| **Label** | Device config (self-reported) | Placement only: which authorized devices actually run the workload — narrows, never widens |
| **`allowedGroups`** | Deployment author | Which groups may view (run + read secrets of) the deployment |
## Storing a secret for a deployment
Write it under the deployment's subtree of the fleet KV mount:
```bash
bao kv put secret/<fleet-ns>/<deployment-name>/db-credentials \
username=app password=
```
Nothing else to configure — access follows from the deployment's
`allowedGroups` below. Every device in an allowed group can read the
whole `<deployment-name>/` subtree, whether or not the workload landed
on it; size your groups to the audience the secret may have.
## Authorizing and placing a deployment
```yaml
apiVersion: fleet.nationtech.io/v1alpha1
kind: Deployment
metadata:
name: hello-web
spec:
allowedGroups: [edge-a] # security: who may view this deployment
targetSelector:
matchLabels:
hw: pi5 # placement: which authorized devices run it
score:
```
The operator schedules onto devices that are **members of an allowed
group AND match the selector**, then attaches the deployment's read
policy to each allowed group. Existing device sessions pick the grant
up immediately — rolling out a new deployment never touches logins,
tokens, or per-device configuration.
## Managing device group membership
Until Zitadel's first-class groups are GA, a fleet group is a Zitadel
project role (one role per group, surfaced as a `groups` claim by the
fleet's token Action — applied by the fleet bootstrap, not by hand).
Grant or remove the role on the device's machine user to move it
between groups.
Membership changes take effect at the device's **next login, at most
one token TTL later (≈ 1 h)**. This is the only eventually-consistent
operation in the system — everything else binds immediately. If that
window matters, use the emergency levers below.
## Reading secrets on the device
Nothing to do: the agent reads through `harmony_config`, which
authenticates to OpenBao with the machine keyfile automatically
(JWT-bearer rung of `OpenbaoSecretStore`). Tokens are short-lived
batch tokens; the store re-logins transparently on expiry.
## Revocation runbook
| Situation | Action | Effect |
|---|---|---|
| Device leaves a group (routine) | Remove the role/group in Zitadel | Converges at next login, ≤ 1 TTL |
| Device compromised | Disable its OpenBao identity entity, then rotate/delete its Zitadel machine key | Token dead at next request; no new logins |
| Secret/deployment compromised | Detach `deployment-<name>` policy from its groups (or rotate the secret) | All devices lose read immediately |
| Whole group compromised | Detach the group's policies | Immediate, group-wide |
## Guarantees and limits
- A device can only ever read secrets of deployments whose
`allowedGroups` intersect its groups. Labels, CR edits, or a
compromised agent cannot widen that — membership is signed into the
Zitadel token and validated against the project (`bound_audiences`),
so other projects' tokens are rejected outright.
- Visibility is **group-granular**: being scheduled is not required to
read. Finer isolation ⇒ finer groups.
- One Zitadel project + OpenBao pair (a *cell*) comfortably serves
~10⁵ devices; beyond that, shard fleets into cells
([ADR-025](../adr/025-fleet-device-secret-access.md), Rationale).