From 8f1d1f85d5fa7c2b5bf777c578e15ee1e0b908c7 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 29 Jul 2026 07:53:44 +0000 Subject: [PATCH 1/2] fix(gateway): reject expired certificate reloads --- dstack/gateway/src/cert_store.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dstack/gateway/src/cert_store.rs b/dstack/gateway/src/cert_store.rs index deeaed393..b133eea74 100644 --- a/dstack/gateway/src/cert_store.rs +++ b/dstack/gateway/src/cert_store.rs @@ -238,6 +238,12 @@ impl CertStoreBuilder { /// The domain is the base domain (e.g., "example.com"). /// All gateway certificates are wildcard certs for "*.{domain}". pub fn add_cert(&mut self, domain: &str, data: &CertData) -> Result<()> { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .context("system time is before Unix epoch")? + .as_secs(); + anyhow::ensure!(data.not_after > now, "certificate is expired"); + let certified_key = parse_certified_key(&data.cert_pem, &data.key_pem) .with_context(|| format!("failed to parse certificate for {}", domain))?; From 54efaa2b04cd4565610a25ccd4c6284dd0f26d39 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 29 Jul 2026 07:53:58 +0000 Subject: [PATCH 2/2] test(gateway): retain cert on expired hot reload --- dstack/gateway/src/cert_store.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/dstack/gateway/src/cert_store.rs b/dstack/gateway/src/cert_store.rs index b133eea74..f1a78017f 100644 --- a/dstack/gateway/src/cert_store.rs +++ b/dstack/gateway/src/cert_store.rs @@ -476,4 +476,29 @@ mod tests { ); assert!(resolver.get().has_cert_for_sni("app.example.com")); } + + #[test] + fn expired_update_retains_previous_certificate() { + let original = make_test_cert_data(); + let mut expired = make_test_cert_data(); + expired.not_after = 1; + + let resolver = CertResolver::new(); + resolver + .update_cert("example.com", &original) + .expect("failed to install original certificate"); + resolver + .update_cert("example.com", &expired) + .expect_err("expired certificate must be rejected"); + + assert_eq!( + resolver + .get() + .get_cert_data("example.com") + .expect("original certificate was lost") + .not_after, + original.not_after + ); + assert!(resolver.get().has_cert_for_sni("app.example.com")); + } }