diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index aebab744..b02149c5 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -180,9 +180,9 @@ 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` too: `mint_and_verify` reads a credential back before writing so a + # re-run keeps the value a live agent already holds instead of rotating it — + # the read is required, not incidental. controllerPolicyText = '' path "auth/cert/certs/hive-*" { capabilities = ["create", "update", "read", "delete"] @@ -201,7 +201,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