certbot
certbot is the EFF's reference ACME client, Python-based, with the broadest community documentation and the most mature distro packaging. This page is the comprehensive reference; for a 5-minute onboarding tour see Getting Started → First Certificate.
certbot/certbot:latest Docker image)certbot's CLI has been stable since 1.x — the certonly / renew / revoke subcommands and their flags work identically across recent versions. If you are on an older 1.x or 2.x release, the syntax on this page still applies.
What changes vs lego / acme.sh
- Key type: certbot generates RSA 4096 by default. Always passes the
signature.min-rsa-bits: 3072policy without tweaking — no surprise like acme.sh's--keylength 2048rejection. - CSR EKU: certbot declares
serverAuthonly — noclientAuthsmuggling, no need to loosencsr.allowed-extra-ekuon the policy. - Trust store: certbot uses Python's own CA bundle (
certifi), not the OS trust store. You point it at the OS bundle via theREQUESTS_CA_BUNDLEenv var. Once set, the OS trust store becomes the single source of truth for both system commands and certbot. - Built-in scheduler: certbot installs a
certbot.timersystemd unit on most distros.systemctl enable --now certbot.timeris enough to wire renewal.
Trusting your internal CA
Certeasy's HTTPS certificate is signed by your internal ADCS root CA. ACME clients need to trust that CA, otherwise the TLS handshake fails before any certificate request can be made.
The recommended approach is to deploy your root CA to the OS trust store on all Linux servers — ideally via your configuration management tool (Ansible, Puppet, Chef…). This is good practice regardless of Certeasy: any internal service using TLS with an internal CA benefits from it.
# Debian / Ubuntu
sudo cp internal-root-ca.pem /usr/local/share/ca-certificates/internal-root-ca.crt
sudo update-ca-certificates
# → consolidated bundle at /etc/ssl/certs/ca-certificates.crt
# RHEL / CentOS / Rocky
sudo cp internal-root-ca.pem /etc/pki/ca-trust/source/anchors/internal-root-ca.pem
sudo update-ca-trust
# → consolidated bundle at /etc/pki/tls/certs/ca-bundle.crt
With Ansible, this becomes a one-liner across your fleet:
- name: Deploy internal root CA
copy:
src: internal-root-ca.pem
dest: /usr/local/share/ca-certificates/internal-root-ca.crt # adjust for RHEL
notify: update-ca-certificates
certbot does not use the OS trust store directly — it uses Python's own CA bundle (certifi). Once your CA is in the OS trust store, you can point certbot to the system bundle file with REQUESTS_CA_BUNDLE, so both stay in sync automatically:
# Debian / Ubuntu
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
# RHEL / CentOS / Rocky
export REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt
For automated renewal, set this in certbot's systemd service:
# /etc/systemd/system/certbot.service.d/override.conf
[Service]
Environment="REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt"
This way there is a single source of truth: the OS trust store. Update it, and certbot picks up the change automatically.
--no-verify-ssl (testing only)
--no-verify-ssl disables TLS certificate verification entirely. The client has no guarantee it is talking to your Certeasy instance — the connection could be intercepted. Acceptable for a quick local test, never for production or automated renewal.
HTTP-01 (standalone)
The simplest approach: certbot spins up a temporary HTTP server on port 80 to answer the challenge.
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
certbot certonly \
--standalone \
--preferred-challenges http \
--server https://acme.corp.internal/acme/directory \
-d app.corp.internal
Certbot opens port 80, Certeasy fetches http://app.corp.internal/.well-known/acme-challenge/<token>, and on success submits the CSR to ADCS. The signed certificate is written to /etc/letsencrypt/live/app.corp.internal/.
HTTP-01 (webroot)
If a web server is already running on port 80, use --webroot instead of --standalone:
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
certbot certonly \
--webroot -w /var/www/html \
--preferred-challenges http \
--server https://acme.corp.internal/acme/directory \
-d app.corp.internal
Certbot writes the challenge file under /var/www/html/.well-known/acme-challenge/. Your web server must serve that path over HTTP.
DNS-01 (for wildcards)
HTTP-01 and TLS-ALPN-01 cannot validate wildcard names (*.corp.internal) — RFC 8555 §8.4 limits them to DNS-01.
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
certbot certonly \
--manual \
--preferred-challenges dns \
--server https://acme.corp.internal/acme/directory \
-d "*.corp.internal"
certbot prompts you interactively to add the _acme-challenge.corp.internal TXT record. For automation, install a DNS plugin matching your provider (certbot-dns-route53, certbot-dns-rfc2136, …) and use --dns-<plugin> flags instead of --manual.
Renewal
Once the certificate is issued, certbot can renew it automatically via the certbot.timer systemd unit:
# Test renewal
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
certbot renew --dry-run
# Enable automatic renewal
systemctl enable --now certbot.timer
Renewal config is stored under /etc/letsencrypt/renewal/<domain>.conf and inherits the flags used at the initial certonly call.
Revocation
certbot revoke --cert-name app.corp.internal \
--server https://acme.corp.internal/acme/directory
--cert-name is the directory name under /etc/letsencrypt/live/. Add --no-delete-after-revoke if you want to keep the files on disk after revocation.
With Caddy (automatic, no certbot)
If you run Caddy as your reverse proxy, it can handle ACME directly — no certbot involvement, no renewal scripts to write:
{
acme_ca https://acme.corp.internal/acme/directory
acme_ca_root /path/to/your/internal-ca.pem
}
app.corp.internal {
reverse_proxy localhost:8080
}
Caddy uses HTTP-01 by default for non-wildcard names. For wildcards, configure a DNS module from caddy-dns.