mirror of
https://github.com/rkivalin/certforge.git
synced 2026-10-13 00:00:42 +00:00
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>
301 lines
9.5 KiB
Markdown
301 lines
9.5 KiB
Markdown
# certforge
|
|
|
|
ACME certificate manager with DANE and systemd integration.
|
|
|
|
Certforge automates TLS certificate issuance and renewal using the ACME protocol (RFC 8555). It supports DNS-01 challenges via RFC 2136, HTTP-01 and TLS-ALPN-01 challenges for IP address certificates, publishes DANE TLSA records, and integrates tightly with systemd for secret storage, service management, and scheduled operation.
|
|
|
|
## Features
|
|
|
|
- **Multiple challenge types** -- DNS-01 (RFC 2136 with TSIG), HTTP-01 (standalone server or webroot), TLS-ALPN-01 (standalone TLS server). Per-domain solver assignment.
|
|
- **IP address certificates** -- issue certificates for IPv4 and IPv6 addresses using HTTP-01 or TLS-ALPN-01.
|
|
- **DANE TLSA** -- automatically publishes TLSA records (RFC 6698/7671/7672) after certificate issuance. Supports DANE-EE with SPKI selector for zero-downtime renewals.
|
|
- **Key rotation with pre-publication** -- when rotating keys, new TLSA records are published alongside the old ones and certforge waits for the old TTL to expire before completing the renewal.
|
|
- **Multiple servers, one certificate** -- a single certificate's DANE block can list multiple TLSA names (e.g., `_25._tcp.mx1.example.com`, `_25._tcp.mx2.example.com`) for services sharing a key.
|
|
- **systemd credentials** -- private keys and TSIG secrets can be stored as encrypted systemd credentials (`systemd-creds`). Services access the same files via `LoadCredentialEncrypted=`.
|
|
- **systemd hooks** -- reload or restart systemd units after renewal via D-Bus. Also supports arbitrary command hooks.
|
|
- **Timer-driven** -- ships with a systemd service and timer for daily renewal checks with randomized delay.
|
|
- **TOML configuration** -- declarative config with named DNS clients, named solvers, and per-certificate DANE and hook blocks.
|
|
|
|
## Installation
|
|
|
|
### From source
|
|
|
|
```
|
|
cargo install --path .
|
|
```
|
|
|
|
### Arch Linux
|
|
|
|
```
|
|
makepkg -si
|
|
```
|
|
|
|
### Debian/Ubuntu
|
|
|
|
Download the `.deb` from the [releases page](https://github.com/rkivalin/certforge/releases) or build from source:
|
|
|
|
```
|
|
dpkg-buildpackage -us -uc
|
|
```
|
|
|
|
## Quick start
|
|
|
|
1. Edit `/etc/certforge/config.toml` (see [examples/config.toml](examples/config.toml) for reference):
|
|
|
|
```toml
|
|
[acme]
|
|
account_key_path = "/etc/certforge/account.key"
|
|
contact = ["mailto:admin@example.com"]
|
|
|
|
[dns.main]
|
|
server = "ns1.example.com"
|
|
zone = "example.com"
|
|
tsig_key_path = "/etc/certforge/tsig.key"
|
|
tsig_key_name = "certforge."
|
|
|
|
[solver.dns]
|
|
type = "dns-01"
|
|
dns = "main"
|
|
|
|
[[certificate]]
|
|
name = "mail"
|
|
domains = ["mail.example.com"]
|
|
solver = "dns"
|
|
key_path = "/etc/certforge/keys/mail.key"
|
|
cert_path = "/etc/certforge/certs/mail.pem"
|
|
|
|
[[certificate.dane]]
|
|
dns = "main"
|
|
names = ["_25._tcp.mail.example.com"]
|
|
|
|
[[certificate.hook]]
|
|
type = "systemd-reload"
|
|
unit = "postfix.service"
|
|
```
|
|
|
|
2. Validate:
|
|
|
|
```
|
|
certforge config-check
|
|
```
|
|
|
|
3. Issue certificates:
|
|
|
|
```
|
|
certforge renew
|
|
```
|
|
|
|
4. Enable the timer for automatic renewal:
|
|
|
|
```
|
|
systemctl enable --now certforge.timer
|
|
```
|
|
|
|
## Usage
|
|
|
|
```
|
|
certforge [OPTIONS] <COMMAND>
|
|
|
|
Commands:
|
|
renew Check all certificates and renew those expiring soon
|
|
status Show status of all configured certificates
|
|
issue Force-issue a specific certificate
|
|
dane-publish Force-(re)publish DANE TLSA records
|
|
dane-check Query and verify published TLSA records against current certs
|
|
config-check Validate configuration file
|
|
account Manage ACME account
|
|
|
|
Options:
|
|
-c, --config <PATH> Config file [default: /etc/certforge/config.toml]
|
|
-v, --verbose... Increase verbosity (-v, -vv, -vvv)
|
|
-n, --dry-run Show what would be done without making changes
|
|
--state-dir <PATH> State directory [default: /var/lib/certforge]
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### DNS clients
|
|
|
|
Named DNS client connections are defined in `[dns.*]` blocks and referenced by solvers and DANE blocks:
|
|
|
|
```toml
|
|
[dns.main]
|
|
server = "ns1.example.com"
|
|
zone = "example.com"
|
|
tsig_key_path = "/etc/certforge/tsig.key"
|
|
tsig_key_name = "certforge."
|
|
tsig_algorithm = "hmac-sha256" # hmac-sha256, hmac-sha384, or hmac-sha512
|
|
```
|
|
|
|
A single DNS client can serve multiple zones. Use `zones` instead of `zone` -- certforge picks the longest matching suffix:
|
|
|
|
```toml
|
|
[dns.main]
|
|
server = "ns1.example.com"
|
|
zones = ["example.com", "example.org"]
|
|
tsig_key_path = "/etc/certforge/tsig.key"
|
|
tsig_key_name = "certforge."
|
|
```
|
|
|
|
### Challenge solvers
|
|
|
|
Solvers are defined in `[solver.*]` blocks. Each certificate references a solver by name.
|
|
|
|
**DNS-01** -- publishes TXT records via RFC 2136 dynamic DNS updates:
|
|
```toml
|
|
[solver.dns]
|
|
type = "dns-01"
|
|
dns = "main" # references a [dns.*] block
|
|
propagation_delay = 5 # seconds to wait for DNS propagation (default: 5)
|
|
```
|
|
|
|
**HTTP-01** -- serves challenge tokens over HTTP:
|
|
```toml
|
|
# Standalone mode: certforge runs a temporary HTTP server
|
|
[solver.http]
|
|
type = "http-01"
|
|
listen = "[::]:80"
|
|
# Multiple addresses: try all, succeed if at least one binds
|
|
# listen = ["[::]:80", "[::]:8080"]
|
|
|
|
# Webroot mode: writes tokens for an existing web server
|
|
[solver.webroot]
|
|
type = "http-01"
|
|
webroot = "/var/www/acme"
|
|
```
|
|
|
|
**TLS-ALPN-01** -- serves challenge certificates via TLS with ALPN `acme-tls/1`:
|
|
```toml
|
|
[solver.tls]
|
|
type = "tls-alpn-01"
|
|
listen = "[::]:443"
|
|
# listen = ["[::]:443", "[::]:8443"]
|
|
```
|
|
|
|
### Certificates
|
|
|
|
Certificates reference solvers by name. Use `solver` for a single solver or `solvers` for per-domain assignment:
|
|
|
|
```toml
|
|
# All domains use the same solver
|
|
[[certificate]]
|
|
name = "web"
|
|
domains = ["example.com", "www.example.com"]
|
|
solver = "dns"
|
|
|
|
# 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:
|
|
|
|
```toml
|
|
default_solver = "dns"
|
|
```
|
|
|
|
### File permissions
|
|
|
|
Key and certificate files support custom permissions. Mode is specified as an octal string. Owner and group are resolved by name:
|
|
|
|
```toml
|
|
[[certificate]]
|
|
name = "mail"
|
|
key_path = "/etc/certforge/keys/mail.key"
|
|
key_mode = "0640"
|
|
key_owner = "root"
|
|
key_group = "postfix"
|
|
cert_path = "/etc/certforge/certs/mail.pem"
|
|
cert_mode = "0644"
|
|
cert_owner = "root"
|
|
cert_group = "postfix"
|
|
```
|
|
|
|
Defaults: key mode `0600`, cert mode `0644`. Owner and group are not changed unless explicitly set. Permissions are applied on every `certforge renew` run, even when no certificates need renewal.
|
|
|
|
### 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:
|
|
|
|
- **`*_credential`** -- full path to a `systemd-creds` encrypted file. Certforge decrypts at runtime via `systemd-creds decrypt`. Services access the same file via `LoadCredentialEncrypted=` in their unit files.
|
|
- **`*_path`** -- plain file on disk. Simpler setup, suitable for development or environments without TPM2.
|
|
|
|
### DANE
|
|
|
|
Each certificate can have multiple `[[certificate.dane]]` blocks. A DANE block references a `[dns.*]` client for TLSA record publication:
|
|
|
|
| Field | Default | Description |
|
|
|-------|---------|-------------|
|
|
| `dns` | required | Name of a `[dns.*]` client for RFC 2136 updates |
|
|
| `usage` | `ee` | TLSA usage: `ee` (3), `ta` (2), `pkix-ee` (1), `pkix-ta` (0) |
|
|
| `selector` | `spki` | `spki` (1) or `full` (0) |
|
|
| `matching` | `sha256` | `sha256` (1), `sha512` (2), or `full` (0) |
|
|
| `names` | required | List of TLSA DNS names (e.g., `_25._tcp.mail.example.com`) |
|
|
| `ttl` | `300` | TTL for TLSA records |
|
|
| `pre_publish` | `false` | Pre-publish new TLSA before key rotation |
|
|
|
|
With `usage = "ee"` and `selector = "spki"` (the recommended DANE-EE configuration), the TLSA record depends only on the public key. This means the record survives certificate renewal as long as the key is not rotated.
|
|
|
|
### Post-renewal hooks
|
|
|
|
**Named hooks** are defined at the top level and referenced by certificates. When multiple certificates reference the same hook, it runs once after all renewals complete, in definition order:
|
|
|
|
```toml
|
|
[[hook]]
|
|
name = "reload-postfix"
|
|
type = "systemd-reload"
|
|
unit = "postfix.service"
|
|
|
|
[[certificate]]
|
|
name = "mail"
|
|
hooks = ["reload-postfix"]
|
|
|
|
[[certificate]]
|
|
name = "imap"
|
|
hooks = ["reload-postfix"] # same hook, runs once
|
|
```
|
|
|
|
**Inline hooks** run per-certificate immediately after each renewal:
|
|
|
|
```toml
|
|
[[certificate.hook]]
|
|
type = "systemd-reload" # or "systemd-restart"
|
|
unit = "postfix.service"
|
|
|
|
[[certificate.hook]]
|
|
type = "command"
|
|
command = ["/usr/local/bin/deploy-cert.sh", "--name", "mail"]
|
|
```
|
|
|
|
Hook failures are logged but do not prevent subsequent hooks from running.
|
|
|
|
## systemd integration
|
|
|
|
The package ships with:
|
|
|
|
- `certforge.service` -- oneshot service that runs `certforge renew`
|
|
- `certforge.timer` -- daily timer with randomized delay
|
|
|
|
```
|
|
systemctl enable --now certforge.timer
|
|
```
|
|
|
|
For encrypted credentials, add `LoadCredentialEncrypted=` directives to service units that need access to TLS keys. See the [Credential storage](#credential-storage) section for details.
|
|
|
|
## Known limitations
|
|
|
|
- **TSIG HMAC-SHA-512**: BIND 9.18+ truncates HMAC-SHA-512 MACs by default, and the underlying DNS library (hickory-dns) does not support verifying truncated HMACs. Use `hmac-sha256` instead.
|
|
- RSA key types not yet supported (ECDSA only).
|
|
|
|
## License
|
|
|
|
MIT
|