Watch
0
0
Fork
You've already forked hyperhive
0

add one shared templated hive policy keyed on the cert alias name

This commit is contained in:
atlas 2026-10-26 08:57:24 +01:00 • committed by mara
commit 03796f1964

View file

@ -16,10 +16,11 @@
//!
//! The document names the hive it is written for: one stanza is the same
//! everywhere, the other is scoped to the reader's own name
//! ([`swarm_secret_client::policy`] explains the asymmetry). There is no shared
//! document to hoist this render up to — doing that hands every hive the stanza
//! naming one of them. The hive list is loaded once because a config change
//! means a redeploy.
//! ([`swarm_secret_client::policy`] explains the asymmetry). The same rule is
//! also written once for all hives, [`policy::SHARED_HIVE_POLICY`], with the
//! reader's own name taken from its login's alias on the cert mount instead of
//! written into the text; a hive's role attaches both. The hive list is loaded
//! once because a config change means a redeploy.
//!
//! Every agent's policy is rewritten by the same kind of pass
//! ([`ensure_agent_policies`]), so a change to `policy::render_agent` reaches
@ -158,6 +159,7 @@ async fn provision(hives: &[String]) -> Result<(), Unreachable> {
tracing::warn!(hive, error = %format!("{e:#}"), "writing this hive's read policy failed");
}
}
let shared = write_shared_policy(&store).await;
let Some(path) = std::env::var_os(ENV_HIVE_CLIENT_CA) else {
tracing::info!("no hive certificate authority configured; hive roles are not managed here");
@ -169,7 +171,7 @@ async fn provision(hives: &[String]) -> Result<(), Unreachable> {
let ca = std::fs::read_to_string(&path)
.map_err(|e| Unreachable(format!("reading the hive authority {path}: {e}")))?;
for hive in hives {
if let Err(e) = write_role(&store, hive, &ca).await {
if let Err(e) = write_role(&store, hive, &ca, shared).await {
tracing::warn!(hive, error = %format!("{e:#}"), "creating this hive's store role failed");
}
}
@ -187,13 +189,78 @@ async fn write_policy_for(store: &SecretStore, hive: &str) -> Result<()> {
Ok(())
}
/// The role for one hive: its own certificate's subject, its own policy.
async fn write_role(store: &SecretStore, hive: &str, ca: &str) -> Result<()> {
/// Write [`policy::SHARED_HIVE_POLICY`], rendered with the cert mount's
/// accessor. Answers whether it is in place: a hive's role attaches it only
/// then.
///
/// Best effort. A hive's own policy grants it the same paths, so a store that
/// refuses the accessor read (the controller's policy may not carry it yet) or
/// the write costs the shared policy and nothing else.
async fn write_shared_policy(store: &SecretStore) -> bool {
let accessor = match store.auth_mount_accessor(DEFAULT_CERT_MOUNT).await {
Ok(accessor) => accessor,
Err(e) => {
tracing::warn!(
error = %e,
"the shared hive policy is not managed: the cert auth mount's accessor could not be read; hives keep their own policy"
);
return false;
}
};
let document = match policy::render_shared_hives(&accessor) {
Ok(document) => document,
Err(e) => {
tracing::warn!(error = %e, "the shared hive policy is not managed: the cert auth mount's accessor is not usable in a policy; hives keep their own policy");
return false;
}
};
match store
.write_policy(policy::SHARED_HIVE_POLICY, &document)
.await
{
Ok(()) => {
tracing::info!(
policy = policy::SHARED_HIVE_POLICY,
"shared hive policy in place"
);
true
}
Err(e) => {
tracing::warn!(
policy = policy::SHARED_HIVE_POLICY,
error = %e,
"writing the shared hive policy failed; hives keep their own policy"
);
false
}
}
}
/// The policies `hive`'s role attaches: its own, and the shared one when that
/// is in place.
fn attached_policies(hive: &str, shared: bool) -> Result<Vec<String>, Error> {
let mut attached = vec![policy::hive_object_name(hive)?];
if shared {
attached.push(policy::SHARED_HIVE_POLICY.to_owned());
}
Ok(attached)
}
/// The role for one hive: its own certificate's subject, its own policy, and
/// the shared one when `shared`.
async fn write_role(store: &SecretStore, hive: &str, ca: &str, shared: bool) -> Result<()> {
let name = policy::hive_object_name(hive)?;
let attached = attached_policies(hive, shared)?;
store
// The subject is the hive's name: `glue-bao-tls.nix` mints a hive's
// client leaf with its name as the CN, and cert auth matches on that.
.write_cert_role(DEFAULT_CERT_MOUNT, &name, ca, hive, &[&name])
.write_cert_role(
DEFAULT_CERT_MOUNT,
&name,
ca,
hive,
&attached.iter().map(String::as_str).collect::<Vec<_>>(),
)
.await
.with_context(|| format!("writing the cert-auth role {name}"))?;
tracing::info!(hive, role = %name, "hive store role in place");
@ -314,8 +381,8 @@ mod tests {
use std::{cell::Cell, time::Duration};
use super::{
RETRY_ATTEMPTS, RETRY_INTERVAL, RETRY_WINDOW, Unreachable, agent_policies, policy,
retry_until_ok,
RETRY_ATTEMPTS, RETRY_INTERVAL, RETRY_WINDOW, Unreachable, agent_policies,
attached_policies, policy, retry_until_ok,
};
#[test]
@ -378,4 +445,33 @@ mod tests {
);
assert_eq!(written[1].1, policy::render_agent("argus").expect("legal"));
}
#[test]
fn a_hives_role_attaches_its_own_policy_and_the_shared_one() {
assert_eq!(
attached_policies("pr1ma", true).expect("legal"),
["hive-pr1ma", policy::SHARED_HIVE_POLICY]
);
}
#[test]
fn without_the_shared_policy_the_role_keeps_the_hives_own() {
assert_eq!(
attached_policies("pr1ma", false).expect("legal"),
["hive-pr1ma"]
);
}
#[test]
fn the_shared_text_serves_every_hive_what_its_own_policy_does() {
let shared = policy::render_shared_hives("auth_cert_0a1b2c3d").expect("legal accessor");
let template = "{{identity.entity.aliases.auth_cert_0a1b2c3d.name}}";
for hive in ["pr1ma", "beta", "gamma-2", "delta_3"] {
assert_eq!(
shared.replace(template, hive),
policy::render(hive).expect("legal"),
"{hive}"
);
}
}
}