hive-c0re: deliver an agent's credential from the store to its state dir
mara on #4015: "not merging code without callers", and on the same PR "see issue, we decided what the first thing should be". #3726 decided it: the controller writes a token to the store and tells the hive; the hive reads it back and writes /agents/<agent>/state/matrix-token-<account> at 0600, where matrix.nix's existing systemd.paths glob re-fires the daemon. So this is the hive half of that, and the library's first caller. The notice names a credential and never carries one, and deploy_subject's own doc is why: the auth-callout responder scopes publish and leaves sub unrestricted, so a hive that wanted another's messages could subscribe to them. A secret in that payload would be readable swarm-wide. The value is read from the store under the reading hive's own certificate, where the store's policy is what actually scopes it. Two boundaries guard the two addresses, and they are not the same check. `path::matrix_account` guards the address in the store. `Ident` guards the address on disk -- `agent_state_dir` takes one, so an unvalidated name off the queue cannot reach a directory. I had written the first and assumed it covered both; the compiler refused the `&str` and was right. `token_path` now takes the newtype so a call site cannot forget. The write is atomic because the path-watcher fires on the file appearing: written in place it would be visible while partial, and the daemon would read a truncated credential exactly once, which is the hardest possible failure to reproduce. The temp name is dot-prefixed so it cannot match the `matrix-token*` glob on its way past. The publish grant is here because without it the failure is invisible. policy.rs already says why for its siblings: a refused publish reaches the client as a timeout, so the symptom is a hive that never receives a credential with nothing in either log naming a permission. Two tests: the controller may publish, a hive may not -- its own subject included. A forged notice leaks nothing, but it would make a hive fetch and overwrite a token file for a name the forger chose. Refs #3726
This commit is contained in:
parent
f2713486a5
commit
7a03ce096a
7 changed files with 282 additions and 1 deletions
|
|
@ -352,6 +352,17 @@ impl Policy {
|
|||
// admission). Same failure mode as the knowledge event above: a
|
||||
// refused publish reaches the client as a timeout.
|
||||
swarm_queue_client::DEPLOY_SUBJECT_WILDCARD.to_owned(),
|
||||
// The credential notices: same per-hive family and same wildcard
|
||||
// reasoning as the deploy events, and the same timeout-not-error
|
||||
// failure if this line is missing.
|
||||
//
|
||||
// 🔑 Worth being explicit that this grant is not a confidentiality
|
||||
// boundary, because it looks like one. `sub` is unrestricted, so
|
||||
// any hive could subscribe to another's notices — which is exactly
|
||||
// why the payload names a credential and never carries one. What
|
||||
// scopes the secret is the store's own policy at read time, under
|
||||
// the reading hive's certificate.
|
||||
swarm_queue_client::CREDENTIAL_SUBJECT_WILDCARD.to_owned(),
|
||||
// The wanted-state buckets — one per hive, all created and written
|
||||
// by this single client.
|
||||
//
|
||||
|
|
@ -581,6 +592,36 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reader_may_publish_a_credential_notice_to_any_hive() {
|
||||
let p = policy().permissions("swarm-controller").expect("a reader");
|
||||
assert!(
|
||||
p.publish
|
||||
.contains(&swarm_queue_client::CREDENTIAL_SUBJECT_WILDCARD.to_owned()),
|
||||
"without this grant the controller's publish is refused, and a \
|
||||
refusal arrives as a timeout — a hive that silently never receives \
|
||||
a credential, with nothing in either log saying why"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hive_may_not_publish_a_credential_notice() {
|
||||
// A forged notice cannot leak a secret — the payload carries none — but
|
||||
// it can make a hive fetch and overwrite an agent's token file with
|
||||
// whatever the store holds for a name the forger chose.
|
||||
let p = policy()
|
||||
.permissions("hive-alpha")
|
||||
.expect("a hive is admitted");
|
||||
assert!(
|
||||
!p.publish
|
||||
.iter()
|
||||
.any(|s| s == swarm_queue_client::CREDENTIAL_SUBJECT_WILDCARD
|
||||
|| s == &swarm_queue_client::credential_subject("hive-alpha")),
|
||||
"a hive must not publish credential notices, its own included: {:?}",
|
||||
p.publish
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hive_may_not_publish_a_deploy_event_to_anyone_including_itself() {
|
||||
// Same arm as the knowledge event's, and it matters more here: a forged
|
||||
|
|
|
|||
Loading…
Reference in a new issue