Watch
0
0
Fork
You've already forked hyperhive
0

swarm-secret-client: hives no longer read swarm/services/*

The hive policy rendered by policy::render granted every hive cert read
on secret/data/swarm/services/*, which holds each swarm service's OIDC
client secret. No reader of that tree authenticates with a hive cert:
grafana, the otel collector and the store forwarder each read with
their own leaf, and the bao UI's secret is read by the store's granter.

render() now emits two stanzas: swarm/agents/* and the hive's own
swarm/hives/<hive>/*. Kind::Service had no other caller and is removed.
Docs and comments that described the grant as a decision are rewritten
to the current state.

Closes #4960
This commit is contained in:
atlas 2026-10-04 22:32:28 +02:00 • committed by mara
commit 2505008b83
13 changed files with 82 additions and 136 deletions

View file

@ -72,37 +72,24 @@ advertises a boundary it doesn't actually hold. A wide grant that says what it
is beats a narrow one that only looks narrow.
What still holds: the grant is **read-only** (a hive can't write an agent's
credential, so it can't hand itself an agent's identity), and it reaches three
prefixes and nothing else in the store — every agent's credentials, the reader's
_own_ entry under the hive namespace, which names the hive asking and so widens
nothing between them, and every **swarm service's** OIDC client secret.
credential, so it can't hand itself an agent's identity), and it reaches two
prefixes and nothing else in the store — every agent's credentials, and the
reader's _own_ entry under the hive namespace, which names the hive asking and
so widens nothing between them.
That third prefix has the same shape of reason as the first, and the same honest
cost. A swarm service (Grafana and the swarm collector are the two there today)
registers **one** client for the whole swarm, so its credential's path names
the service and never the host — and which hive runs a given service is a
`deploy.*` fact, per-host by definition, so nothing swarm-wide exists to scope
the grant to. The host running such a service has no store identity of its own
either; it reads with the certificate of the hive it is. **Any hive can
therefore read any swarm service's client secret**, which is worth what it
buys: every swarm service gets its credential the same way from anywhere,
instead of only where the identity provider happens to sit. Giving such a
service its own store identity is what would remove this rather than re-scope
it.
A hive reads no **swarm service's** OIDC client secret. Every reader of
`swarm/services/` presents a certificate that isn't a hive's: Grafana, the swarm
collector and the store forwarder each fetch theirs with a leaf of their own,
and the store's granter fetches the bao UI's.
**A tracked follow-up** narrows this, with the two candidate directions: scope
the grant per hive (and pay for the re-emission), or give each agent container
its own store identity so credentials never pass through a hive at all.
### Per-hive credentials live under the hive kind, not beside a service
### Per-hive credentials live under the hive kind
`swarm_secret_client::policy` scopes the **hive** stanza narrowly (unlike the
agent and service stanzas above) because a hive's own path names it, so
scoping to the reader's own name costs nothing and drifts nowhere. That only
holds if a credential that must be one-per-hive is actually stored under the
hive kind.
agent stanza above) because a hive's own path names it, so scoping to the
reader's own name costs nothing and drifts nowhere. That only holds if a
credential that must be one-per-hive is actually stored under the hive kind.
**A credential that must be one-per-hive goes under `Kind::Hive`**; putting it
under `Kind::Service` and expecting the grant to scope it — that's the mistake
under the agent prefix and expecting the grant to scope it — that's the mistake
this note exists to stop.
### Threat model: prompt injection → confused deputy