stop writing and attaching per-agent and per-hive bao policies
This commit is contained in:
parent
eac7f2a7c1
commit
139007e656
1 changed files with 253 additions and 419 deletions
|
|
@ -14,18 +14,16 @@
|
|||
//! take down a daemon that serves everything else, and a store that answers is
|
||||
//! answering the same way next time, so the next start is the retry for those.
|
||||
//!
|
||||
//! 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). 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.
|
||||
//! The rule is written once for all hives, [`policy::SHARED_HIVE_POLICY`]: one
|
||||
//! stanza is the same everywhere, the other is scoped to the reader's own name
|
||||
//! ([`swarm_secret_client::policy`] explains the asymmetry), taken from its
|
||||
//! login's alias on the cert mount instead of written into the text. A hive's
|
||||
//! role attaches that policy and no other. 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
|
||||
//! agents minted before it. Its roster is the store's `hive-agent-*` cert-auth
|
||||
//! roles.
|
||||
//! The shared agent policy is written by the same kind of pass
|
||||
//! ([`ensure_agent_policies`]), together with each agent's identity entity.
|
||||
//! Its roster is the store's `hive-agent-*` cert-auth roles.
|
||||
|
||||
use std::{future::Future, time::Duration};
|
||||
|
||||
|
|
@ -128,10 +126,12 @@ where
|
|||
None
|
||||
}
|
||||
|
||||
/// One pass: log in once, write every hive's policy, then every hive's role.
|
||||
/// One pass: log in once, write the shared hive policy, then every hive's role.
|
||||
///
|
||||
/// Policy first, because the role names the policy — the other order leaves a
|
||||
/// window in which the role points at nothing.
|
||||
/// window in which the role points at nothing. Without the policy the roles are
|
||||
/// left as they are: a role written to name a policy that does not exist logs
|
||||
/// its hive in able to read nothing.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Unreachable`] for the states a later attempt can resolve: a store that is
|
||||
|
|
@ -154,12 +154,9 @@ async fn provision(hives: &[String]) -> Result<(), Unreachable> {
|
|||
}
|
||||
Err(e) => return Err(Unreachable(e.to_string())),
|
||||
};
|
||||
for hive in hives {
|
||||
if let Err(e) = write_policy_for(&store, hive).await {
|
||||
tracing::warn!(hive, error = %format!("{e:#}"), "writing this hive's read policy failed");
|
||||
}
|
||||
if !write_shared_policy(&store).await {
|
||||
return Ok(());
|
||||
}
|
||||
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");
|
||||
|
|
@ -171,38 +168,25 @@ 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, shared).await {
|
||||
if let Err(e) = write_role(&store, hive, &ca).await {
|
||||
tracing::warn!(hive, error = %format!("{e:#}"), "creating this hive's store role failed");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One hive's policy: its own name, its own document.
|
||||
async fn write_policy_for(store: &SecretStore, hive: &str) -> Result<()> {
|
||||
let name = policy::hive_object_name(hive)?;
|
||||
store
|
||||
.write_policy(&name, &policy::render(hive)?)
|
||||
.await
|
||||
.with_context(|| format!("writing the read policy {name}"))?;
|
||||
tracing::info!(hive, policy = %name, "hive read policy in place");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// accessor. Answers whether it is in place: hive roles are written 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.
|
||||
/// A store that refuses the accessor read (the controller's policy may not
|
||||
/// carry it yet) or the write leaves the roles already in the store as they are.
|
||||
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"
|
||||
"the shared hive policy is not managed: the cert auth mount's accessor could not be read; hive roles are left as they are"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -210,7 +194,7 @@ async fn write_shared_policy(store: &SecretStore) -> bool {
|
|||
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");
|
||||
tracing::warn!(error = %e, "the shared hive policy is not managed: the cert auth mount's accessor is not usable in a policy; hive roles are left as they are");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -229,28 +213,23 @@ async fn write_shared_policy(store: &SecretStore) -> bool {
|
|||
tracing::warn!(
|
||||
policy = policy::SHARED_HIVE_POLICY,
|
||||
error = %e,
|
||||
"writing the shared hive policy failed; hives keep their own policy"
|
||||
"writing the shared hive policy failed; hive roles are left as they are"
|
||||
);
|
||||
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 policies a hive's role attaches: the shared one, and no policy named for
|
||||
/// the hive.
|
||||
fn attached_policies() -> Vec<String> {
|
||||
vec![policy::SHARED_HIVE_POLICY.to_owned()]
|
||||
}
|
||||
|
||||
/// 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<()> {
|
||||
/// The role for one hive: its own certificate's subject and the shared policy.
|
||||
async fn write_role(store: &SecretStore, hive: &str, ca: &str) -> Result<()> {
|
||||
let name = policy::hive_object_name(hive)?;
|
||||
let attached = attached_policies(hive, shared)?;
|
||||
let attached = attached_policies();
|
||||
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.
|
||||
|
|
@ -267,8 +246,8 @@ async fn write_role(store: &SecretStore, hive: &str, ca: &str, shared: bool) ->
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Rewrite every agent's policy from [`policy::render_agent`], with the retry
|
||||
/// [`ensure_hive_access`] has and for the same reason.
|
||||
/// Write the shared agent policy and give every agent its identity entity, with
|
||||
/// the retry [`ensure_hive_access`] has and for the same reason.
|
||||
///
|
||||
/// Returns once the first pass is done. When that pass cannot reach the store,
|
||||
/// it repeats in the background for up to [`RETRY_WINDOW`].
|
||||
|
|
@ -294,7 +273,7 @@ pub async fn ensure_agent_policies() {
|
|||
tracing::warn!(
|
||||
attempts = RETRY_ATTEMPTS,
|
||||
window = ?RETRY_WINDOW,
|
||||
"giving up on the agent policy rewrite; agents keep the policy they were minted with until this daemon is restarted"
|
||||
"giving up on the agent policy rewrite; the shared agent policy and the agents' identity entities are not checked until this daemon is restarted"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -302,9 +281,9 @@ pub async fn ensure_agent_policies() {
|
|||
}
|
||||
|
||||
/// One pass: log in once, list the agents, write the shared agent policy and
|
||||
/// each one's own, and give each its identity entity.
|
||||
/// give each agent its identity entity.
|
||||
///
|
||||
/// Policies and entities only. An agent's cert-auth role and certificate are
|
||||
/// The policy and entities only. An agent's cert-auth role and certificate are
|
||||
/// `agent_identity::mint_and_verify`'s, and that reissues the certificate, so
|
||||
/// it is not what a pass on every start may call.
|
||||
///
|
||||
|
|
@ -343,46 +322,17 @@ async fn rewrite_agent_policies() -> Result<(), Unreachable> {
|
|||
"writing the shared agent policy failed"
|
||||
),
|
||||
}
|
||||
let policies = agent_policies(&roles);
|
||||
let mut written = 0_usize;
|
||||
for (name, document) in &policies {
|
||||
match store.write_policy(name, document).await {
|
||||
Ok(()) => written += 1,
|
||||
Err(e) => {
|
||||
tracing::warn!(policy = %name, error = %e, "rewriting this agent's policy failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::info!(written, agents = policies.len(), "agent policies rewritten");
|
||||
crate::agent_entity::ensure_agents(&store, &policy::agents_from_role_names(&roles)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The policy to write for each agent among `roles`, a listing of cert-auth
|
||||
/// roles: its name, which is the role's own, and its document.
|
||||
///
|
||||
/// A role that is not an agent's — a hive's `hive-<hive>`, a host's — gets
|
||||
/// nothing: the same `write_policy` call with an agent's document would
|
||||
/// narrow that principal to one agent's subtree.
|
||||
fn agent_policies(roles: &[String]) -> Vec<(String, String)> {
|
||||
policy::agents_from_role_names(roles)
|
||||
.iter()
|
||||
.filter_map(|agent| {
|
||||
Some((
|
||||
policy::agent_object_name(agent).ok()?,
|
||||
policy::render_agent(agent).ok()?,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cell::Cell, time::Duration};
|
||||
|
||||
use super::{
|
||||
RETRY_ATTEMPTS, RETRY_INTERVAL, RETRY_WINDOW, Unreachable, agent_policies,
|
||||
attached_policies, policy, retry_until_ok,
|
||||
RETRY_ATTEMPTS, RETRY_INTERVAL, RETRY_WINDOW, Unreachable, attached_policies, policy,
|
||||
retry_until_ok,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -424,54 +374,15 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn the_pass_writes_agents_policies_under_their_role_names_and_no_one_elses() {
|
||||
let roles: Vec<String> = [
|
||||
"hive-agent-atlas",
|
||||
"hive-pr1ma",
|
||||
"swarm-controller",
|
||||
"hive-agent-argus",
|
||||
]
|
||||
.map(str::to_owned)
|
||||
.to_vec();
|
||||
let written = agent_policies(&roles);
|
||||
let names: Vec<&str> = written.iter().map(|(name, _)| name.as_str()).collect();
|
||||
// The role names an agent's policy by its own name, so a policy written
|
||||
// under any other name is one the role does not attach.
|
||||
assert_eq!(names, ["hive-agent-atlas", "hive-agent-argus"]);
|
||||
assert_eq!(
|
||||
written[0].1,
|
||||
policy::render_agent("atlas").expect("legal"),
|
||||
"the prefix is stripped before rendering, or the document names `agent-atlas`"
|
||||
fn a_hives_role_attaches_the_shared_policy_and_none_named_for_the_hive() {
|
||||
let attached = attached_policies();
|
||||
assert_eq!(attached, [policy::SHARED_HIVE_POLICY]);
|
||||
let own = policy::hive_object_name("pr1ma").expect("legal");
|
||||
assert!(
|
||||
!attached.contains(&own),
|
||||
"the role must not attach a policy named for the hive"
|
||||
);
|
||||
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}"
|
||||
);
|
||||
}
|
||||
// The control: the same lookup finds a policy that is attached.
|
||||
assert!(attached.contains(&policy::SHARED_HIVE_POLICY.to_owned()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue