ACME clients and automation
How to get certificates with lego and the DNS-01 challenge
lego v5 issues a Let's Encrypt certificate for example.com and *.example.com with one lego run --dns <provider> command, which creates the DNS-01 TXT records through your DNS provider's API. A deploy hook installs the certificate and reloads the web server, and a daily systemd timer handles renewal.
The steps apply to lego v5 (released May 2026; check the GitHub releases for the current version) on Linux, with Cloudflare DNS as the example.
Prerequisites
DNS-01 needs API access to the DNS zone and no inbound ports.
- A Linux server with systemd and sudo access.
- The domain's DNS hosted at a provider supported by lego (
lego dnshelplists them). - A Cloudflare API token with Zone → DNS → Edit and Zone → Zone → Read on the zone, or the equivalent credential for your provider.
- Outbound HTTPS to Let's Encrypt and the DNS provider API.
Step 1: Install lego
Download the archive for your platform from the lego GitHub releases page, then install the binary:
tar -xzf lego_*_linux_amd64.tar.gz lego
sudo install -m 755 lego /usr/local/bin/lego
lego --version
Alternatives listed in the docs: go install github.com/go-acme/lego/v5@latest (Go 1.25+), the goacme/lego Docker image, Homebrew and distribution packages. If you upgrade from v4, run lego migrate against your existing data directory before anything else.
Step 2: Store the DNS credentials
Keep the token out of shell history by putting it in an environment file readable only by root:
sudo install -d -m 700 /etc/lego
sudo tee /etc/lego/env > /dev/null <<'EOF'
CF_DNS_API_TOKEN=your-cloudflare-api-token
LEGO_EMAIL=admin@example.com
EOF
sudo chmod 600 /etc/lego/env
CLOUDFLARE_DNS_API_TOKEN is an accepted alias. Other providers use their own variables, listed on each provider page of the lego docs.
Step 3: Issue the certificate
One lego run command issues the certificate for both names:
sudo bash -c 'set -a; . /etc/lego/env; set +a; \
lego run --accept-tos --path /etc/lego \
--dns cloudflare -d example.com -d "*.example.com"'
lego creates the _acme-challenge TXT records, waits for propagation, validates and cleans up. Add --profile tlsserver to request Let's Encrypt's 45-day profile (see Let's Encrypt certificate profiles), and --server https://acme-staging-v02.api.letsencrypt.org/directory while testing.
Step 4: Add a deploy hook
--deploy-hook runs after a certificate is created or renewed. lego passes the file locations in LEGO_HOOK_CERT_PATH and LEGO_HOOK_CERT_KEY_PATH:
sudo tee /etc/lego/deploy.sh > /dev/null <<'EOF'
#!/bin/sh
set -e
install -d -m 700 /etc/nginx/ssl
install -m 644 "$LEGO_HOOK_CERT_PATH" /etc/nginx/ssl/example.com.crt
install -m 600 "$LEGO_HOOK_CERT_KEY_PATH" /etc/nginx/ssl/example.com.key
systemctl reload nginx
EOF
sudo chmod 755 /etc/lego/deploy.sh
Point Nginx at /etc/nginx/ssl/example.com.crt (lego bundles the issuer certificate into the .crt by default, so it is the full chain) and /etc/nginx/ssl/example.com.key. Run lego certificates list --path /etc/lego and copy the files once by hand for the first deployment.
Verify
example.com and www.example.com should both present the Let's Encrypt certificate that covers example.com and *.example.com. Check them with the SSL checker, or from any terminal:
curl ssl.sh/example.com
curl ssl.sh/www.example.com
On the server:
sudo lego certificates list --path /etc/lego
openssl x509 -in /etc/nginx/ssl/example.com.crt -noout -ext subjectAltName -enddate
Automatic renewal
Run lego run daily from a systemd timer; lego has no daemon and renews only when a certificate is due. With --renew-days 0 (the default) lego derives the renewal point from the certificate lifetime, and it follows Let's Encrypt's ARI window unless you pass --ari-disable. Create a service and timer:
# /etc/systemd/system/lego.service
[Unit]
Description=lego certificate renewal
[Service]
Type=oneshot
EnvironmentFile=/etc/lego/env
ExecStart=/usr/local/bin/lego run --accept-tos --path /etc/lego --dns cloudflare -d example.com -d *.example.com --deploy-hook /etc/lego/deploy.sh
# /etc/systemd/system/lego.timer
[Unit]
Description=Daily lego renewal check
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload && sudo systemctl enable --now lego.timer
sudo systemctl start lego.service && sudo journalctl -u lego -n 20 --no-pager
lego sleeps for a random interval before renewing unless you pass --no-random-sleep. Daily checks cover Let's Encrypt's move to 64-day certificates on February 10, 2027 and 45-day certificates on February 16, 2028. Add the domain to ssl.sh monitoring (in development) as a safety net.
Troubleshooting
- unknown command "renew" or flag provided but not defined: you are running v4 syntax against lego v5; see the FAQ and the migration guide.
- Cloudflare: failed to find zone or authentication error: the token lacks Zone Read or DNS Edit on this zone.
- propagation: time limit exceeded: increase the propagation wait (
--dns.propagation.wait, seelego run --help) or check that the zone is authoritative at the provider you configured. - CAA record prevents issuance: see CAA record prevents issuance.
- Rate limited: too many test orders against production; use the staging server and read Let's Encrypt rate limits.
- Web server still serves the old certificate: the deploy hook did not run or failed; check
journalctl -u lego.
FAQ
I followed an older guide with lego renew. Why does it fail?
lego v5 removed the renew command; lego run renews when a certificate is due. Flags moved after the subcommand (lego run --dns foo, not lego --dns foo run), and several were renamed. Run lego migrate once on an existing v4 data directory, then update your scripts.
Where does lego store the certificate?
Under the --path directory (default .lego in the current working directory), in the certificates/ subfolder. A wildcard-first certificate is stored as _.example.com.crt. Run lego certificates list --path /etc/lego to see the exact files rather than guessing.
Does Traefik use lego?
Yes. Traefik's ACME support is built on the lego library, which is why its dnsChallenge.provider names and environment variables match lego's DNS provider pages. See Traefik with Let's Encrypt.