ACME clients
What stays the same as Let’s Encrypt
If you have ever obtained a Let’s Encrypt certificate, you can already use the internal CA. The protocol is identical — ACME RFC 8555. The clients you already know work without modification.
Same:
- HTTP-01 and DNS-01 challenges.
- Account registration and key management.
- 90-day default certificate lifetime.
- Automatic renewal at 60 days.
- Wildcard certificates via DNS-01.
What is different
Two small differences, both one-time setup steps:
- The ACME directory URL points to the internal CA, not
https://acme-v02.api.letsencrypt.org/directory. - The root certificate of the internal CA must be trusted by the requesting client. This is normally arranged via configuration management (Ansible, Puppet, MDM) at host provisioning.
That is the entire difference. Everything else is the same Let’s Encrypt workflow you already know.
Pointing your client at the internal CA
The internal ACME endpoint is published at a stable internal URL
(typically https://acme.example.internal/acme/acme/directory,
but the exact path depends on your deployment). Below are
common-client recipes.
certbot
certbot --server https://acme.example.internal/acme/acme/directory \
--email ops@example.internal \
--no-eff-email \
--agree-tos \
certonly --standalone -d service.example.internalFor renewal, the --server flag is persisted in renewal/*.conf,
so the periodic certbot renew cron will pick it up
automatically.
acme.sh
acme.sh --register-account \
--server https://acme.example.internal/acme/acme/directory \
-m ops@example.internal
acme.sh --issue \
--server https://acme.example.internal/acme/acme/directory \
--standalone \
-d service.example.internallego
lego --server=https://acme.example.internal/acme/acme/directory \
--email=ops@example.internal \
--domains=service.example.internal \
--http runCaddy
In your Caddyfile:
{
acme_ca https://acme.example.internal/acme/acme/directory
email ops@example.internal
}
service.example.internal {
reverse_proxy localhost:8080
}Caddy will automatically obtain and renew the certificate.
Traefik
In your dynamic configuration:
certificatesResolvers:
internal:
acme:
caServer: "https://acme.example.internal/acme/acme/directory"
email: ops@example.internal
storage: /etc/traefik/acme.json
httpChallenge:
entryPoint: webReference certResolver: internal from your router definitions.
Installing the root certificate on clients
For the certificates the CA issues to be trusted without warnings, the CA’s root must be in the system trust store of every host that will see the certificates.
Linux (Debian/Devuan family)
sudo cp internal-ca-root.crt /usr/local/share/ca-certificates/internal-ca-root.crt
sudo update-ca-certificatesLinux (RHEL family)
sudo cp internal-ca-root.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extractOpenBSD / FreeBSD
# OpenBSD
sudo install -m 0644 internal-ca-root.crt /etc/ssl/internal-ca-root.crt
sudo cat /etc/ssl/cert.pem internal-ca-root.crt > /etc/ssl/cert.pem.new
sudo mv /etc/ssl/cert.pem.new /etc/ssl/cert.pemmacOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain internal-ca-root.crtWindows
certutil.exe -addstore -f "ROOT" internal-ca-root.crtFor browsers that maintain their own trust stores (Firefox), the root must additionally be imported via Settings → Privacy → Certificates → Authorities.
This installation is normally handled by configuration management. Doing it by hand is fine for a one-off but does not scale to a fleet.
Wildcard certificates
Wildcards are issued via the DNS-01 challenge — the client proves control of a domain by creating a TXT record. This works for internal-only domains because the internal CA can validate TXT records on any DNS server you control.
Example with acme.sh using the BIND nsupdate provider:
acme.sh --issue \
--server https://acme.example.internal/acme/acme/directory \
--dns dns_nsupdate \
-d '*.example.internal' \
-d 'example.internal'You will need the nsupdate provider to be configured against
your internal DNS — see the acme.sh documentation for the
specific environment variables.
Renewal
Renewal works the same way as with Let’s Encrypt: the client checks remaining validity and renews when below the threshold (60 days for a 90-day cert).
For most deployments, the existing renewal cron (certbot renew,
acme.sh --cron, Caddy’s automatic renewal) just works.
If you see renewal failures:
- Is the directory URL still reachable? A network change may have broken the path from the client to the CA.
- Has the root certificate expired or been rotated? Roots are typically valid for 10 years; intermediates for shorter periods. A rotation that did not propagate to all clients will manifest as renewal failures.
- Is the challenge type still appropriate? A service that used to be reachable for HTTP-01 may now sit behind a load balancer that breaks the challenge response.
Limits and policies
The internal CA enforces per-account policies — what domains each account may request certificates for, what maximum lifetime is allowed, what challenge types are accepted.
If a request that worked yesterday suddenly fails with a
policy denied error, the policy has changed (or your account
binding has). Contact your CA operator. The
Operations service is the escalation path
if your CA operator is us.