AI Automation

Automate wildcard SSL with acme.sh and DNS API

Wildcard certificates used to mean a manual dance: log into the DNS panel, create a TXT record, wait for propagation, copy the private key to every server, and remember to do it again in 90 days. On a Linux VPS that serves multiple subdomains, that workflow quietly becomes the most error-prone job in your infrastructure. acme.sh with a DNS API removes the manual step entirely: it creates the TXT record itself, waits for propagation, issues the certificate, installs it, and schedules renewal in the background. This guide walks through the full setup on Ubuntu 24.04 with Cloudflare as the DNS provider, and covers the differences for other providers.

Prerequisites

  • A Linux VPS running Ubuntu 24.04, Debian 12, or AlmaLinux 9. This guide uses Ubuntu 24.04.
  • Root or sudo access to the server.
  • A domain with DNS managed by a provider that supports API access. Cloudflare, DigitalOcean, Vultr, and Hetzner all work.
  • API credentials for your DNS provider. Keep them in a safe place, they give full control over DNS records.
  • Nothing listening on port 80. The DNS-01 challenge does not need it.

Why wildcard certificates and why acme.sh

A wildcard certificate covers *.example.com plus, in most cases, the bare domain. One certificate, one renewal date, one private key. For a VPS running staging, API, and admin subdomains, that is simpler to manage than five separate certificates. The trade-off is security: a leaked wildcard key compromises every subdomain, so the key file permissions and backup policy matter more than usual.

acme.sh is a pure-shell ACME client that has become the default choice for DNS-01 automation. Unlike Certbot, which often needs a plugin per DNS provider, acme.sh ships with over 150 DNS API integrations built in. The DNS-01 challenge only requires a TXT record, so it works even when your server has no public HTTP port, sits behind a firewall, or only exposes SSH. That makes it the right tool on a locked-down SSL certificate setup where you do not want to open port 80 just for issuance.

使用 acme.sh 和 DNS API 自动签发通配符证书,全程无需 80 端口。

Using acme.sh with a DNS API issues wildcard certificates automatically, no port 80 required.

Step 1 - Installing acme.sh

The installer is a single command. It creates a dedicated user (acme), installs the script to ~/.acme.sh/, and adds a cron job for renewal checks.

curl https://get.acme.sh | sh -s [email protected]

Replace [email protected] with the address you want failing renewals sent to. The installer adds this line to your crontab:

# acme.sh cron job
0 0 * * * "/home/acme/.acme.sh"/acme.sh --cron --home "/home/acme/.acme.sh" > /dev/null

It runs daily at midnight. Each run checks whether any certificate is within 30 days of expiry, and reissues if so. No manual renewal ever needed.

Verify the install:

acme.sh --version

Expected output shows the version (v3.1 as of 2026) and the ACME server in use, Let's Encrypt by default.

Step 2 - Getting DNS API credentials

Each DNS provider has its own credential type. The API key must have permission to create and delete TXT records. This is the only permission acme.sh needs, so create a scoped token where the provider allows it, rather than using a global key.

For Cloudflare, create an API token with the Zone.DNS:Edit permission for the specific zone. For DigitalOcean, generate a personal access token with write scope. For Vultr, use the API key from your account settings. For Hetzner, create a project API token. The credential format changes, but the workflow stays identical: acme.sh reads it from the environment, calls the provider API, and cleans up the record after issuance.

# Cloudflare
export CF_Token="your_cloudflare_api_token"

# DigitalOcean
export DO_API_KEY="your_do_token"

# Vultr
export VULTR_API_KEY="your_vultr_key"

# Hetzner
export HETZNER_Token="your_hetzner_token"

These exports are temporary. acme.sh stores them in ~/.acme.sh/account.conf after the first issuance, but the file is readable only by the owner. Keep the master credentials somewhere else, a password manager or an encrypted file, and remove them from your shell history.

Step 3 - Issuing a wildcard certificate with DNS API

With the credential exported, issue the certificate. The --dns flag tells acme.sh which provider to use, dns_cf for Cloudflare, dns_dgon for DigitalOcean, dns_vultr for Vultr, dns_hetzner for Hetzner.

acme.sh --issue --dns dns_cf --domain example.com --domain "*.example.com" --server letsencrypt

