48 Commits
Author SHA1 Message Date
Roman KivalinandClaude Opus 4.6 94e55f79ac Bump version to 0.11.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:31:42 +02:00
Roman KivalinandClaude Opus 4.6 a100395097 Only save key when new/rotated, make DANE non-fatal, fix service file
- 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>
v0.10.0
2026-04-21 11:19:03 +02:00
Roman KivalinandClaude Opus 4.6 81c4e4f507 Skip systemd reload/restart hooks for inactive units
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>
2026-04-18 14:57:38 +02:00
Roman KivalinandClaude Opus 4.6 533c65cca9 Fix changelog extraction stripping non-empty section headers
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>
2026-04-18 00:16:20 +02:00
Roman KivalinandClaude Opus 4.6 82a3986aea Bump version to 0.10.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 00:12:46 +02:00
Roman KivalinandClaude Opus 4.6 e37507fc0e Simplify release workflow and build Arch package from source
- 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>
v0.9.0
2026-04-18 00:00:51 +02:00
Roman KivalinandClaude Opus 4.6 95a3715d32 Respect dry-run flag for file permission updates
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>
2026-04-17 23:14:17 +02:00
Roman KivalinandClaude Opus 4.6 cf7ccb45db Fix renew_before_days not triggering on exact day boundary
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>
2026-04-17 21:43:59 +02:00
Roman KivalinandClaude Opus 4.6 9af56cee30 Bump version to 0.9.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 21:34:08 +02:00
Roman KivalinandClaude Opus 4.6 ff83ce0ca1 Fix solver startup race condition with Notify
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>
v0.8.0
2026-04-17 21:28:36 +02:00
Roman KivalinandClaude Opus 4.6 2e2cabe19d Bump version to 0.8.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 21:15:40 +02:00
Roman KivalinandClaude Opus 4.6 c717da348a Add custom file permissions for key and certificate files
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>
v0.7.0
2026-04-17 21:03:59 +02:00
Roman KivalinandClaude Opus 4.6 aeb17eef08 Bump version to 0.7.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:56:52 +02:00
Roman KivalinandClaude Opus 4.6 d70c7eec05 Implement account deactivate command
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>
v0.6.0
2026-04-14 17:29:27 +02:00
Roman KivalinandClaude Opus 4.6 9a09ec5bab Improve user-facing output and README documentation
- 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>
2026-04-14 17:20:42 +02:00
Roman KivalinandClaude Opus 4.6 a0d275527b Remove dead code and simplify listen address deserialization
- Remove unused delete_tlsa_record, find_certificate, CertInfo.pem_chain,
  ChallengeInfo.challenge_type
- Remove all #[allow(dead_code)] annotations
- Replace custom deserializers with generic OneOrMany<T> enum used
  directly in SolverConfig fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:16:02 +02:00
