swarm: revoke an agent's queue credential when it is declared destroyed
A per-agent queue credential is minted at agent creation and nothing has ever removed it. An agent declared destroyed loses its container and keeps its credential: a bearer secret recovered from a snapshot or a stale capture still authenticates as that agent, so the set of usable credentials only grows. Delete the path the mint published, on the one transition that ends an agent's life. It mirrors step 3 of `mint_and_verify` and no other step: the leaf, the ACL document and the cert role are what a hive uses to collect an agent's secrets and are re-minted on every run of the mint. Every version, not the newest. The mint rewrites the path when the principal it names needs correcting, so KV v2's plain delete would leave the identical secret readable at ?version=N. That is a separately-ACL'd path, hence the second stanza in the controller's grant -- `delete` on metadata discloses nothing, and `update` on the data path already lets this principal destroy any agent credential's usability. The destroy is not blocked by a failed revocation: the declaration is already published and refusing the call would leave an operator with an agent they cannot tear down. The failure is logged at error instead, naming the agent, since a silent orphan is the fault being removed.
This commit is contained in:
parent
88c386c96c
commit
8caf688ee4
5 changed files with 229 additions and 12 deletions
|
|
@ -362,18 +362,16 @@ let
|
|||
# ../module-eval.nix cannot read them, and a heredoc would make the HCL's
|
||||
# indentation a function of this file's.
|
||||
#
|
||||
# The last grant is a different kind from the others: they let the controller
|
||||
# bootstrap hives, this lets it write an agent's credentials. Two things about
|
||||
# it do not read as they look.
|
||||
#
|
||||
# `secret/data/` is KV v2's ACL prefix, not part of the path the code passes:
|
||||
# `swarm-secret-client` writes `swarm/agents/<agent>/...` under mount
|
||||
# `secret`, and the engine inserts `data/`. Matching the code's spelling
|
||||
# literally would grant nothing.
|
||||
#
|
||||
# `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.
|
||||
# The credential-write grant below (unlike the bootstrap ones above it) has
|
||||
# three things about its paths that do not read as written. `secret/data/` is
|
||||
# KV v2's ACL prefix, not part of the path the code passes:
|
||||
# `swarm-secret-client` writes `swarm/agents/<agent>/...` under mount `secret`,
|
||||
# and the engine inserts `data/` — matching the code's spelling literally would
|
||||
# grant nothing. `read` is required 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. `metadata/` is the revocation half: `delete` on
|
||||
# `data/` only soft-deletes the newest version, and `+` being one path segment
|
||||
# keeps this to the queue leaf alone.
|
||||
#
|
||||
# The swarm appservice token and its own OIDC client secret, read-only: it
|
||||
# uses both and writes neither. matrix-ctl publishes the token
|
||||
|
|
@ -402,6 +400,10 @@ let
|
|||
capabilities = ["create", "read", "update"]
|
||||
}
|
||||
|
||||
path "${credentialMountPath}/metadata/swarm/agents/*" {
|
||||
capabilities = ["delete"]
|
||||
}
|
||||
|
||||
path "${credentialMountPath}/data/${swarmAppserviceTokenLeaf}" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1147,6 +1147,25 @@ let
|
|||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/agents/*\" {\n capabilities = [\"create\", \"read\", \"update\"]" s;
|
||||
}
|
||||
{
|
||||
# Revocation, and the reason it is a stanza of its own: `delete` on the
|
||||
# `data/` path soft-deletes the newest version and leaves earlier ones
|
||||
# readable, so a credential the mint had ever rewritten would survive it.
|
||||
# `metadata/` is the path that removes every version, and the store ACLs
|
||||
# it separately — without this grant the revocation is a 403 and a
|
||||
# destroyed agent's credential stays valid.
|
||||
#
|
||||
# Pinned as the whole capability list for the same reason as the stanza
|
||||
# above: `read` or `list` here would hand a write-only principal the
|
||||
# version history of every agent's secrets.
|
||||
name = "the controller may revoke an agent credential, and only by removing every version of it";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/metadata/swarm/agents/*\" {\n capabilities = [\"delete\"]" s
|
||||
&& !(lib.hasInfix "secret/metadata/*" s);
|
||||
}
|
||||
{
|
||||
# The swarm appservice token is a homeserver-admin credential. The
|
||||
# controller mints agents' accounts with it and has no business replacing
|
||||
|
|
|
|||
Loading…
Reference in a new issue