Hardening Checklist
Run through this before an acme-proxy deployment issues a certificate anything
depends on. Every item links to the page that explains it; nothing here is
explained only here.
The defaults are already the safe end of most of these. The items that need a decision from you are marked decide.
Before it serves anything
-
challenge.bypassisfalse. It is the default. With it on,[filter]is the only thing between a client and a certificate for any name it names. → Challenge Validation - At least one gate is configured — filters, EAB, or both. Validation alone proves the client controls the name; it does not say the client is allowed to have a certificate from you. → Filters, EAB
- decide —
server.bind_addressis the interface you meant. The default[::]:3000is every interface. → Configuration Reference -
server.base_urlmatches how clients actually reach the server, including the scheme. It is checked against the JWSurlof every signed request, so a mismatch fails every request rather than degrading. → Configuration Reference - ACME is served over HTTPS — either
server.tls.enabled = trueor a reverse proxy in front. RFC 8555 §6.1 expects it. → TLS Termination -
acme-proxy filter explainagrees with what you meant, for both a client that should be served and one that should not. A policy is easier to get subtly wrong than a list. → CLI -
/crland/ca.pemare still reachable if any check is address-based. Both are served by the profile router, so an allowlist covers them too — and neither the relying parties that fetch the CRL nor the hosts that have yet to install the root are the ACME clients you allowlisted. → Path Check
An or is a hole you opened deliberately
A check that cannot reach its authority answers “unknown” rather than “no”, and
pass or unknown is pass. That is the point — it is what keeps an inventory
outage from locking every client out — but it means an or weakens the
fail-closed property to whatever its other side says.
when = "mgmt-net or inventory"
reads as “the inventory decides, unless the address is already trusted”. If
mgmt-net is wide, the inventory is decorative for everything inside it. That
may be exactly what you want; what you must not do is write it believing both
checks apply.
The rule of thumb: an or over an address check is a bypass for that
address range, so keep the range as small as the outage you are insuring
against. and has no such property — fail and unknown is fail, so a
conjunction never becomes more permissive because something broke.
The CA key
- decide — the issuing key is an intermediate, not a root. An offline root means a compromise is recoverable by re-issuing the intermediate rather than re-trusting every endpoint. → Local CA
- decide — the key lives in a PKCS#11 token if the deployment justifies it. The key then cannot be copied, only used. → Hardware Keys
-
ca.keyis0600and owned by the service user.acme-proxycreates it that way; a key restored from a backup may not be. - The CRL is reachable by everything that validates your certificates, and the database is backed up — the revocations recorded there are the authoritative record, not the CRL file. → Revocation & CRL
Behind a proxy
-
filter.trusted_proxiesnames the hops you trust, or the forwarded header is ignored entirely. Settingfilter.forwarded_headeralone does not make it trusted. → Allowed IP - The proxy does not forward
/healthif you do not want it public — it is mounted outside the filter chain on purpose. → Monitoring - With
signer.relay.challenge_strategy = "http01", port 80 of every name being issued forwards or redirects/.well-known/acme-challenge/here. Nothing in the process can do this for you. → Relay
The web admin
Skip this section entirely if admin.enabled is false, which is the default.
-
admin.bind_addressis loopback, oradmin.tls.enabledistrue— startup refuses the other combination. → Web Admin - decide — reach it over an SSH tunnel or a VPN rather than exposing the socket. It has no filter chain and no admission control. → Deployment
-
admin.base_urlis the origin operators actually type. It is load-bearing four ways — the CSRF origin check, the generated certificate’s host, and the label an authenticator app shows. - Every operator has a second factor, and
admin.require_mfa = trueso the next one does too. It does not retroactively end sessions that predate it;admin session revoke --alldoes. → Users & Sessions - Recovery codes are stored somewhere that is not the panel. Without
them, a lost authenticator needs
admin user totp reseton the host. → Users & Sessions - No
--passwordanywhere in your provisioning. There is no such flag; the password arrives on stdin or via--password-file. → Users & Sessions
Secrets
- Tokens and TSIG keys come from the environment, not the file where the
option exists —
ipam.netbox.token,signer.relay.dns01.rfc2136.tsig_key_secret. -
[signer.relay.eab]is emptied after the first registration. It is a bootstrap credential that authorizes exactly onenewAccount; the server warns on every startup for as long as it stays set. → Relay -
insecure_skip_verifyis unset. It warns on every startup by design, so it stays visible for exactly as long as it is needed. → NetBox - The database file is
0600. It holds EAB secrets and TOTP secrets in a form the server reads back, so anyone who can read the file can too. → Database Schema
Ongoing
- decide — a rotation interval for each long-lived secret. Nothing in the server expires these on a timer; the page recommends one per secret and names the events that force a rotation early. → Secret Rotation
- Backups copy the WAL.
sqlite.dbalone is missing every recent write; use.backup, or take all three files. → Database Schema - decide —
audit.retention_days.0, the default, keeps everything for ever, which is the right default for a trail whose value is that it is complete. → Audit Trail - Something watches the logs for refusals —
certificate_issue_failedandcertificate_revoke_failedrows, and a run of unknown-certificate revocation attempts, which is somebody enumerating serials. → Monitoring - Startup warnings are read, not filtered out. Three of them repeat on
every start precisely so they cannot become background noise:
challenge_validation_bypassed,ipam_netbox_tls_verification_disabled,signer_relay_eab_secret_in_config. → Monitoring