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:
parent
8d206f124f
commit
c2c61b6146
19 changed files with 643 additions and 394 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue