Watch
0
0
Fork
You've already forked hyperhive
0

bring existing agent cert roles to the 1h token ttl on controller start

This commit is contained in:
atlas 2026-10-29 13:47:32 +01:00 • committed by mara
commit 76de27fc97

View file

@ -0,0 +1,233 @@
//! The token lifetime of the agents' cert-auth roles that already exist.
//!
//! A role is written with its lifetimes only when an agent's certificate is
//! minted or renewed, and a role written before the lifetimes were set leaves
//! every login at the mount's default of 32 days, one store lease each. The
//! start pass in [`crate::read_policy`] calls [`bring_current`] so those roles
//! take [`swarm_secret_client::client::CERT_ROLE_TOKEN_TTL`] without waiting for
//! a renewal.
//!
//! Only the two lifetimes are written: the role's certificate, allowed names
//! and policies stay as they are, and no token or lease is revoked. Tokens
//! issued before expire on their own.
use swarm_secret_client::{client::DEFAULT_CERT_MOUNT, policy};
use crate::store::AccessStore;
/// What one pass did, one count per role.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Counts {
pub rewritten: usize,
pub already_current: usize,
pub failed: usize,
}
/// Give every agent role among `roles`, a listing of the cert mount's roles,
/// the current token lifetimes, and log the counts once.
///
/// A role that cannot be read or written is named in a warning and skipped, so
/// it never stops the rest or the start. A role gone since the listing is
/// counted nowhere: there is nothing left to bring current.
pub async fn bring_current(store: &impl AccessStore, roles: &[String]) -> Counts {
let mut counts = Counts::default();
for agent in policy::agents_from_role_names(roles) {
let Ok(role) = policy::agent_object_name(&agent) else {
continue;
};
match bring_one(store, &role).await {
Ok(Outcome::Rewritten) => counts.rewritten += 1,
Ok(Outcome::AlreadyCurrent) => counts.already_current += 1,
Ok(Outcome::Gone) => {}
Err(e) => {
counts.failed += 1;
tracing::warn!(role, error = %e, "this agent role's token lifetime could not be brought current");
}
}
}
tracing::info!(
rewritten = counts.rewritten,
already_current = counts.already_current,
failed = counts.failed,
"agent role token lifetimes checked"
);
counts
}
enum Outcome {
Rewritten,
AlreadyCurrent,
Gone,
}
async fn bring_one(
store: &impl AccessStore,
role: &str,
) -> Result<Outcome, swarm_secret_client::Error> {
let Some(ttls) = store.cert_role_token_ttls(DEFAULT_CERT_MOUNT, role).await? else {
return Ok(Outcome::Gone);
};
if ttls.is_current() {
return Ok(Outcome::AlreadyCurrent);
}
store
.set_cert_role_token_ttls(DEFAULT_CERT_MOUNT, role)
.await?;
Ok(Outcome::Rewritten)
}
#[cfg(test)]
mod tests {
use swarm_secret_client::client::{CERT_ROLE_TOKEN_TTL_SECS, CertRoleTokenTtls};
use crate::store::fake::{FakeAccess, FakeRole};
use super::{Counts, bring_current};
const CURRENT: CertRoleTokenTtls = CertRoleTokenTtls {
token_ttl: CERT_ROLE_TOKEN_TTL_SECS,
token_max_ttl: CERT_ROLE_TOKEN_TTL_SECS,
};
const STALE: CertRoleTokenTtls = CertRoleTokenTtls {
token_ttl: 0,
token_max_ttl: 0,
};
fn role(agent: &str, ttls: CertRoleTokenTtls) -> FakeRole {
FakeRole {
certificate: format!("CA-{agent}"),
common_name: format!("hive-agent-{agent}"),
policies: vec!["hive-agent".to_owned()],
ttls,
}
}
fn listing(agents: &[&str]) -> Vec<String> {
agents.iter().map(|a| format!("hive-agent-{a}")).collect()
}
#[tokio::test]
async fn a_stale_role_takes_the_ttl_and_keeps_its_certificate_names_and_policies() {
let before = role("a", STALE);
let store = FakeAccess::default().with_role("hive-agent-a", before.clone());
let counts = bring_current(&store, &listing(&["a"])).await;
assert_eq!(
counts,
Counts {
rewritten: 1,
..Counts::default()
}
);
assert_eq!(
store.role("hive-agent-a"),
Some(FakeRole {
ttls: CURRENT,
..before
})
);
}
#[tokio::test]
async fn a_role_with_a_lifetime_unset_or_different_is_rewritten() {
let store = FakeAccess::default()
.with_role(
"hive-agent-a",
role(
"a",
CertRoleTokenTtls {
token_ttl: CERT_ROLE_TOKEN_TTL_SECS,
token_max_ttl: 0,
},
),
)
.with_role(
"hive-agent-b",
role(
"b",
CertRoleTokenTtls {
token_ttl: 900,
token_max_ttl: 900,
},
),
);
let counts = bring_current(&store, &listing(&["a", "b"])).await;
assert_eq!(counts.rewritten, 2);
assert_eq!(store.role("hive-agent-a").expect("kept").ttls, CURRENT);
assert_eq!(store.role("hive-agent-b").expect("kept").ttls, CURRENT);
}
#[tokio::test]
async fn a_current_role_is_read_and_not_written() {
let store = FakeAccess::default().with_role("hive-agent-a", role("a", CURRENT));
let counts = bring_current(&store, &listing(&["a"])).await;
assert_eq!(
counts,
Counts {
already_current: 1,
..Counts::default()
}
);
assert_eq!(store.calls(), ["cert_role_token_ttls hive-agent-a"]);
}
#[tokio::test]
async fn a_role_that_fails_does_not_stop_the_others() {
let store = FakeAccess::default()
.with_role("hive-agent-a", role("a", STALE))
.with_role("hive-agent-b", role("b", STALE))
.with_role("hive-agent-c", role("c", STALE))
.with_role("hive-agent-d", role("d", CURRENT))
.with_unreadable_role("hive-agent-a")
.with_unwritable_role("hive-agent-b");
let counts = bring_current(&store, &listing(&["a", "b", "c", "d"])).await;
assert_eq!(
counts,
Counts {
rewritten: 1,
already_current: 1,
failed: 2,
}
);
// The ones after the failures were still handled.
assert_eq!(store.role("hive-agent-c").expect("kept").ttls, CURRENT);
// The failed ones were left as they were.
assert_eq!(store.role("hive-agent-a").expect("kept").ttls, STALE);
assert_eq!(store.role("hive-agent-b").expect("kept").ttls, STALE);
}
#[tokio::test]
async fn only_agent_roles_are_touched() {
let store = FakeAccess::default()
.with_role("hive-agent-a", role("a", STALE))
.with_role("hive-pr1ma", role("pr1ma", STALE))
.with_role("swarm-controller", role("c", STALE));
let roles = vec![
"hive-agent-a".to_owned(),
"hive-pr1ma".to_owned(),
"swarm-controller".to_owned(),
];
let counts = bring_current(&store, &roles).await;
assert_eq!(counts.rewritten, 1);
assert_eq!(store.role("hive-agent-a").expect("kept").ttls, CURRENT);
assert_eq!(store.role("hive-pr1ma").expect("kept").ttls, STALE);
assert_eq!(store.role("swarm-controller").expect("kept").ttls, STALE);
}
#[tokio::test]
async fn a_role_gone_since_the_listing_is_counted_nowhere() {
let store = FakeAccess::default().with_role("hive-agent-b", role("b", STALE));
let counts = bring_current(&store, &listing(&["a", "b"])).await;
assert_eq!(
counts,
Counts {
rewritten: 1,
..Counts::default()
}
);
assert!(
!store
.calls()
.contains(&"set_cert_role_token_ttls hive-agent-a".to_owned())
);
}
}