bring existing agent cert roles to the 1h token ttl on controller start
This commit is contained in:
parent
e8190a4972
commit
76de27fc97
1 changed files with 513 additions and 13 deletions
233
swarm-controller/src/agent_role_ttl.rs
Normal file
233
swarm-controller/src/agent_role_ttl.rs
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue