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

5.1 KiB

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; 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:

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

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, Rationale).