ACME clients and automation
How to get a wildcard certificate with acme.sh and a DNS API
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
curlorwget,openssl,cronand 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
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:
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:
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
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).
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:
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:
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, or from a terminal:
curl ssl.sh/example.com
curl ssl.sh/www.example.com
Locally, list what acme.sh manages and inspect the names in the certificate:
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.
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 (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 120to wait longer. - CAA record prevents issuance: add
0 issue "letsencrypt.org"(andissuewildif you use it); see CAA record prevents issuance. - Rate limited: too many test issuances; use
--stagingand see Let's Encrypt rate limits. - 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-fileinstead of--fullchain-file; see missing intermediate certificate.
FAQ
Does a wildcard certificate cover the bare domain?
No. *.example.com matches www.example.com and api.example.com but not example.com itself, and not a.b.example.com. Request both -d example.com -d '*.example.com' in one certificate.
Where does acme.sh store my DNS API token?
After the first successful issue, acme.sh saves the credentials as SAVED_... entries in ~/.acme.sh/account.conf so renewals can reuse them. Protect that file, use a token limited to DNS edit rights on the zones you need, and rotate it if the server is compromised.
What is dns-persist-01 in acme.sh?
acme.sh 3.1.4 added support for the dns-persist-01 challenge (--dns-persist-days), which validates with a long-lived TXT record bound to your ACME account instead of a new record on every order. Check your CA's announcements for whether it is available in production before relying on it.
Can I keep a ZeroSSL certificate instead?
Yes. ZeroSSL is acme.sh's default CA and requires an account email (acme.sh --register-account -m you@example.com). Your CAA records must allow the CA you use.