- save_key was called on every renewal even when reusing an existing
key, which caused EROFS errors under ProtectSystem=strict
- Track whether the key is new/rotated via KeyResult.needs_save
- DANE TLSA publication errors are logged but don't prevent hooks
- Remove ProtectSystem=strict and ReadWritePaths from systemd service
since users can configure arbitrary paths in config
- Add file paths to write error messages for easier diagnosis
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Query the unit's ActiveState via D-Bus before calling reload or
restart. If the unit is not active, log and skip rather than
starting a stopped service.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sed-based empty section filter removed all ### headers followed
by a blank line, but our format always has a blank line between the
header and content. Replace with awk that collects each section and
only prints it if it has content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Merge build and package-debian into a single job per architecture
- Arch package builds from source using the real PKGBUILD with rustup
inside archlinux:base-devel, independent of the binary build
- Add !debug to PKGBUILD options to skip empty debug package
- Extract latest changelog entry for release description
- Reduce from 4 jobs (build, arch, debian, release) to 3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Deactivates the ACME account on the server and removes local
credential files so subsequent commands don't use the stale account.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Display impls for KeyType, DaneUsage, DaneSelector, DaneMatching
so status output shows ecdsa-p256/ee/spki/sha256 instead of Debug
formatting
- Document multi-zone DNS clients (zones field) in README
- Document multiple listen addresses for HTTP-01 and TLS-ALPN-01
- Document default_solver with example
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>
The default hickory TSigner only signs update messages. Wrap it in
AlwaysSignTsig that overrides should_finalize_message to return true
for all messages, so regular queries (used by dane-check) are also
TSIG-signed. This is needed when the DNS server uses TSIG keys for
view selection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously replace_tlsa_records sent separate delete and add requests.
A failure between them would leave the name with no TLSA records. Now
both operations are combined into a single RFC 2136 update message.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split AcmeClient::new into load (existing only) and create (register
new). account show now uses load and errors if no credentials exist,
account create always registers a new account.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The init command printed systemd-creds encrypt commands, but this is
already covered more thoroughly in the README credential storage docs.
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>
DNS clients now accept both `zone` (single) and `zones` (list).
Zone matching uses longest suffix: `mail.example.com` matches
`example.com`, and `host.sub.example.com` prefers `sub.example.com`
over `example.com`.
Config validation checks that every certificate domain (via its DNS-01
solver) and every DANE name matches at least one zone in the
referenced DNS client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Arch's makepkg LTO interferes with ring's asm objects. The standard
Arch workaround is options=(!lto) in the PKGBUILD. Also:
- Add backup= to CI release PKGBUILD for consistency
- Add /pkg/ and *.pkg.tar.zst to .gitignore
- Consolidate PKGBUILD changelog entries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
BIND 9.18+ truncates HMAC-SHA-512 MACs by default, and hickory-dns
does not support verifying truncated HMACs. Document the workaround
(use hmac-sha256) and add hmac-sha384 as an additional option.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document named DNS clients, solver configuration (DNS-01, HTTP-01,
TLS-ALPN-01), per-domain solver assignment, and IP address certificates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HTTP-01 solver with two modes:
- Standalone: embedded hyper HTTP server on configurable address
- Webroot: writes token files for an existing web server
TLS-ALPN-01 solver:
- Embedded rustls TLS server with ALPN "acme-tls/1"
- Dynamic certificate resolution per-identifier
- Self-signed cert with critical acmeIdentifier extension (RFC 8737)
Both solvers use rustls/ring crypto backend (no aws-lc-rs dependency).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Solver trait with present/cleanup/needs_propagation_delay methods
- DNS-01 solver extracted from inline renew.rs code
- ACME module now supports mixed Identifier::Dns + Identifier::Ip
- Generic ChallengeInfo replaces DnsChallenge
- Per-domain challenge type selection based on solver config
- Propagation delay only applied when DNS-01 solvers are in use
- Solver instances deduplicated by name across domains
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>
- Use ring backend for instant-acme instead of aws-lc-rs (fixes Arch
build linker errors, also reduces binary from 9MB to 6.6MB)
- Remove redundant mv in deb packaging (dpkg-deb already outputs the
correct filename)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enable LTO, single codegen unit, symbol stripping, and panic=abort
in the release profile. Trim tokio features to only what's needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rust CLI tool for automated ACME certificate management with:
- DNS-01 challenge via RFC 2136 dynamic DNS updates with TSIG auth
- DANE TLSA record publication (RFC 6698/7671/7672)
- systemd credential integration (systemd-creds encrypt/decrypt)
- systemd unit management via D-Bus (zbus)
- Post-renewal hooks (systemctl reload/restart, arbitrary commands)
- Key rotation with DANE pre-publication protocol
- TOML configuration with per-zone DNS overrides
Tested against Let's Encrypt staging. 12 unit tests, zero warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>