Watch
0
0
Fork
You've already forked hyperhive
0

export bao client-cert not_before/not_after gauges and alert past 80% of lifetime

This commit is contained in:
atlas 2026-10-25 17:41:47 +01:00 • committed by mara
commit 078696aaee
23 changed files with 604 additions and 53 deletions

View file

@ -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.

View file

@ -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);

View file

@ -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<()> {