Aller au contenu principal

lego

lego is a single static Go binary with no runtime dependency — ideal for container images, CI runners, and minimal Linux installs. This page covers the lego-specific bits; for the general onboarding flow and trust-store setup see First Certificate.

Documented for lego 5.x (tested with 5.0.2)

lego 5.0 introduced a CLI breaking change: every flag (--server, --email, --domains, --http, --dns, etc.) is now a subcommand flag, not a global flag. The subcommand (run, renew, revoke) must come first. The legacy renew and top-level revoke are gone — use run --renew-force and certificates revoke. If you are still on lego 4.x, the global-flag-first syntax of the old documentation applies; consider upgrading.

What changes vs certbot

  • Key type: lego generates ECDSA P-256 keys by default for both the ACME account and the certificate. This sidesteps the signature.min-rsa-bits policy entirely. To force RSA, pass --key-type rsa3072 (or rsa4096). rsa2048 will be refused under the default min-rsa-bits: 3072 policy.
  • CSR EKU: lego declares serverAuth only in its CSR — no clientAuth smuggling, no need to loosen csr.allowed-extra-eku on the policy.
  • Trust store: lego reads a single PEM file pointed at by the env var LEGO_CA_CERTIFICATES. Point it at your OS bundle to keep one source of truth:
# Debian / Ubuntu
export LEGO_CA_CERTIFICATES=/etc/ssl/certs/ca-certificates.crt
# RHEL / CentOS / Rocky
export LEGO_CA_CERTIFICATES=/etc/pki/tls/certs/ca-bundle.crt
  • No built-in scheduler: unlike certbot's certbot.timer or acme.sh's --install-cronjob, lego expects you to wire renewal yourself via a systemd timer or cron job.
Command-line layout

The run, renew, and revoke subcommands come first, then the flags that configure them (--server, --email, --domains, --http, --dns, ...). Only logging flags (--log.level, --log.format) and --config are global and may appear before the subcommand.

HTTP-01 (standalone)

export LEGO_CA_CERTIFICATES=/etc/ssl/certs/ca-certificates.crt

lego run \
--server https://acme.corp.internal/acme/directory \
--email ops@corp.internal \
--accept-tos \
--http \
--domains app.corp.internal \
--path /etc/lego

Cert + chain + key land under /etc/lego/certificates/:

  • app.corp.internal.crt — the full chain (leaf first, then any intermediates).
  • app.corp.internal.key — the private key.
  • app.corp.internal.issuer.crt — issuer cert only.

lego opens port 80 inside the process to answer the HTTP-01 challenge, then exits. Port 80 must therefore be free at the moment of the call. On systems where another service holds port 80 permanently, see HTTP-01 webroot below.

HTTP-01 (webroot)

When a web server is already serving on port 80, use the webroot solver instead. lego writes the challenge token to a directory of your choice; the web server only has to serve /.well-known/acme-challenge/ from there.

lego run \
--server https://acme.corp.internal/acme/directory \
--email ops@corp.internal \
--accept-tos \
--http.webroot /var/www/html \
--domains app.corp.internal \
--path /etc/lego

TLS-ALPN-01

lego run \
--server https://acme.corp.internal/acme/directory \
--email ops@corp.internal \
--accept-tos \
--tls \
--domains app.corp.internal \
--path /etc/lego

lego binds port 443 with the ACME-specific ALPN protocol; Certeasy probes the IP at port 443 with ALPN acme-tls/1 to verify ownership. Useful when port 80 is unavailable but port 443 is free.

DNS-01 (for wildcards)

lego ships built-in plugins for ~80 DNS providers — no extra package needed. Pick the one matching your DNS, configure it via env vars, and pass --dns <provider>.

Point the propagation check at your internal resolver

By default, lego waits until the just-installed _acme-challenge.<domain> TXT record is visible through public resolvers (Cloudflare 1.1.1.1, Google 8.8.8.8) before notifying Certeasy. On an internal-only deployment that check will never succeed — the names do not exist publicly — and worse, the internal domain name is leaked in cleartext to those resolvers.

Use --dns.resolvers=<host:port> to point the propagation probe at your authoritative internal resolver instead. This keeps the sanity check (detects a misconfigured plugin) without leaking anything outside:

lego run \
--dns rfc2136 \
--dns.resolvers '10.0.0.53:53' \
...

If the propagation check is not desired at all (synchronous plugin, very short TTLs, etc.) pass --dns.propagation.disable-ans (authoritative nameservers) and/or --dns.propagation.disable-rns (recursive resolvers) to skip it entirely. --dns.propagation.wait <duration> replaces the check with a fixed sleep when neither flag fits.

export LEGO_CA_CERTIFICATES=/etc/ssl/certs/ca-certificates.crt
# Example: RFC 2136 / nsupdate against an internal Bind server
export RFC2136_NAMESERVER='10.0.0.53'
export RFC2136_TSIG_KEY='acme.'
export RFC2136_TSIG_SECRET='<base64-secret>'
export RFC2136_TSIG_ALGORITHM='hmac-sha256.'

lego run \
--server https://acme.corp.internal/acme/directory \
--email ops@corp.internal \
--accept-tos \
--dns rfc2136 \
--dns.resolvers '10.0.0.53:53' \
--domains '*.corp.internal' \
--path /etc/lego

For internal infrastructures without a public DNS provider, rfc2136 against your own Bind/PowerDNS is usually the simplest path. The full list of providers is in lego's DNS providers documentation.

Renewal

lego has no built-in scheduler. lego run itself issues or renews depending on whether the cert is already on disk — call it on a schedule (systemd timer, cron) and pass --renew-days N to actually do the renew only when the cert is within N days of expiration. Add --renew-force if you want to force a renew regardless:

lego run \
--server https://acme.corp.internal/acme/directory \
--email ops@corp.internal \
--accept-tos \
--http \
--domains app.corp.internal \
--path /etc/lego \
--renew-days 30

Minimal systemd setup (/etc/systemd/system/lego-renew.service + lego-renew.timer):

# lego-renew.service
[Unit]
Description=Renew certificates via lego

[Service]
Type=oneshot
Environment=LEGO_CA_CERTIFICATES=/etc/ssl/certs/ca-certificates.crt
ExecStart=/usr/local/bin/lego run \
--server https://acme.corp.internal/acme/directory \
--email ops@corp.internal \
--accept-tos \
--http \
--domains app.corp.internal \
--path /etc/lego \
--renew-days 30

# lego-renew.timer
[Unit]
Description=Daily lego renewal check

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=4h

[Install]
WantedBy=timers.target

Enable with systemctl enable --now lego-renew.timer.

Revocation

LEGO_PATH=/etc/lego lego certificates revoke \
--server https://acme.corp.internal/acme/directory \
--email ops@corp.internal \
--cert.name app.corp.internal

--cert.name is the certificate's ID/name on disk — by default this is the first domain you passed at issuance. certificates revoke does not accept --path or --domains; pass the storage location via the LEGO_PATH environment variable instead. Revoke uses the account key — no need to present the certificate key separately.