mirror of
https://github.com/rkivalin/certforge.git
synced 2026-10-13 00:00:42 +00:00
Add configurable DNS propagation delay per solver
DNS-01 solvers now accept a propagation_delay option (default 5s). When multiple solvers are used for a certificate, the longest delay is used. The Solver trait method changes from needs_propagation_delay() returning bool to propagation_delay() returning Option<Duration>. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
538ee182d9
commit
b6d299300d
@@ -7,6 +7,7 @@
|
||||
- Named hooks (`[[hook]]`) referenced by certificates via `hooks = ["name"]`. Triggered hooks run after all renewals complete, in definition order.
|
||||
- Multiple listen addresses for HTTP-01 and TLS-ALPN-01 solvers (`listen = ["[::]:80", "[::]:8080"]`). Certforge tries all addresses and succeeds if at least one binds. Useful for initial provisioning when a proxy isn't running yet.
|
||||
- `dane-check` now queries published TLSA records from DNS and verifies them against expected values. Exits with code 1 if any records are missing or mismatched.
|
||||
- Configurable DNS propagation delay per solver (`propagation_delay`, default 5s).
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -135,7 +135,8 @@ Solvers are defined in `[solver.*]` blocks. Each certificate references a solver
|
||||
```toml
|
||||
[solver.dns]
|
||||
type = "dns-01"
|
||||
dns = "main" # references a [dns.*] block
|
||||
dns = "main" # references a [dns.*] block
|
||||
propagation_delay = 5 # seconds to wait for DNS propagation (default: 5)
|
||||
```
|
||||
|
||||
**HTTP-01** -- serves challenge tokens over HTTP:
|
||||
|
||||
Vendored
+1
@@ -5,6 +5,7 @@ certforge (0.6.0-1) unstable; urgency=low
|
||||
* Fix inline hooks ignoring --dry-run flag
|
||||
* Improve config validation
|
||||
* dane-check verifies published TLSA records against expected values
|
||||
* Configurable DNS propagation delay per solver
|
||||
|
||||
-- Roman Kivalin <roman@shl.dev> Mon, 14 Apr 2026 00:00:00 +0000
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ protocol = "tcp"
|
||||
[solver.dns]
|
||||
type = "dns-01"
|
||||
dns = "main" # references [dns.main]
|
||||
# propagation_delay = 5 # seconds to wait for DNS propagation (default: 5)
|
||||
|
||||
# [solver.dns-org]
|
||||
# type = "dns-01"
|
||||
@@ -77,6 +78,7 @@ dns = "main" # references [dns.main]
|
||||
# name = "mail"
|
||||
# domains = ["mail.example.com"]
|
||||
# solver = "dns"
|
||||
# key_type = "ecdsa-p256" # ecdsa-p256, ecdsa-p384
|
||||
# key_credential = "/etc/credstore.encrypted/mail-tls-key"
|
||||
# # key_path = "/etc/certforge/keys/mail.key"
|
||||
# cert_path = "/etc/certforge/certs/mail.pem"
|
||||
|
||||
+10
-3
@@ -127,6 +127,10 @@ fn default_dns_protocol() -> DnsProtocol {
|
||||
DnsProtocol::Tcp
|
||||
}
|
||||
|
||||
fn default_propagation_delay() -> u64 {
|
||||
5
|
||||
}
|
||||
|
||||
/// Challenge solver configuration, tagged by type.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -136,6 +140,9 @@ pub enum SolverConfig {
|
||||
Dns01 {
|
||||
/// Reference to a [dns.*] client config by name.
|
||||
dns: String,
|
||||
/// Seconds to wait for DNS propagation after publishing challenge records.
|
||||
#[serde(default = "default_propagation_delay")]
|
||||
propagation_delay: u64,
|
||||
},
|
||||
#[serde(rename = "http-01")]
|
||||
Http01 {
|
||||
@@ -433,7 +440,7 @@ impl Config {
|
||||
// Validate solver configs
|
||||
for (name, solver) in &self.solver {
|
||||
match solver {
|
||||
SolverConfig::Dns01 { dns } => {
|
||||
SolverConfig::Dns01 { dns, .. } => {
|
||||
if !self.dns.contains_key(dns) {
|
||||
return Err(Error::Config(format!(
|
||||
"solver.{name}: dns client '{dns}' not found in [dns.*]"
|
||||
@@ -526,7 +533,7 @@ impl Config {
|
||||
)));
|
||||
}
|
||||
// DNS-01: verify domain matches a zone in the DNS client
|
||||
if let Some(SolverConfig::Dns01 { dns: dns_name }) = self.solver.get(name)
|
||||
if let Some(SolverConfig::Dns01 { dns: dns_name, .. }) = self.solver.get(name)
|
||||
&& let Some(dns_config) = self.dns.get(dns_name.as_str()) {
|
||||
let base = domain.strip_prefix("*.").unwrap_or(domain);
|
||||
let challenge_name = format!("_acme-challenge.{base}");
|
||||
@@ -597,7 +604,7 @@ impl Config {
|
||||
|
||||
// Solvers reference DNS clients
|
||||
for solver in self.solver.values() {
|
||||
if let SolverConfig::Dns01 { dns } = solver {
|
||||
if let SolverConfig::Dns01 { dns, .. } = solver {
|
||||
referenced_dns.insert(dns);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -29,9 +29,12 @@ fn build_solvers(config: &Config, solver_names: &[&str]) -> Result<HashMap<Strin
|
||||
}
|
||||
let solver_config = config.solver_config(name)?;
|
||||
let solver: Box<dyn Solver> = match solver_config {
|
||||
SolverConfig::Dns01 { dns } => {
|
||||
SolverConfig::Dns01 { dns, propagation_delay } => {
|
||||
let dns_config = config.dns_client(dns)?;
|
||||
Box::new(Dns01Solver::new(dns_config.clone()))
|
||||
Box::new(Dns01Solver::new(
|
||||
dns_config.clone(),
|
||||
std::time::Duration::from_secs(*propagation_delay),
|
||||
))
|
||||
}
|
||||
SolverConfig::Http01 { listen, webroot } => {
|
||||
if let Some(webroot) = webroot {
|
||||
@@ -224,10 +227,10 @@ async fn renew_certificate(
|
||||
|
||||
// Run ACME flow, ensuring cleanup happens even on failure
|
||||
let order_result = async {
|
||||
// Wait for DNS propagation if any solver needs it
|
||||
if solvers.values().any(|s| s.needs_propagation_delay()) {
|
||||
tracing::debug!("waiting 5s for DNS propagation");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
// Wait for DNS propagation (use the longest delay across all solvers)
|
||||
if let Some(delay) = solvers.values().filter_map(|s| s.propagation_delay()).max() {
|
||||
tracing::debug!(seconds = delay.as_secs(), "waiting for DNS propagation");
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
|
||||
tracing::debug!(name = %cert_config.name, "setting challenges ready");
|
||||
|
||||
+7
-4
@@ -1,3 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use hickory_proto::rr::Name;
|
||||
|
||||
use crate::acme::ChallengeInfo;
|
||||
@@ -9,11 +11,12 @@ use crate::error::{Error, Result};
|
||||
/// DNS-01 challenge solver using RFC 2136 dynamic updates.
|
||||
pub struct Dns01Solver {
|
||||
dns_config: DnsClientConfig,
|
||||
propagation_delay: Duration,
|
||||
}
|
||||
|
||||
impl Dns01Solver {
|
||||
pub fn new(dns_config: DnsClientConfig) -> Self {
|
||||
Self { dns_config }
|
||||
pub fn new(dns_config: DnsClientConfig, propagation_delay: Duration) -> Self {
|
||||
Self { dns_config, propagation_delay }
|
||||
}
|
||||
|
||||
/// Strip wildcard prefix for DNS-01 challenge name.
|
||||
@@ -81,7 +84,7 @@ impl super::Solver for Dns01Solver {
|
||||
updater.delete_txt_record(&zone, &name, &challenge.dns_value).await
|
||||
}
|
||||
|
||||
fn needs_propagation_delay(&self) -> bool {
|
||||
true
|
||||
fn propagation_delay(&self) -> Option<Duration> {
|
||||
Some(self.propagation_delay)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -2,6 +2,8 @@ pub mod dns01;
|
||||
pub mod http01;
|
||||
pub mod tls_alpn01;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::acme::ChallengeInfo;
|
||||
use crate::error::Result;
|
||||
|
||||
@@ -14,8 +16,9 @@ pub trait Solver: Send + Sync {
|
||||
/// Clean up a challenge after validation (success or failure).
|
||||
async fn cleanup(&self, challenge: &ChallengeInfo) -> Result<()>;
|
||||
|
||||
/// Whether this solver needs a DNS propagation delay after presenting.
|
||||
fn needs_propagation_delay(&self) -> bool {
|
||||
false
|
||||
/// Propagation delay to wait after presenting challenges.
|
||||
/// Returns None for solvers that don't need a delay (HTTP-01, TLS-ALPN-01).
|
||||
fn propagation_delay(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user