What happens here matters, because it is what makes this whole setup reliable:

  1. acme.sh calls the Cloudflare API and adds a TXT record named _acme-challenge.example.com with a validation token.
  2. It polls DNS propagation, default sleep 20 seconds, and checks with a public resolver.
  3. It tells Let's Encrypt to validate, and the CA resolves the TXT record to confirm you control the domain.
  4. It removes the TXT record automatically. No cleanup step.
  5. It stores the certificate under ~/.acme.sh/example.com/.

Verify the certificate files:

ls -l ~/.acme.sh/example.com/

Expected output shows four files: fullchain.cer, example.com.cer, example.com.key, and ca.cer. The .cer extension is a PEM-encoded X.509 certificate, which is what nginx and most services expect.

The DNS-01 challenge works on any rent Linux VPS with outbound HTTPS, which is every VPS. No inbound ports, no web server configuration, no firewall rule. This is the entire point of the DNS API approach.

Step 4 - Installing the certificate and configuring nginx

Issue alone is not enough. The certificate needs to be deployed to the service that uses it, and on the next renewal the new files must overwrite the old ones without restarting the service. acme.sh handles both with the --install-cert command and a reload hook.

acme.sh --install-cert --domain "*.example.com" \
  --key-file /etc/nginx/ssl/example.com.key \
  --fullchain-file /etc/nginx/ssl/example.com.crt \
  --reloadcmd "systemctl reload nginx"

This copies the private key and full chain to /etc/nginx/ssl/ with restrictive permissions, then runs the reload command after every successful renewal. The reload is what makes renewal zero-downtime: nginx picks up the new certificate without dropping a single connection.

Make sure the target directory exists:

mkdir -p /etc/nginx/ssl
chown root:root /etc/nginx/ssl
chmod 700 /etc/nginx/ssl

Then reference the certificates in your server block:

server {
    listen 443 ssl;
    server_name example.com *.example.com;

    ssl_certificate     /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
}

Test the configuration and reload:

nginx -t && systemctl reload nginx

Verify the certificate is served:

echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates

Expected output shows subject=CN = *.example.com with the correct validity dates, and a short chain. If the chain is long, you are missing intermediate certificates. Check that the fullchain file is installed, not just the leaf certificate.

File pathPurposePermissions
/etc/nginx/ssl/example.com.crtFull chain, leaf plus intermediates644
/etc/nginx/ssl/example.com.keyPrivate key600
~/.acme.sh/example.com/acme.sh working directory, source for installs700

Step 5 - Testing renewal manually

Do not wait 80 days to find out the renewal is broken. Force a dry-run renewal now, and then force a real one once, to confirm the whole path works end to end.

acme.sh --renew --domain "*.example.com" --force

The --force flag reissues even though the certificate is far from expiry. In a dry run you would see the validation steps log without writing files, but a real forced renewal is the definitive test. After it completes, confirm nginx picked up the new files:

systemctl status nginx --no-pager | head -5

Expected output shows active (running). If nginx failed to reload, the --reloadcmd step is where the problem is, and it is far better to find that out now than in three months.

You can verify the cron job will handle future renewals:

crontab -l | grep acme

Expected output shows the daily cron entry. Note that the cron runs as whichever user ran the installer, not as root. If you installed as root, renewals run as root, which is fine for a small VPS. If you want a separate non-root user for everything, install acme.sh as that user from the start, it is harder to migrate later.

Step 6 - Automating deployment with a hook script

Renewal is automated, but deployment only covers nginx. When a service like Postfix, Dovecot or HAProxy uses the certificate, the reload command needs to handle those too. Rather than piling commands into --reloadcmd, use a deploy hook script. acme.sh runs it after every successful renewal, and it can do anything: copy files, reload services, push to other servers.

cat > /etc/nginx/ssl/deploy-hook.sh <<'EOF'
#!/bin/bash
# Deploy renewed certificates to all services
systemctl reload nginx
systemctl reload postfix || true
systemctl reload dovecot || true
# Copy to a second server for load balancing
scp /etc/nginx/ssl/example.com.crt [email protected]:/etc/ssl/example.com.crt
ssh [email protected] "systemctl reload nginx"
EOF
chmod +x /etc/nginx/ssl/deploy-hook.sh

Then reference the hook in the install command. Re-run the install command so acme.sh records the hook for future renewals:

acme.sh --install-cert --domain "*.example.com" \
  --key-file /etc/nginx/ssl/example.com.key \
  --fullchain-file /etc/nginx/ssl/example.com.crt \
  --reloadcmd "/etc/nginx/ssl/deploy-hook.sh"

