export bao client-cert not_before/not_after gauges and alert past 80% of lifetime
This commit is contained in:
parent
daea7a56be
commit
078696aaee
23 changed files with 604 additions and 53 deletions
|
|
@ -94,6 +94,8 @@ serde_json.workspace = true
|
|||
serde_norway = "0.9.42"
|
||||
strum.workspace = true
|
||||
swarm-authelia-bridge-sock.workspace = true
|
||||
# The `bao.client_cert.*` gauges on this exporter.
|
||||
swarm-cert-metrics.workspace = true
|
||||
# `schema`: the forge and schedule routes describe these bodies in the spec.
|
||||
swarm-controller-api = { workspace = true, features = ["schema"] }
|
||||
# The queue connect (token mint + auth callback + reconnect) is shared with
|
||||
|
|
@ -148,8 +150,6 @@ tracing.workspace = true
|
|||
url.workspace = true
|
||||
utoipa.workspace = true
|
||||
utoipa-axum.workspace = true
|
||||
# `agent_renewal.rs` reads an agent certificate's validity window.
|
||||
x509-cert.workspace = true
|
||||
|
||||
# `test-util` for `#[tokio::test(start_paused = true)]`: `agent_renewal`'s
|
||||
# tests wait out its five-minute bound on paused time.
|
||||
|
|
|
|||
|
|
@ -33,9 +33,8 @@ use std::collections::BTreeSet;
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use swarm_secret_client::{SecretStore, mtls, queue};
|
||||
use swarm_secret_client::{SecretStore, cert, mtls, queue};
|
||||
use swarm_wire::wanted::{AgentState, HiveWanted};
|
||||
use x509_cert::der::DecodePem as _;
|
||||
|
||||
use crate::wanted::WantedWriter;
|
||||
|
||||
|
|
@ -101,20 +100,6 @@ pub fn apply_secret_step(
|
|||
})
|
||||
}
|
||||
|
||||
/// A certificate's validity window, `(notBefore, notAfter)` in unix seconds.
|
||||
///
|
||||
/// # Errors
|
||||
/// When `pem` is not one PEM-encoded X.509 certificate.
|
||||
pub fn cert_validity(pem: &str) -> Result<(i64, i64)> {
|
||||
let cert = x509_cert::Certificate::from_pem(pem.as_bytes())
|
||||
.context("decoding the stored certificate")?;
|
||||
let validity = &cert.tbs_certificate.validity;
|
||||
let secs = |t: x509_cert::time::Time| {
|
||||
i64::try_from(t.to_unix_duration().as_secs()).unwrap_or(i64::MAX)
|
||||
};
|
||||
Ok((secs(validity.not_before), secs(validity.not_after)))
|
||||
}
|
||||
|
||||
/// Whether a certificate valid from `not_before` to `not_after` is past half
|
||||
/// its life at `now`.
|
||||
pub fn cert_is_due(not_before: i64, not_after: i64, now: i64) -> bool {
|
||||
|
|
@ -272,7 +257,7 @@ async fn observe_cert(store: &SecretStore, agent: &str, now: i64) -> Observed {
|
|||
return Observed::Unknown;
|
||||
}
|
||||
};
|
||||
match cert_validity(&credential.cert) {
|
||||
match cert::validity(&credential.cert) {
|
||||
Ok((not_before, not_after)) if cert_is_due(not_before, not_after, now) => {
|
||||
Observed::Due(now.saturating_sub(not_before))
|
||||
}
|
||||
|
|
@ -434,21 +419,7 @@ mod tests {
|
|||
const NOW: i64 = 1_790_000_000;
|
||||
const DAY: i64 = 86_400;
|
||||
|
||||
/// Self-signed, CN `hive-agent-atlas`, valid 2026-01-01T00:00:00Z to
|
||||
/// 2026-04-01T00:00:00Z: 90 days, a window the agent role no longer issues. Its key was
|
||||
/// discarded.
|
||||
const CERT_PEM: &str = "-----BEGIN CERTIFICATE-----
|
||||
MIIBizCCATGgAwIBAgIUSFpuYmy1UoHvq/MYiyrAudRaWXcwCgYIKoZIzj0EAwIw
|
||||
GzEZMBcGA1UEAwwQaGl2ZS1hZ2VudC1hdGxhczAeFw0yNjAxMDEwMDAwMDBaFw0y
|
||||
NjA0MDEwMDAwMDBaMBsxGTAXBgNVBAMMEGhpdmUtYWdlbnQtYXRsYXMwWTATBgcq
|
||||
hkjOPQIBBggqhkjOPQMBBwNCAAR7p2eZAxjiE1RdsuSHE3CcTjbnd4swzfnmqUPW
|
||||
9NJN7mvOtqEzxCKfOSFjaIhhyShIQ41/V6vGi80EPDB+uR56o1MwUTAdBgNVHQ4E
|
||||
FgQUxgZFsN1VY3ODrPS1NUfY/x3EnmswHwYDVR0jBBgwFoAUxgZFsN1VY3ODrPS1
|
||||
NUfY/x3EnmswDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiEAnU9e
|
||||
WOioNTUNK9b6FignejpK5FpyrODUuH/iL4TqCh0CIAz6GzJeLebhRPvYlfVUofdV
|
||||
hWx3sOwmUgjkRQXoxY+p
|
||||
-----END CERTIFICATE-----
|
||||
";
|
||||
/// A 90-day window, 2026-01-01T00:00:00Z to 2026-04-01T00:00:00Z.
|
||||
const CERT_NOT_BEFORE: i64 = 1_767_225_600;
|
||||
const CERT_NOT_AFTER: i64 = 1_775_001_600;
|
||||
|
||||
|
|
@ -505,20 +476,6 @@ hWx3sOwmUgjkRQXoxY+p
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_certificates_validity_is_read_from_the_certificate() {
|
||||
assert_eq!(
|
||||
cert_validity(CERT_PEM).expect("a well-formed certificate"),
|
||||
(CERT_NOT_BEFORE, CERT_NOT_AFTER)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn something_that_is_not_a_certificate_is_an_error() {
|
||||
assert!(cert_validity("not a certificate").is_err());
|
||||
assert!(cert_validity("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ninety_day_certificate_is_due_from_day_forty_five() {
|
||||
let (nb, na) = (CERT_NOT_BEFORE, CERT_NOT_AFTER);
|
||||
|
|
|
|||
|
|
@ -171,8 +171,9 @@ async fn main() -> Result<()> {
|
|||
)
|
||||
.ok()
|
||||
.map(|c| Box::new(c) as Box<dyn opentelemetry_http::HttpClient>);
|
||||
let _jobq_metrics_provider =
|
||||
let jobq_metrics_provider =
|
||||
hive_jobq_metrics::spawn_exporter(Arc::clone(&jobq), "swarm-controller", jobq_http_client);
|
||||
register_cert_gauges(jobq_metrics_provider.as_ref());
|
||||
|
||||
let webhook_secret = load_webhook_secret();
|
||||
|
||||
|
|
@ -243,6 +244,16 @@ async fn main() -> Result<()> {
|
|||
serve(listener, state).await
|
||||
}
|
||||
|
||||
/// Registers the `bao.client_cert.*` gauges for the controller's own store
|
||||
/// certificate (`controller.pem`) on the jobq exporter, the one provider built
|
||||
/// at startup rather than on first use. Nothing when no exporter is configured.
|
||||
fn register_cert_gauges(provider: Option<&opentelemetry_sdk::metrics::SdkMeterProvider>) {
|
||||
use opentelemetry::metrics::MeterProvider as _;
|
||||
if let Some(provider) = provider {
|
||||
swarm_cert_metrics::register(&provider.meter("hyperhive.bao_client_cert"), "controller");
|
||||
}
|
||||
}
|
||||
|
||||
/// Start taking the queued review requests and answering the notification
|
||||
/// router's, then serve `state` on `listener`.
|
||||
async fn serve(listener: tokio::net::UnixListener, state: AppState) -> Result<()> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue