In dry-run mode, log what permission changes would be made without
actually applying them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
expires_within_days() compared the exact expiry timestamp against
now + N days, so a cert with 3 days 23 hours remaining would report
"days_remaining=3" but not renew with renew_before_days=3.
Fix by rounding days_until_expiry() to the nearest day (using seconds
for precision) instead of truncating. This matches user expectations
and provides a half-day buffer that absorbs systemd timer
randomization delays.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The HTTP-01 and TLS-ALPN-01 solvers used tokio::sync::Notify to signal
that the server task had started. If the spawned task called
notify_waiters() before the caller registered with notified(), the
notification was lost and the solver would hang indefinitely.
Fix by creating the Notified future before spawning the task.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Support key_mode, key_owner, key_group and cert_mode, cert_owner,
cert_group per certificate. Mode is an octal string (e.g. "0640"),
owner/group are resolved by name via getpwnam/getgrnam.
Permissions are checked before applying — system calls are only made
when the current state differs from the desired state, and changes
are logged. Runs on every renew for existing files, not just when
certificates are renewed. Defaults: key 0600, cert 0644, owner/group
unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
dane-check now queries the authoritative DNS server for published TLSA
records and compares them against expected values computed from the
current certificate. Queries are TSIG-signed using the same key as the
DNS client config (for view selection support). Reports OK, MISSING,
or MISMATCH for each name and exits with code 1 if any check fails.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Reject duplicate certificate names and duplicate hook names
- Reject certificates where a domain has no solver (no solver/solvers
and no default_solver)
- Warn about DNS clients, solvers, and hooks that are defined but
never referenced
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inline per-certificate hooks were always executed even in dry-run mode
because the dry_run parameter was hardcoded to false.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
listen now accepts a single address or a list. Certforge tries binding
all addresses and succeeds if at least one works. This supports
initial provisioning scenarios where a proxy (e.g., haproxy) may not
be running yet — certforge can bind port 80 directly as a fallback
alongside a proxy-forwarded port.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Named hooks defined in [[hook]] are referenced by certificates via
hooks = ["name"]. Triggered hooks run after all renewals complete,
in definition order.
Inline [[certificate.hook]] blocks are still supported for
per-certificate hooks that run immediately after each renewal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per RFC 8555 §8.4, the DNS-01 challenge for *.example.com uses
_acme-challenge.example.com (strip the *. prefix), not the invalid
_acme-challenge.*.example.com.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Omit --name= from systemd-creds encrypt so the encrypted credential
can be loaded under any name via LoadCredentialEncrypted=. Previously,
the embedded name had to match the credential name in the service
unit, causing decryption failures when services used different names.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Breaking config change. The DNS and challenge configuration is now
structured around named, reusable components:
- [dns.*] named DNS client connections with explicit zone field
- [solver.*] challenge solvers (dns-01, http-01, tls-alpn-01)
- Certificates reference solvers by name (solver/solvers fields)
- DANE blocks reference DNS clients by name (dns field)
- Removed automatic zone detection heuristics
- Added default_solver for global fallback
- Validation: DNS-01 rejected for IP addresses, solver/solvers
mutual exclusivity, length matching, reference integrity
HTTP-01 and TLS-ALPN-01 solver implementations to follow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wait 5 seconds after publishing DNS-01 TXT records before notifying
the ACME server, giving the DNS server time to propagate the update.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>