The || true guards matter: if Postfix is not installed on this server, the reload fails, but that should not abort the rest of the hook. For copying to other servers, prefer SSH keys over passwords, so the hook runs unattended.

This is where a wildcard certificate pays off on a multi-service VPS. One renewal, one hook, everything updated. If instead each service requested its own certificate, you would need a deploy hook per certificate and the failure modes multiply.

Why does the DNS-01 challenge matter for automation

The alternative to DNS API is the HTTP-01 challenge, where the CA connects to port 80 on your server and checks a specific file. It works, but it couples certificate management to your web server: the renewal job has to reach the right vhost, the firewall has to allow port 80, and a misconfigured nginx can silently break the next renewal. The DNS-01 challenge sidesteps all of it. The only requirement is outbound HTTPS, which every VPS has.

There is one practical caveat: the DNS API credential is more powerful than a web server file, because it can modify DNS for the whole zone. Keep it scoped, keep it out of git, and rotate it if you ever suspect a leak. On a WordPress VPS or any other single-purpose box this is usually overkill, but on a server that handles certificates for multiple domains it is the difference between "renewal just works" and "renewal breaks at 2 AM on a Sunday".

Troubleshooting common renewal failures

Three failures hit most people, and each has a clear diagnostic.

TXT record not visible to the CA. This shows up as a validation error with DNS problem: NXDOMAIN or wrong TXT record. The record exists in your provider panel but is not propagating. Most providers are fast, but some take 30 seconds or more. Increase the propagation wait: rebuild the challenge with the --dnssleep flag, which you can pass with the issue command. Prefer this over hardcoding a wait, as acme.sh handles the polling itself.

journalctl -u acme.sh --since "1 hour ago"

Check the log for the exact validation URL returned by the CA, then query it directly to see what the CA sees.

API credential has insufficient scope. The error is usually a 403 from the provider API, often masked as a generic failure. Verify the token can create TXT records independently, in the provider dashboard, before blaming acme.sh. A token with Zone.Zone:Read but not Zone.DNS:Edit will fail exactly this way.

Reload command fails after renewal. The certificate renews fine, but the service does not reload, so the old certificate stays until the service restart. Check the reload command by hand, exactly as it appears in the cron environment:

ls -l /etc/nginx/ssl/

If the timestamps show the new certificate but systemctl status nginx still loads a stale one, the reload command exited non-zero. Run the hook manually and read the error. On SELinux systems, setsebool -P httpd_can_network_connect 1 may be required for the reload to work, which is an easy one to miss on an AlmaLinux or Rocky box.

FAQ

Does acme.sh still work in 2026?

Yes. acme.sh v3.1 is actively maintained, supports the current ACME protocol, and the Let's Encrypt and ZeroSSL servers. It remains the standard tool for scripted DNS-01 automation thanks to its large set of built-in DNS provider integrations.

Is a wildcard certificate less secure than individual certificates?

A wildcard private key protects every subdomain, so its exposure is a higher-impact event. Mitigate with strict file permissions (600 on the key), a limited-SSL certificate renewal scope, and short-lived certificates, which Let's Encrypt provides by default at 90 days.

Which DNS providers does acme.sh support?

Over 150. Cloudflare, DigitalOcean, Vultr, Hetzner, Linode, AWS Route 53, Google Cloud DNS, and most smaller registrars are covered. Check the official acme.sh wiki for the exact environment variable needed for your provider.

Do I need to open port 80 for the DNS-01 challenge?

No. The DNS-01 challenge only requires creating a TXT record via the provider API, so no inbound network access of any kind is needed on the VPS. This works even on a server with a strict firewall that only allows SSH.

How early does acme.sh renew certificates?

The default is 60 days before expiry, which leaves a wide safety margin for propagation delays or provider hiccups. You can change this with --days when issuing, but the default is reasonable for most setups.

Related articles

通配符 SSL 证书自动签发要点

使用 acme.sh 配合 DNS API,可以完全不开放 80 端口,自动签发和续期通配符证书。关键在于给 API 令牌设置最小权限,并且务必手动强制续期一次,确认部署钩子正常工作。续期后通过系统日志验证服务已加载新证书,这样就能长期免维护运行。证书私钥权限要收紧到 600,防止泄露影响所有子域名。

Note: This guide is for general reference. Every system and infrastructure has its own specifics, so test each step in a safe environment and consult a qualified engineer before applying it in production.