Trusting the CA
With the local_ca signer, acme-proxy mints
certificates from a CA it generated itself. Those certificates are perfectly
valid, but nothing on your network trusts the CA that signed them yet — so
browsers, curl, and every TLS library will reject them until you install the
root.
This page covers distributing that root. It does not apply when you use the
relay backend to relay to a public CA, whose
roots are already trusted everywhere.
Getting the root certificate
Each profile serves its own CA material unauthenticated at
{base_url}/profile/<name>/ca.pem, as application/x-pem-file:
curl -o internal-root.pem https://acme.internal/profile/default/ca.pem
These are exactly the bytes appended to every certificate that profile issues, so a client that fetches them here and one that reads the tail of its own chain end up trusting the same anchor.
Two things worth knowing about the route. It is not advertised in the ACME
directory — it is CA infrastructure rather than an ACME resource, so a client
will not find it on its own and you distribute the URL yourself. And it is
served inside the profile router, which means it sits behind that profile’s
filter policy: if you restrict the endpoint by address, the hosts that most
need the root — the ones that do not have it yet — may be exactly the ones
refused. Add a path check allowing /ca.pem if so.
The same file is on the server’s disk at signer.local_ca.cert_path, ca.pem
by default in the working directory, which is the way to get it when the server
is not reachable or is not running:
scp acme-host:/var/lib/acme-proxy/ca.pem ./internal-root.pem
Inspect it before distributing it:
openssl x509 -in internal-root.pem -noout -subject -issuer -dates -ext basicConstraints
A freshly generated root is self-signed (subject equals issuer) and carries
CA:TRUE, pathlen:0.
If cert_path holds a bundle — an intermediate followed by a root, as in
the multi-tier
setup — then
the last certificate in the file is the root, and it is the only one your
clients need to trust. The intermediate is shipped with every issued certificate
and does not need installing.
# Split a bundle into its constituent certificates.
csplit -z -f cert- -b '%02d.pem' ca_bundle.pem '/-----BEGIN CERTIFICATE-----/' '{*}'
Installing it
Debian / Ubuntu
The file must have a .crt extension, and must be PEM despite the name.
sudo cp internal-root.pem /usr/local/share/ca-certificates/acme-proxy-root.crt
sudo update-ca-certificates
RHEL / Fedora / CentOS
sudo cp internal-root.pem /etc/pki/ca-trust/source/anchors/acme-proxy-root.pem
sudo update-ca-trust extract
Alpine
sudo cp internal-root.pem /usr/local/share/ca-certificates/acme-proxy-root.crt
sudo update-ca-certificates
Verify
curl -v https://internal.example.com 2>&1 | grep -i 'SSL certificate verify'
# or, without a server:
openssl verify -CAfile internal-root.pem issued-cert.pem
Applications with their own trust store
Updating the system store is not enough for everything. These maintain their own:
| Runtime | How to add the root |
|---|---|
| Firefox | Its own store, always. Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import. Enterprise deployments can use the Certificates policy in policies.json. |
| Chrome / Edge | Uses the system store on Windows and macOS; on Linux it reads the NSS database — certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n acme-proxy-root -i internal-root.pem. |
| Java / JVM | keytool -importcert -trustcacerts -alias acme-proxy-root -file internal-root.pem -keystore "$JAVA_HOME/lib/security/cacerts". |
| Node.js | Ignores the system store by default. Set NODE_EXTRA_CA_CERTS=/path/to/internal-root.pem. |
Python requests | Uses certifi, not the system store. Set REQUESTS_CA_BUNDLE (or SSL_CERT_FILE for ssl/urllib). |
| Go | Uses the system store on Linux; no action needed after update-ca-certificates. |
| Containers | Each image has its own store. Mount the root in and run the distribution’s update command in your Dockerfile, or bake it into a base image. |
Distributing at scale
Installing a root by hand does not survive a fleet. In practice:
- Ansible / Puppet / Chef — ship the file and run the update command as a handler. This is the common approach for Linux estates.
- Active Directory Group Policy — Computer Configuration → Windows Settings → Security Settings → Public Key Policies → Trusted Root Certification Authorities.
- MDM (Jamf, Intune, …) — deploy as a certificate payload.
- Golden images — bake the root into your base image so new hosts trust it from first boot.
Whichever you use, deploy the root before you start issuing certificates from it, or the first clients to renew will break.
Revocation
If you revoke certificates, clients need to be able to see the CRL. It is served
unauthenticated at {base_url}/profile/<name>/crl as application/pkix-crl:
curl -o internal.crl https://acme.internal/profile/default/crl
openssl crl -in internal.crl -inform DER -noout -text
Like /ca.pem, the CRL is not advertised in the ACME directory and sits behind
the profile’s filter policy. Issued certificates carry a CRL distribution point
only when you set signer.local_ca.crl_distribution_points; leave it unset and
a client will not find the CRL automatically, so distribute the URL alongside
the root if your validation policy needs it. See
Revocation & CRL.
Planning ahead
The root’s validity is finite, and replacing it later means touching every host that trusts it. Two things make that easier:
- Use an intermediate. Keep an offline root and hand
acme-proxyonly an intermediate. The root you distribute then long outlives any single signing key, and a compromised proxy costs you an intermediate rather than your whole trust anchor. See Multi-Tier PKI. - Distribute early, rotate overlapping. Trust stores accept multiple roots, so push a replacement root well before it is needed and remove the old one only after nothing is signed by it.