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>
This commit is contained in:
Roman Kivalin
2026-04-12 20:07:27 +02:00
co-authored by Claude Opus 4.6
commit c1fdfba3fe
22 changed files with 6113 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
[Unit]
Description=Certforge ACME certificate renewal
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certforge renew
# Hardening
ProtectSystem=strict
ReadWritePaths=/etc/certforge/certs /var/lib/certforge /etc/credstore.encrypted
PrivateTmp=yes
NoNewPrivileges=yes
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Certforge daily certificate check
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=1800
Persistent=true
[Install]
WantedBy=timers.target
+85
View File
@@ -0,0 +1,85 @@
# Example certforge configuration
#
# Full documentation: see the project README
[acme]
directory_url = "https://acme-v02.api.letsencrypt.org/directory"
# Use staging for testing:
# directory_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
# ACME account key: stored as encrypted systemd credential
account_key_credential = "/etc/credstore.encrypted/acme-account-key"
# Or as a plain file:
# account_key_path = "/etc/certforge/account.key"
contact = ["mailto:admin@example.com"]
# Default DNS server for RFC 2136 updates
[dns.defaults]
server = "ns1.example.com"
tsig_key_credential = "/etc/credstore.encrypted/dns-tsig-key"
# tsig_key_path = "/etc/certforge/tsig.key"
tsig_key_name = "certforge-update."
tsig_algorithm = "hmac-sha256"
port = 53
protocol = "tcp"
# Per-zone DNS overrides (optional)
# [dns.zones."example.org"]
# server = "ns2.example.org"
# tsig_key_credential = "/etc/credstore.encrypted/dns-tsig-org"
# Mail server certificate
[[certificate]]
name = "mail"
domains = ["mail.example.com"]
key_type = "ecdsa-p256"
key_credential = "/etc/credstore.encrypted/mail-tls-key"
cert_path = "/etc/certforge/certs/mail.pem"
renew_before_days = 30
rotate_key = false
# DANE TLSA for SMTP (port 25)
[[certificate.dane]]
usage = "ee"
selector = "spki"
matching = "sha256"
names = ["_25._tcp.mail.example.com"]
ttl = 300
pre_publish = true
# DANE TLSA for IMAP (port 993)
[[certificate.dane]]
usage = "ee"
selector = "spki"
matching = "sha256"
names = ["_993._tcp.mail.example.com"]
ttl = 300
[[certificate.hook]]
type = "systemd-reload"
unit = "postfix.service"
[[certificate.hook]]
type = "systemd-restart"
unit = "dovecot.service"
# Web server certificate
[[certificate]]
name = "web"
domains = ["example.com", "www.example.com"]
key_type = "ecdsa-p256"
key_credential = "/etc/credstore.encrypted/web-tls-key"
cert_path = "/etc/certforge/certs/web.pem"
renew_before_days = 30
[[certificate.dane]]
usage = "ee"
selector = "spki"
matching = "sha256"
names = ["_443._tcp.example.com", "_443._tcp.www.example.com"]
ttl = 3600
[[certificate.hook]]
type = "systemd-reload"
unit = "nginx.service"