diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index aebab744..22560aa1 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -180,9 +180,17 @@ let # `secret`, and the engine inserts `data/`. Matching the code's spelling # literally would grant nothing. # - # Write-only on purpose. The controller mints these; nothing in its job reads - # one back, and a read capability here would let it recover every agent's - # credentials rather than merely replace them. + # `read` is there because the controller mints idempotently: `mint_and_verify` + # reads the queue credential back before writing so that a re-run keeps the + # value a live agent is already authenticating with instead of rotating it. + # That read is `read_optional`, which treats only a 404 as absence — a denial + # stays an error on purpose, so without `read` the first re-run of a mint + # aborts on a 403 rather than concluding the path is empty. + # + # It is the whole of the widening: `read` on the paths this same stanza + # already lets the controller create and replace, and no `list`, so it can + # only fetch a credential for an agent it was told the name of — which is an + # agent it could already overwrite. controllerPolicyText = '' path "auth/cert/certs/hive-*" { capabilities = ["create", "update", "read", "delete"] @@ -201,7 +209,7 @@ let } path "${credentialMountPath}/data/swarm/agents/*" { - capabilities = ["create", "update"] + capabilities = ["create", "read", "update"] } ''; diff --git a/nix/module-eval/bao-grants.nix b/nix/module-eval/bao-grants.nix index c03e0fcf..0d4075dd 100644 --- a/nix/module-eval/bao-grants.nix +++ b/nix/module-eval/bao-grants.nix @@ -182,11 +182,13 @@ let && !(lib.hasInfix "sys/policies/acl" s); } { - # 🩸 `read` is load-bearing here and is the one capability neither - # sibling has. matrix-ctl's first act is to read this path back and stop - # if something is there — that read IS "and only once", so without the - # capability every container restart would mint a second access token and - # invalidate the hive's. + # 🩸 `read` is load-bearing here, and the publisher — the one sibling + # that still has no `read` — shows what its absence costs. matrix-ctl's + # first act is to read this path back and stop if something is there — + # that read IS "and only once", so without the capability every container + # restart would mint a second access token and invalidate the hive's. + # (The controller holds `read` for the same idempotency reason, on the + # agent prefix.) name = "matrix-ctl may read back the one path it writes"; ok = let @@ -473,16 +475,18 @@ let && !(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"; + # The exact list is the property, not an accident of how it was typed. + # `read` is in it because `mint_and_verify` reads a queue credential back + # before rewriting it; `list` is not, so the controller can fetch a + # credential only for an agent it was handed the name of, never enumerate + # the tree. 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 create/read/update and nothing else"; ok = let s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script; in - lib.hasInfix "path \"secret/data/swarm/agents/*\" {\n capabilities = [\"create\", \"update\"]" s; + lib.hasInfix "path \"secret/data/swarm/agents/*\" {\n capabilities = [\"create\", \"read\", \"update\"]" s; } { # The policy above grants paths under a mount nothing else creates, so