add one shared templated hive policy keyed on the cert alias name
This commit is contained in:
parent
e437f817dd
commit
03796f1964
1 changed files with 232 additions and 12 deletions
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue