swarm-controller: re-issue agent certificates and re-mint queue secrets at half-life
A five-minute pass over every agent some hive's wanted state declares as anything but destroyed queues, per agent: - `MintAgentIdentity` (the node agent creation uses) when the stored certificate at swarm/agents/<agent>/bao-mtls is past half its validity, read from its own notBefore/notAfter: day 45 of the role's 90; - the new `RenewAgentQueueCredential` node when the queue secret at swarm/agents/<agent>/queue is 45 days old or has no mint time. The node re-decides, writes a fresh value with `minted_at`, reads it back, and logs the agent and the old age. When both are due the secret node runs after_any the certificate node, because mint_and_verify compares the queue secret it read with the one it reads back. A credential that is not stored is never created here. `queue::AgentCredential` gains an optional `minted_at` (unix seconds); agent creation now sets it. Stored objects without it decode unchanged and count as due, so every existing queue secret is re-minted on the first pass. Both replacements reach the agent at its next start. The old certificate stays valid until it expires; the old queue secret does not, so a queue reconnect before that restart is denied. Adds x509-cert 0.2 (with der_derive and flagset) to read the validity. docs/swarm/credentials.md: the renewal column splits into automatic re-mint and automatic re-pull, filled from the code as it stands.
This commit is contained in:
parent
b14ff2796c
commit
2115ec2bb3
10 changed files with 763 additions and 29 deletions
|
|
@ -25,8 +25,8 @@
|
|||
//! The authority is the store's own agent CA, generated inside its agent PKI
|
||||
//! mount; its key never leaves the store. It signs no host leaf, and no host
|
||||
//! role pins it, so an agent's certificate satisfies only that agent's role.
|
||||
//! Nothing re-issues a leaf before it expires (the role's `ttl`); until a
|
||||
//! renewal path exists, an operator re-runs agent creation.
|
||||
//! [`crate::agent_renewal`] re-issues a live agent's leaf once it is past half
|
||||
//! its validity (the role's `ttl`), by queueing the same node as creation.
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use swarm_secret_client::{
|
||||
|
|
@ -98,7 +98,7 @@ const QUEUE_SECRET_BYTES: usize = 32;
|
|||
/// When the kernel will not supply randomness. Bubbled rather than panicked
|
||||
/// on: the caller is a job node that reports a named failure, and a secret
|
||||
/// from a degraded source is worse than no secret.
|
||||
fn generate_queue_secret() -> Result<String> {
|
||||
pub(crate) fn generate_queue_secret() -> Result<String> {
|
||||
let mut bytes = [0u8; QUEUE_SECRET_BYTES];
|
||||
getrandom::fill(&mut bytes).context("drawing a queue secret from the kernel's CSPRNG")?;
|
||||
Ok(base64::Engine::encode(
|
||||
|
|
@ -130,7 +130,8 @@ fn generate_queue_secret() -> Result<String> {
|
|||
/// ⚠️ **Step 3 is idempotent and steps 1–2 are not.** Re-running issues a
|
||||
/// fresh leaf, picked up on the agent's next boot, but leaves an existing
|
||||
/// queue secret alone: this is re-run against running agents, which hold that
|
||||
/// secret in a live connection. Revoking one means deleting the path.
|
||||
/// secret in a live connection. Revoking one means deleting the path;
|
||||
/// replacing one by age is [`crate::agent_renewal`]'s.
|
||||
///
|
||||
/// # Errors
|
||||
/// Anything that stops one of those five steps, with the step named. A
|
||||
|
|
@ -161,15 +162,15 @@ pub async fn mint_and_verify(agent: &str) -> Result<()> {
|
|||
.read_optional(&queue_path)
|
||||
.await
|
||||
.with_context(|| format!("checking whether {queue_path} already holds a credential"))?;
|
||||
// The secret survives a re-run. An object naming a different agent is
|
||||
// corrected by rewriting the name around the *same* `value`, which no live
|
||||
// connection notices.
|
||||
let wanted = queue::AgentCredential {
|
||||
value: match &existing {
|
||||
Some(existing) => existing.value.clone(),
|
||||
None => generate_queue_secret()?,
|
||||
// The secret and its mint time survive a re-run. An object naming a
|
||||
// different agent is corrected by rewriting the name around the *same*
|
||||
// `value`, which no live connection notices.
|
||||
let wanted = match &existing {
|
||||
Some(existing) => queue::AgentCredential {
|
||||
agent: agent.to_owned(),
|
||||
..existing.clone()
|
||||
},
|
||||
agent: agent.to_owned(),
|
||||
None => crate::agent_renewal::fresh(agent, crate::agent_renewal::unix_now())?,
|
||||
};
|
||||
if existing.as_ref() == Some(&wanted) {
|
||||
tracing::info!(
|
||||
|
|
|
|||
Loading…
Reference in a new issue