diff --git a/dstack/gateway/src/cert_store.rs b/dstack/gateway/src/cert_store.rs index deeaed393..f1a78017f 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))?; @@ -470,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")); + } }