From c11d7397f6e28d9704f63b5d2ff09aea719366cc Mon Sep 17 00:00:00 2001 From: Evrard-Nil Daillet Date: Wed, 14 Jan 2026 16:11:51 +0100 Subject: [PATCH 1/2] Make DNS TXT record TTL configurable. --- certbot/src/acme_client.rs | 13 +++++++++++-- certbot/src/bot.rs | 23 +++++++++++++++++++---- certbot/src/dns01_client.rs | 3 ++- certbot/src/dns01_client/cloudflare.rs | 7 ++++--- gateway/gateway.toml | 3 +++ gateway/src/config.rs | 9 +++++++++ 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/certbot/src/acme_client.rs b/certbot/src/acme_client.rs index d4ebcf51..50ec4589 100644 --- a/certbot/src/acme_client.rs +++ b/certbot/src/acme_client.rs @@ -28,6 +28,8 @@ pub struct AcmeClient { credentials: Credentials, dns01_client: Dns01Client, max_dns_wait: Duration, + /// TTL for DNS TXT records used in ACME challenges (in seconds). + dns_txt_ttl: u32, } #[derive(Debug, Clone)] @@ -58,6 +60,7 @@ impl AcmeClient { dns01_client: Dns01Client, encoded_credentials: &str, max_dns_wait: Duration, + dns_txt_ttl: u32, ) -> Result { let credentials: Credentials = serde_json::from_str(encoded_credentials)?; let account = Account::from_credentials(credentials.credentials).await?; @@ -67,6 +70,7 @@ impl AcmeClient { dns01_client, credentials, max_dns_wait, + dns_txt_ttl, }) } @@ -75,6 +79,7 @@ impl AcmeClient { acme_url: &str, dns01_client: Dns01Client, max_dns_wait: Duration, + dns_txt_ttl: u32, ) -> Result { let (account, credentials) = Account::create( &NewAccount { @@ -97,6 +102,7 @@ impl AcmeClient { dns01_client, credentials, max_dns_wait, + dns_txt_ttl, }) } @@ -328,10 +334,13 @@ impl AcmeClient { .remove_txt_records(&acme_domain) .await .context("failed to remove existing dns record")?; - debug!("creating TXT record for {acme_domain}"); + debug!( + "creating TXT record for {acme_domain} with TTL {}s", + self.dns_txt_ttl + ); let id = self .dns01_client - .add_txt_record(&acme_domain, &dns_value) + .add_txt_record(&acme_domain, &dns_value, self.dns_txt_ttl) .await .context("failed to create dns record")?; challenges.push(Challenge { diff --git a/certbot/src/bot.rs b/certbot/src/bot.rs index 5a9c775f..1b59b767 100644 --- a/certbot/src/bot.rs +++ b/certbot/src/bot.rs @@ -37,6 +37,10 @@ pub struct CertBotConfig { renew_expires_in: Duration, renewed_hook: Option, max_dns_wait: Duration, + /// TTL for DNS TXT records used in ACME challenges (in seconds). + /// Minimum is 60 for Cloudflare. + #[builder(default = 60)] + dns_txt_ttl: u32, } impl CertBotConfig { @@ -55,9 +59,14 @@ async fn create_new_account( dns01_client: Dns01Client, ) -> Result { info!("creating new ACME account"); - let client = AcmeClient::new_account(&config.acme_url, dns01_client, config.max_dns_wait) - .await - .context("failed to create new account")?; + let client = AcmeClient::new_account( + &config.acme_url, + dns01_client, + config.max_dns_wait, + config.dns_txt_ttl, + ) + .await + .context("failed to create new account")?; let credentials = client .dump_credentials() .context("failed to dump credentials")?; @@ -90,7 +99,13 @@ impl CertBot { let acme_client = match fs::read_to_string(&config.credentials_file) { Ok(credentials) => { if acme_matches(&credentials, &config.acme_url) { - AcmeClient::load(dns01_client, &credentials, config.max_dns_wait).await? + AcmeClient::load( + dns01_client, + &credentials, + config.max_dns_wait, + config.dns_txt_ttl, + ) + .await? } else { create_new_account(&config, dns01_client).await? } diff --git a/certbot/src/dns01_client.rs b/certbot/src/dns01_client.rs index 701d5ba9..b4d4aeaa 100644 --- a/certbot/src/dns01_client.rs +++ b/certbot/src/dns01_client.rs @@ -28,7 +28,8 @@ pub(crate) trait Dns01Api { /// Creates a TXT DNS record with the given domain and content. /// /// Returns the ID of the created record. - async fn add_txt_record(&self, domain: &str, content: &str) -> Result; + /// The `ttl` parameter specifies the time-to-live in seconds (1 = auto, min 60 for Cloudflare). + async fn add_txt_record(&self, domain: &str, content: &str, ttl: u32) -> Result; /// Add a CAA record for the given domain. async fn add_caa_record( diff --git a/certbot/src/dns01_client/cloudflare.rs b/certbot/src/dns01_client/cloudflare.rs index 222028da..d7a6b1f5 100644 --- a/certbot/src/dns01_client/cloudflare.rs +++ b/certbot/src/dns01_client/cloudflare.rs @@ -270,12 +270,13 @@ impl Dns01Api for CloudflareClient { Ok(()) } - async fn add_txt_record(&self, domain: &str, content: &str) -> Result { + async fn add_txt_record(&self, domain: &str, content: &str, ttl: u32) -> Result { let response = self .add_record(&json!({ "type": "TXT", "name": domain, "content": content, + "ttl": ttl, })) .await?; Ok(response.result.id) @@ -358,7 +359,7 @@ mod tests { let subdomain = random_subdomain(); println!("subdomain: {}", subdomain); let record_id = client - .add_txt_record(&subdomain, "1234567890") + .add_txt_record(&subdomain, "1234567890", 60) .await .unwrap(); let record = client.get_txt_records(&subdomain).await.unwrap(); @@ -375,7 +376,7 @@ mod tests { let subdomain = random_subdomain(); println!("subdomain: {}", subdomain); let record_id = client - .add_txt_record(&subdomain, "1234567890") + .add_txt_record(&subdomain, "1234567890", 60) .await .unwrap(); let record = client.get_txt_records(&subdomain).await.unwrap(); diff --git a/gateway/gateway.toml b/gateway/gateway.toml index 78446b0e..cf704b5c 100644 --- a/gateway/gateway.toml +++ b/gateway/gateway.toml @@ -38,6 +38,9 @@ renew_interval = "1h" renew_before_expiration = "10d" renew_timeout = "120s" max_dns_wait = "5m" +# TTL for DNS TXT records used in ACME challenges (in seconds). +# Minimum is 60 for Cloudflare. +dns_txt_ttl = 60 [core.wg] public_key = "" diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 4809aef8..3b990795 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -209,6 +209,14 @@ pub struct CertbotConfig { /// Maximum time to wait for DNS propagation #[serde(with = "serde_duration")] pub max_dns_wait: Duration, + /// TTL for DNS TXT records used in ACME challenges (in seconds). + /// Minimum is 60 for Cloudflare. Lower TTL means faster DNS propagation. + #[serde(default = "default_dns_txt_ttl")] + pub dns_txt_ttl: u32, +} + +fn default_dns_txt_ttl() -> u32 { + 60 } impl CertbotConfig { @@ -228,6 +236,7 @@ impl CertbotConfig { .renew_expires_in(self.renew_before_expiration) .auto_set_caa(self.auto_set_caa) .max_dns_wait(self.max_dns_wait) + .dns_txt_ttl(self.dns_txt_ttl) .build() } From 696441b4ee2b5d81afba3addf1aea72b106f31b5 Mon Sep 17 00:00:00 2001 From: Evrard-Nil Daillet Date: Thu, 15 Jan 2026 11:21:12 +0100 Subject: [PATCH 2/2] Make DNS TXT record TTL configurable in entrypoint and deployment scripts. --- gateway/dstack-app/builder/entrypoint.sh | 1 + gateway/dstack-app/deploy-to-vmm.sh | 2 ++ 2 files changed, 3 insertions(+) diff --git a/gateway/dstack-app/builder/entrypoint.sh b/gateway/dstack-app/builder/entrypoint.sh index cd25da1f..9cd46755 100755 --- a/gateway/dstack-app/builder/entrypoint.sh +++ b/gateway/dstack-app/builder/entrypoint.sh @@ -118,6 +118,7 @@ renew_interval = "1h" renew_before_expiration = "10d" renew_timeout = "5m" max_dns_wait = "${CERTBOT_MAX_DNS_WAIT:-5m}" +dns_txt_ttl = "${CERTBOT_DNS_TXT_TTL:-60}" [core.wg] public_key = "$PUBLIC_KEY" diff --git a/gateway/dstack-app/deploy-to-vmm.sh b/gateway/dstack-app/deploy-to-vmm.sh index 2584d450..47da3fcf 100755 --- a/gateway/dstack-app/deploy-to-vmm.sh +++ b/gateway/dstack-app/deploy-to-vmm.sh @@ -82,6 +82,7 @@ GUEST_AGENT_ADDR=127.0.0.1:9206 WG_ADDR=0.0.0.0:9202 CERTBOT_MAX_DNS_WAIT=5m +CERTBOT_DNS_TXT_TTL=60 # The token used to launch the App APP_LAUNCH_TOKEN=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) @@ -141,6 +142,7 @@ SUBNET_INDEX=$SUBNET_INDEX APP_LAUNCH_TOKEN=$APP_LAUNCH_TOKEN RPC_DOMAIN=$RPC_DOMAIN CERTBOT_MAX_DNS_WAIT=$CERTBOT_MAX_DNS_WAIT +CERTBOT_DNS_TXT_TTL=$CERTBOT_DNS_TXT_TTL EOF if [ -n "$APP_COMPOSE_FILE" ]; then