From e99e89a58354ad66ee2b7d8c14f67eca33aa418c Mon Sep 17 00:00:00 2001 From: Daniel Nash <38335330+dannash100@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:34:15 +1200 Subject: [PATCH] renew the canopy cert --- src/bin/operator.rs | 21 +++++++++++++++++++++ src/canopy.rs | 11 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/bin/operator.rs b/src/bin/operator.rs index 7b29151..a4e07da 100644 --- a/src/bin/operator.rs +++ b/src/bin/operator.rs @@ -44,6 +44,9 @@ const DEFAULT_METRICS_ADDR: &str = "[::]:8080"; const DEFAULT_METRICS_PORT: u16 = 8080; const DEFAULT_BROKER_ADDR: &str = "[::]:9091"; const DEFAULT_CANOPY_RECONCILE_INTERVAL_SECS: u64 = 30; +/// The client certificate bestool mints at construction lives six days, so the +/// operator re-mints well inside that. +const CANOPY_CERT_RENEW_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); const CONFIGMAP_NAME: &str = "postgres-restore-operator-config"; /// Annotate the operator's own pod with the running version. @@ -442,6 +445,24 @@ async fn main() -> anyhow::Result<()> { register_capabilities(register_ctx).await; }); + let renew_ctx = ctx.clone(); + tokio::spawn(async move { + let mut interval = tokio::time::interval(CANOPY_CERT_RENEW_INTERVAL); + interval.tick().await; + loop { + interval.tick().await; + let Some(canopy) = renew_ctx.canopy.as_ref() else { + break; + }; + match canopy.renew().await { + Ok(()) => info!("renewed the canopy client certificate"), + Err(error) => { + warn!(error = %error, "renewing the canopy client certificate failed"); + } + } + } + }); + let interval_secs = std::env::var("CANOPY_RECONCILE_INTERVAL_SECS") .ok() .and_then(|v| v.parse::().ok()) diff --git a/src/canopy.rs b/src/canopy.rs index e8825b6..a1be877 100644 --- a/src/canopy.rs +++ b/src/canopy.rs @@ -148,6 +148,17 @@ impl Client { Ok(Some(Self { inner })) } + /// Mint a fresh client certificate from the device key. The certificate the + /// client builds at construction is short-lived, so a process outliving it + /// authenticates with an expired one and canopy's edge rejects every call. + pub async fn renew(&self) -> Result<()> { + self.inner + .transport() + .renew() + .await + .map_err(|err| Error::Canopy(format!("renew: {err}"))) + } + /// Register the intent descriptors this consumer supports. Replaces the /// registered set wholesale (per canopy's semantics). Each descriptor /// carries the intent name, the canopy semantics it opts into, and its