Watch
0
0
Fork
You've already forked hyperhive
0

hive status publishing: read the hive's OIDC secret from the store, retry until it is there

hive-c0re publishes this hive's status to the swarm queue as the OIDC
client `hive-<name>`. Its secret came from
`deploy.hive-controller.statusPublish.clientSecretFile`, which defaulted
to authelia's minted file only on a host running both authelia and the
queue. Every other hive had no secret and never published, so the
controller showed it `never_reported`.

Now `swarm-secret-publish`, beside authelia, also copies each hive's
`hive-<name>` secret to `swarm/hives/<hive>/oidc/client`. Its policy
already allows `swarm/hives/*`. hive-c0re reads that path under the
hive's own store leaf, which the existing `swarm/hives/<hive>/*` grant
covers, and holds the value in memory.

The shared queue client no longer remembers a failed first connect for
the life of the process. A failed attempt is retried with a backoff
from 5s, doubling to a cap of 5 minutes; the dashboard banner stays up
until an attempt succeeds. The two status publishers wait for the
client instead of giving up on the first `None`, so a secret published
after boot is picked up without a restart.

Removed: the `statusPublish.clientSecretFile` option (both spellings,
now removed options with a message), its same-host default, its
LoadCredential, the assertion tying it to the other two coordinates,
and `swarm_queue_client::QueueConfig::from_env`, whose only caller was
this read. Publishing is now on wherever the queue URL and token
endpoint are set and the hive holds a store leaf.

Refs #5015
This commit is contained in:
atlas 2026-10-09 23:12:28 +02:00 • committed by mara
commit c2c61b6146
19 changed files with 643 additions and 394 deletions

View file

@ -86,6 +86,30 @@ pub struct ControllerCredential {
pub value: String,
}
/// The path holding `hive`'s own OIDC client secret: the `hive-<hive>`
/// identity hive-c0re publishes the hive's status to the swarm queue with.
///
/// Not [`agent_client_path`], which is the agents' client on the same hive.
/// Both sit under the hive's own prefix, which the hive's policy reads
/// ([`crate::policy::render`]).
///
/// The nix half is `swarm-secret-publisher.nix`, which writes it.
///
/// # Errors
/// [`Error::PathSegment`] when `hive` contains anything but `[A-Za-z0-9_-]`.
pub fn hive_client_path(hive: &str) -> Result<String, Error> {
let prefix = principal_prefix(Kind::Hive, hive)?;
Ok(format!("{prefix}/oidc/client"))
}
/// What [`hive_client_path`] holds. No client id beside the value: the reader
/// is told its id by its own environment, as the controller is.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HiveClientCredential {
/// The secret itself, under the field name every kind here uses.
pub value: String,
}
/// What [`agent_queue_path`] holds: the secret, and the agent it proves.
///
/// No hive: an agent's identity is not tied to one, and the subjects the
@ -213,6 +237,40 @@ mod tests {
assert!(reads(&own), "{own} should be readable under {granted:?}");
}
/// Spelled out in full because `swarm-secret-publisher.nix` writes the
/// same string by hand.
#[test]
fn a_hives_own_client_lands_under_its_own_principal_prefix() {
assert_eq!(
hive_client_path("alpha").expect("a plain name is legal"),
"swarm/hives/alpha/oidc/client"
);
}
#[test]
fn a_hives_own_policy_reads_its_client_and_no_other_hives() {
let doc = crate::policy::render("alpha").expect("a plain name is legal");
let granted: Vec<&str> = doc
.lines()
.filter_map(|l| l.strip_prefix("path \"secret/data/"))
.filter_map(|p| p.strip_suffix("*\" {"))
.collect();
let reads = |path: &str| granted.iter().any(|g| path.starts_with(g));
let own = hive_client_path("alpha").expect("legal");
assert!(reads(&own), "{own} should be readable under {granted:?}");
let other = hive_client_path("beta").expect("legal");
assert!(!reads(&other), "{other} is readable under {granted:?}");
}
#[test]
fn the_hive_client_objects_field_name_is_the_one_the_publisher_writes() {
let json = serde_json::to_value(HiveClientCredential {
value: "s3cr3t".to_owned(),
})
.expect("serialises");
assert_eq!(json, serde_json::json!({ "value": "s3cr3t" }));
}
#[test]
fn the_controller_objects_field_name_is_the_one_the_publisher_writes() {
let json = serde_json::to_value(ControllerCredential {