Add ACME profile support for IP address certificates

- New per-certificate `profile` field for ACME profile selection
- Let's Encrypt requires "shortlived" for IP address certs (~6 day validity)
- Profile documented in README with link to LE profiles page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Roman Kivalin
2026-04-13 20:59:04 +02:00
co-authored by Claude Opus 4.6
parent 0726e829a9
commit f36b2f8559
6 changed files with 30 additions and 12 deletions
+1
View File
@@ -6,6 +6,7 @@
- Support for HTTP-01 and TLS-ALPN-01 challenge solvers (standalone server + webroot modes)
- IP address certificates (IPv4 and IPv6 SANs)
- ACME profile selection per certificate (e.g., `shortlived` for IP certs on Let's Encrypt)
- Per-domain solver assignment via `solvers = [...]` or single `solver = "..."`
- Named DNS client connections reusable by solvers and DANE blocks
+7 -1
View File
@@ -170,15 +170,21 @@ name = "web"
domains = ["example.com", "www.example.com"]
solver = "dns"
# Per-domain solver (e.g., mixed domain + IP certificate)
# IP address certificate (requires shortlived profile, ~6 day validity)
[[certificate]]
name = "dns-server"
domains = ["dns.example.com", "203.0.113.1", "2001:db8::1"]
solvers = ["dns", "http", "http"]
profile = "shortlived"
renew_before_days = 3
```
If neither `solver` nor `solvers` is set, the global `default_solver` is used.
### ACME profiles
Some ACME providers require a specific profile for certain certificate types. Set `profile` on a certificate to select one. For example, Let's Encrypt requires the `shortlived` profile for IP address certificates (see [Let's Encrypt profiles](https://letsencrypt.org/docs/profiles/)).
### Credential storage
Every secret (ACME account key, TSIG keys, TLS private keys) supports two modes:
+7 -5
View File
@@ -81,13 +81,15 @@ dns = "main" # references [dns.main]
# type = "systemd-reload"
# unit = "postfix.service"
#
# # Certificate with per-domain solver assignment
# # IP address certificate (requires shortlived profile, ~6 day validity)
# [[certificate]]
# name = "mixed"
# domains = ["example.com", "203.0.113.1", "2001:db8::1"]
# name = "dns-server"
# domains = ["dns.example.com", "203.0.113.1", "2001:db8::1"]
# solvers = ["dns", "http", "http"]
# key_path = "/etc/certforge/keys/mixed.key"
# cert_path = "/etc/certforge/certs/mixed.pem"
# profile = "shortlived"
# renew_before_days = 3
# key_path = "/etc/certforge/keys/dns-server.key"
# cert_path = "/etc/certforge/certs/dns-server.pem"
#
# # Simple certificate using default_solver
# [[certificate]]
+7 -5
View File
@@ -99,7 +99,7 @@ impl AcmeClient {
}
/// Create a new ACME order for the given domains/IPs.
pub async fn create_order(&self, domains: &[String]) -> Result<Order> {
pub async fn create_order(&self, domains: &[String], profile: Option<&str>) -> Result<Order> {
let identifiers: Vec<Identifier> = domains
.iter()
.map(|d| {
@@ -111,10 +111,12 @@ impl AcmeClient {
})
.collect();
let order = self
.account
.new_order(&NewOrder::new(&identifiers))
.await?;
let mut new_order = NewOrder::new(&identifiers);
if let Some(profile) = profile {
new_order = new_order.profile(profile);
}
let order = self.account.new_order(&new_order).await?;
Ok(order)
}
+7
View File
@@ -123,6 +123,11 @@ pub struct CertificateConfig {
pub cert_path: PathBuf,
/// ACME profile to use for the order.
/// Some providers require a specific profile for certain certificate types
/// (e.g., Let's Encrypt requires "shortlived" for IP address certificates).
pub profile: Option<String>,
#[serde(default = "default_renew_before_days")]
pub renew_before_days: u32,
@@ -493,6 +498,7 @@ name = "mixed"
domains = ["example.com", "203.0.113.1"]
key_path = "/etc/certforge/keys/mixed.key"
cert_path = "/etc/certforge/certs/mixed.pem"
profile = "shortlived"
solvers = ["dns", "http"]
"#;
@@ -640,6 +646,7 @@ name = "ip"
domains = ["203.0.113.1"]
key_path = "/etc/certforge/keys/ip.key"
cert_path = "/etc/certforge/certs/ip.pem"
profile = "shortlived"
solver = "dns"
"#;
+1 -1
View File
@@ -193,7 +193,7 @@ async fn renew_certificate(
// Create ACME order
tracing::debug!(name = %cert_config.name, domains = ?cert_config.domains, "creating ACME order");
let mut order = acme.create_order(&cert_config.domains).await?;
let mut order = acme.create_order(&cert_config.domains, cert_config.profile.as_deref()).await?;
// Collect challenges
let challenges = AcmeClient::collect_challenges(&mut order, &desired_types).await?;