swarm-bao: write every swarm-* grant as a bao granter, not with a 24h token

Every unit that writes a bao policy or cert-auth role ran only while the
operator-placed bootstrap token existed, and skipped silently otherwise.
The token lives 24h, so on any real swarm a PR adding or changing a grant
deployed with its unit skipped, and each one needed a manual token refresh
(plus a root `bao policy write` when it added a path).

A `bao-granter` principal now writes them. Its leaf is minted by
swarm-bao-pki on the store host (0600 root, never copied off it), and its
policy covers `swarm-*` policies, `swarm-*` cert-auth roles and
`pki/roles/swarm-*` by glob, plus the mount and services-root paths the
controller's unit already used. All ten granting units
(controller, secret-publisher, matrix-ctl, matrix-token, queue-agent,
grafana-oidc, otel-oidc, forwarder-oidc, services-issuer, nats-tls) log in
with it instead of reading the token. They keep the 2880 x 30s retry, now
require swarm-bao-pki, and when the store refuses the granter they fail
and print the one-time step instead of skipping.

swarm-bao-granter-role is the one unit left on the token. It enables the
auth mounts (moved out of the controller's unit) and writes the granter's
own policy and role. The bootstrap policy is renamed `bao-bootstrap` and
shrinks to those five stanzas; it is shipped at
/etc/hyperhive/bao-bootstrap-policy.hcl. The old name `swarm-bootstrap`
matched the granter's own `swarm-*` glob.

The granter's CN joins certAuthCns, so no hive can be named into its role.
An assertion keeps both pki role names under `swarm-`. With no client CA
the granting units no longer render, and a warning says so.

module-eval pins the granter's policy stanza by stanza, what it cannot
reach, that every call a granting unit makes is granted, and that only
swarm-bao-granter-role reads the token.

Refs #4704
This commit is contained in:
atlas 2026-09-27 03:33:38 +02:00 • committed by mara
commit e9cec0da21
12 changed files with 898 additions and 443 deletions

View file

@ -26,12 +26,10 @@ let
natsName = "nats.t.local";
natsUrl = "tls://${natsName}:4222";
# Every service on one host, with a bootstrap token so the store's granting
# units render. The queue, the store and every in-tree client of the queue
# Every service on one host. The queue, the store and every in-tree client of the queue
# are all here, so the scan below reads each of them.
allLocal = hive {
deploy.singleHostSwarm = true;
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
};
# The same host on the mesh.
@ -232,8 +230,8 @@ let
&& !(lib.elem 4222 allLocal.networking.firewall.allowedTCPPorts);
}
{
# Ordering, never a requirement: the policy unit skips once the bootstrap
# token is gone, and a skipped unit counts as done.
# Ordering, never a requirement: a policy unit that failed still counts
# as done, and the leaf unit's own retries carry it past that.
name = "the leaf unit is ordered after its policy unit, with no requires";
ok =
let