Files
harmony/docs/adr/025-device-secret-access/025-2-entity-by-policy.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

75 lines
3.6 KiB
Markdown

# ADR 025-2: Entity-by-Policy Scoping (as built)
Initial Author: Jean-Gabriel Gill-Couture
Initial Date: 2026-06-11
## Status
Retired. First implementation shipped on
`feat/fleet-device-secret-access`, then superseded by the group-based
design (main [ADR-025](../025-fleet-device-secret-access.md)) after
the scale analysis in
[025-3](./025-3-groups-as-security-boundary.md): per-device entity
writes are O(targeted devices) per deployment change, which does not
hold at fleet scale. The identity layer built alongside it
(`harmony_zitadel_jwt`, the JWT-bearer auth rung, `refresh_auth()`)
carries over unchanged.
## What changed vs 025-1
[025-1](./025-1-jwt-deployments-claim.md) carried per-deployment
membership *inside the token*: Zitadel user metadata → token Action →
JWT `deployments` claim → OpenBao `groups_claim` → external groups.
Implementation replaced that whole chain with one server-side write:
**A device's secret access is its OpenBao identity entity's policy
list.** The operator sets the entity's policies to exactly
`deployment-<name>` for each deployment currently scheduled on the
device. OpenBao resolves identity policies **at request time**, so a
grant or revocation takes effect on the device's very next read — the
device logs in once and keeps its token; no re-auth, no thundering herd
on bulk reassignment. Proven by the e2e test
`entity_policy_grants_existing_openbao_token_access`
(`fleet/harmony-fleet-e2e/tests/openbao_policy.rs`).
## Why the claim chain lost
- **Tokens snapshot, entities don't.** Claim-carried scope is frozen at
mint time; every membership change required invalidating two cached
tokens (Zitadel + Bao) per device. Entity policies change server-side
with zero device participation — strictly better for revocation and
for offline devices.
- **Less machinery.** Dropped: the Zitadel token Action (server-side JS
in the security path), `groups_claim` + external groups, the agent's
`cached_scope()` and before-reconcile refresh-check, and the
operator's Zitadel-metadata write scope. The operator now writes only
OpenBao.
- **One source of truth.** Membership derives from the operator's view
of the `Deployment` CRs in its reconcile loop, not from a second copy
in Zitadel user metadata.
## Shipped shape
| Piece | Home |
|---|---|
| `DeviceSecretAccess` trait (devices ↔ deployments, backend-agnostic) + in-memory test impl | `harmony/src/modules/fleet/secret_access.rs` |
| `OpenBaoDeviceSecretAccess` (entity resolution via JWT-mount alias, policy-list writes) | `harmony/src/modules/openbao/identity.rs` |
| Operator wiring: `sync_secret_access` after every selector/device change | `fleet/harmony-fleet-operator/src/fleet_aggregator.rs` |
| JWT-bearer minter (RFC 7523), extracted to a leaf crate to break a dependency cycle | `harmony_zitadel_jwt` (re-exported by `harmony_zitadel_auth`) |
| JWT-bearer rung on the auth ladder + `refresh_auth()` | `harmony_secret/src/store/openbao.rs` |
Entity resolution subtlety: OpenBao auto-provisions an entity (random
name) + alias (name = `user_claim`, i.e. the device id) on first JWT
login. The operator therefore resolves device → entity **via the
alias**, creating entity + alias itself when the device has never
logged in, so access can be granted ahead of first login.
## What this iteration deliberately does not solve
Scheduling — and therefore secret access — is gated only by the
deployment's label selector over **device-reported** labels. A device
that can choose its own labels can attract deployments and their
secrets. That gap is the subject of
[025-3](./025-3-groups-as-security-boundary.md).