Roman KivalinandClaude Opus 4.6 b6d299300d 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>
2026-04-14 17:02:09 +02:00
Roman KivalinandClaude Opus 4.6 538ee182d9 Sign DNS queries with TSIG for view selection support
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>
2026-04-14 16:31:43 +02:00
Roman KivalinandClaude Opus 4.6 7398d5523c Make TLSA record replacement atomic with a single DNS update
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>
2026-04-14 16:22:34 +02:00
Roman KivalinandClaude Opus 4.6 d6e95bade5 Fix account show creating account when credentials are missing
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>
2026-04-14 15:24:30 +02:00
Roman KivalinandClaude Opus 4.6 4a41e60790 Remove init command
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>
2026-04-14 15:21:12 +02:00
Roman KivalinandClaude Opus 4.6 b2b470dd69 Implement TLSA record verification in dane-check
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>
2026-04-14 15:12:15 +02:00
Roman KivalinandClaude Opus 4.6 07384116cb Add config validation for uniqueness, missing solvers, and unused definitions
- 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>
2026-04-14 15:02:20 +02:00
Roman KivalinandClaude Opus 4.6 ece514133d Fix dry_run flag not passed to inline hooks
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>
2026-04-14 14:33:11 +02:00
Roman KivalinandClaude Opus 4.6 280e008ec9 Support multiple listen addresses for HTTP-01 and TLS-ALPN-01 solvers
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>
2026-04-14 10:48:16 +02:00
Roman KivalinandClaude Opus 4.6 004d93384d Add named hooks with cross-certificate deduplication
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>
2026-04-14 10:41:04 +02:00
Roman KivalinandClaude Opus 4.6 252c2967fd Bump version to 0.6.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:24:31 +02:00
Roman KivalinandClaude Opus 4.6 f63af47487 Fix wildcard DNS-01 challenges
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>
v0.5.0
2026-04-13 23:40:39 +02:00
Roman KivalinandClaude Opus 4.6 96f98352a8 Don't bind systemd-creds to a specific credential name
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>
2026-04-13 23:29:26 +02:00
Roman KivalinandClaude Opus 4.6 37f6e4d443 Support multiple zones per DNS client with automatic zone matching
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>
2026-04-13 23:14:54 +02:00
Roman KivalinandClaude Opus 4.6 3674a222a6 Bump version to 0.5.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:17:03 +02:00
Roman KivalinandClaude Opus 4.6 4ecd500a96 Fix PKGBUILD: build from local repo, options=(!lto) for Arch compat
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>
v0.4.0
2026-04-13 22:04:33 +02:00
Roman KivalinandClaude Opus 4.6 f36b2f8559 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>
2026-04-13 20:59:04 +02:00
Roman KivalinandClaude Opus 4.6 0726e829a9 Add HMAC-SHA-384 TSIG support, document HMAC-SHA-512 truncation limitation
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>
2026-04-13 20:35:12 +02:00
Roman KivalinandClaude Opus 4.6 25722231e0 Update README for new config format and challenge solvers
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>
2026-04-13 19:48:01 +02:00
Roman KivalinandClaude Opus 4.6 47fdf010f9 Add HTTP-01 and TLS-ALPN-01 challenge solvers
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>
2026-04-13 19:40:57 +02:00
Roman KivalinandClaude Opus 4.6 f990494242 Add solver trait and refactor renewal to use it
- 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>
2026-04-13 19:34:50 +02:00
Roman KivalinandClaude Opus 4.6 da8c937782 Refactor config: named DNS clients, challenge solvers, per-domain solver assignment
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>
2026-04-13 19:15:01 +02:00
Roman KivalinandClaude Opus 4.6 40d7240ef9 Bump version to 0.4.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:54:40 +02:00
Roman KivalinandClaude Opus 4.6 af8cdd3e41 Add DNS propagation delay before ACME challenge validation
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>
v0.3.0
2026-04-12 23:31:59 +02:00
Roman KivalinandClaude Opus 4.6 8ed22ca1d5 Bump version to 0.3.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:29:36 +02:00
Roman KivalinandClaude Opus 4.6 08cacff569 Build release binaries on Ubuntu 22.04 for glibc 2.35 compat
Debian 12 (bookworm) ships glibc 2.36. The previous ubuntu-latest
(24.04) produced binaries requiring glibc 2.39.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.2.0
2026-04-12 22:43:35 +02:00
Roman KivalinandClaude Opus 4.6 1c5aa8b930 Bump version to 0.2.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:52:07 +02:00
Roman KivalinandClaude Opus 4.6 7161fe2fb6 Fix release workflow and drop aws-lc-rs dependency
- 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>
v0.1.0
2026-04-12 21:44:12 +02:00
Roman KivalinandClaude Opus 4.6 000ec410b1 Add README and CHANGELOG
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:07:20 +02:00
Roman KivalinandClaude Opus 4.6 1c517ecbab Add packaging, CI, and license
- MIT license
- Arch Linux PKGBUILD
- Debian packaging (debian/)
- GitHub Actions: CI (test + clippy) and release workflow
  builds x86_64 + aarch64 binaries, Arch and Debian packages
- Default config is now no-op (certificate blocks commented out)
- Fix clippy warnings (collapsible ifs, redundant closures)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:47:00 +02:00
Roman KivalinandClaude Opus 4.6 32697369cf Optimize release binary size (16MB → 9MB)
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>
2026-04-12 20:12:06 +02:00
Roman KivalinandClaude Opus 4.6 c1fdfba3fe Initial implementation of certforge ACME certificate manager
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>
2026-04-12 20:07:27 +02:00