swarm-bao: let the controller write agent credentials, and gate that it can

Closes #4124.

The controller's policy granted only the bootstrap paths -- hive cert-auth
roles and hive ACLs. #4113 then made it a secret WRITER, and nothing related
the grants to the paths the code writes, so every matrix token provision
answered 403. The two halves landed on different issues and neither looked
wrong on its own.

`secret/data/` is KV v2's ACL prefix and is absent from the path the code
passes, so matching `swarm-secret-client`'s spelling literally would have
granted nothing. Write-only: the controller mints these and never reads one
back, and a read capability would let it recover every agent's credentials
rather than only replace them.

The gate is the point. Two module-eval arms -- the grant exists and is not
a broader wildcard, and its capability list is pinned whole, because an
ADDED capability is what a presence check misses -- plus a test in path.rs
pinning MOUNT/AGENT_PREFIX and naming the nix file, since renaming either
constant is a silent 403 rather than a compile error.

setup.md carried two warnings this makes false: that nothing in the tree had
ever authenticated to the store, and that no deployment shape mints a leaf
whose CN reads swarm-controller. glue-bao-tls.nix has minted one since #3726
item 1.
This commit is contained in:
atlas 2026-09-09 00:46:01 +02:00
commit cc8fb0ee44
4 changed files with 62 additions and 8 deletions

View file

@ -528,6 +528,34 @@ let
in
lib.hasInfix "sys/policies/acl/hive-*" s && !(lib.hasInfix "sys/policies/acl/*" s);
}
{
# The policy authorising this route lives in another file, and nothing
# else relates the grants to the paths the code actually writes.
#
# `secret/data/` is KV v2's ACL prefix; `swarm/agents` is
# `swarm_secret_client::path::AGENT_PREFIX`, whose value that crate
# pins in its own test.
name = "the controller may write agent credentials, and only under the agent prefix";
ok =
let
s = baoGrantHere.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy.script;
in
lib.hasInfix "secret/data/swarm/agents/*" s
&& !(lib.hasInfix "secret/data/*" s)
&& !(lib.hasInfix "path \"secret/*\"" s);
}
{
# Write-only is the property, not an accident of how it was typed: a
# `read` here would let the controller recover every agent's credentials
# instead of only replacing them. Pinned as the whole capability list,
# because an added capability is exactly what a presence check misses.
name = "the controller's grant on agent credentials is write-only";
ok =
let
s = baoGrantHere.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy.script;
in
lib.hasInfix "path \"secret/data/swarm/agents/*\" {\n capabilities = [\"create\", \"update\"]" s;
}
{
# The policy above grants paths under a mount nothing else creates, so
# the unit that writes the policy has to create it too — otherwise every