# lego DNS-01: wildcard certificates with the lego v5 CLI

> Use the lego v5 ACME client for Let's Encrypt wildcard certificates via DNS-01: install, lego run with a DNS provider, deploy hook, daily timer.

Source: https://ssl.sh/guides/lego-dns-01/
Last updated: 2026-11-10

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 dnshelp` lists 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:

```bash
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:

```bash
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:

```bash
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](https://ssl.sh/guides/lets-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`:

```bash
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](https://ssl.sh/tools/ssl-checker/), or from any terminal:

```bash
curl ssl.sh/example.com
curl ssl.sh/www.example.com
```

On the server:

```bash
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:

```ini
# /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
```

```ini
# /etc/systemd/system/lego.timer
[Unit]
Description=Daily lego renewal check

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

[Install]
WantedBy=timers.target
```

```bash
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](https://ssl.sh/monitor/) (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`, see `lego run --help`) or check that the zone is authoritative at the provider you configured.
- **CAA record prevents issuance**: see [CAA record prevents issuance](https://ssl.sh/errors/caa-record-prevents-issuance/).
- **Rate limited**: too many test orders against production; use the staging server and read [Let's Encrypt rate limits](https://ssl.sh/errors/lets-encrypt-rate-limit/).
- **Web server still serves the old certificate**: the deploy hook did not run or failed; check `journalctl -u lego`.
