# acme.sh wildcard certificate with DNS API (DNS-01)

> Issue a Let's Encrypt wildcard certificate with acme.sh and a DNS API: install, set the default CA, issue via DNS-01, install with a reload command.

Source: https://ssl.sh/guides/acme-sh-dns-api-wildcard/
Last updated: 2026-11-10

acme.sh gets a wildcard certificate by answering the DNS-01 challenge through your DNS provider's API: it creates the `_acme-challenge` TXT record, waits for validation and deletes it. One command issues `example.com` and `*.example.com` together, and the cron job renews and reloads the web server.

The commands use Cloudflare DNS; other providers differ only in variable names. They apply to acme.sh 3.1.4 or later on Linux, macOS or BSD.

## Prerequisites

DNS-01 needs no open ports, only API access to the DNS zone.

- A Unix-like server with `curl` or `wget`, `openssl`, `cron` and root (or a user that can write the certificate directory and reload the web server).
- The domain's authoritative DNS hosted at a provider supported by acme.sh (list on the dnsapi wiki page).
- An API credential limited to editing DNS records of that zone. For Cloudflare, create an API token with **Zone → DNS → Edit** for `example.com`, and note the Zone ID from the zone overview page.
- Outbound HTTPS to the CA and the DNS provider's API.

## Step 1: Install acme.sh

```bash
curl https://get.acme.sh | sh -s email=admin@example.com
exec $SHELL -l
acme.sh --version
```

The installer copies acme.sh to `~/.acme.sh/`, adds an `acme.sh` alias and installs a cron job. The email is optional for Let's Encrypt.

## Step 2: Switch the default CA to Let's Encrypt

acme.sh issues from ZeroSSL unless you change the default CA:

```bash
acme.sh --set-default-ca --server letsencrypt
```

You can also pass `--server letsencrypt` per command.

## Step 3: Export the DNS API credentials

acme.sh reads DNS API credentials from environment variables named after the provider. For Cloudflare with a scoped token:

```bash
export CF_Token="your-cloudflare-api-token"
export CF_Zone_ID="your-zone-id"
```

Use `CF_Account_ID` instead of `CF_Zone_ID` if one token manages several zones in the same account. For other providers, look up the `--dns dns_xxx` name and variables on the acme.sh dnsapi wiki page.

## Step 4: Issue the wildcard certificate

```bash
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'
```

acme.sh creates `_acme-challenge` TXT records through the API, waits for them to propagate, lets the CA validate and removes them again. Quote `'*.example.com'` so the shell does not expand it. To request Let's Encrypt's 45-day profile instead of the default, add `--cert-profile tlsserver` (see [Let's Encrypt certificate profiles](https://ssl.sh/guides/lets-encrypt-certificate-profiles/)).

## Step 5: Install the certificate and set the reload command

`--install-cert` copies the certificate to a stable path and saves a reload command that runs after every renewal. Do not point the web server at files inside `~/.acme.sh/`, because their layout may change:

```bash
mkdir -p /etc/nginx/ssl
acme.sh --install-cert -d example.com --ecc \
  --key-file       /etc/nginx/ssl/example.com.key \
  --fullchain-file /etc/nginx/ssl/example.com.fullchain.pem \
  --reloadcmd      "systemctl reload nginx"
```

`--ecc` selects the ECDSA certificate, which is acme.sh's default key type. Then reference the files in Nginx:

```nginx
ssl_certificate     /etc/nginx/ssl/example.com.fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
```

## Verify

Every covered host should present the same certificate, with `example.com` and `*.example.com` in its SAN list. Check any of them with the [SSL checker](https://ssl.sh/tools/ssl-checker/), or from a terminal:

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

Locally, list what acme.sh manages and inspect the names in the certificate:

```bash
acme.sh --list
openssl x509 -in /etc/nginx/ssl/example.com.fullchain.pem -noout -ext subjectAltName -enddate
```

## Automatic renewal

The installer's cron job runs `acme.sh --cron`; since 3.1.4 it runs every 6 hours and follows the CA's ARI renewal window by default (set `NO_ARI=1` to disable). Without ARI, acme.sh renews on its own schedule, which you can override with `--days`; an explicit `--days` takes precedence over ARI since 3.1.5. After each renewal it re-runs the install step and your `--reloadcmd`.

```bash
crontab -l | grep acme.sh
acme.sh --cron --home ~/.acme.sh
acme.sh --upgrade --auto-upgrade
```

Test a full renewal against the staging CA (`--staging`) rather than forcing renewals in production, which count against rate limits. Add the domain to [ssl.sh monitoring](https://ssl.sh/monitor/) (in development), since Let's Encrypt no longer sends expiry emails.

## Troubleshooting

- **Error adding TXT record** or **invalid token**: the token lacks DNS edit permission on this zone, or the variable names are wrong; re-export and run again.
- **Verify error: no TXT record found**: the record had not propagated to all authoritative servers; add `--dnssleep 120` to wait longer.
- **CAA record prevents issuance**: add `0 issue "letsencrypt.org"` (and `issuewild` if you use it); see [CAA record prevents issuance](https://ssl.sh/errors/caa-record-prevents-issuance/).
- **Rate limited**: too many test issuances; use `--staging` and see [Let's Encrypt rate limits](https://ssl.sh/errors/lets-encrypt-rate-limit/).
- **Browser still shows the old certificate**: the web server points into `~/.acme.sh/` or the reload command failed; check `~/.acme.sh/acme.sh.log`.
- **Browser reports a missing intermediate**: you installed `--cert-file` instead of `--fullchain-file`; see [missing intermediate certificate](https://ssl.sh/errors/missing-intermediate-certificate/).
