← Back to posts
comparison guide self-hosted · · 10 min read

Certbot vs acme.sh vs lego vs Dehydrated: Best ACME DNS-01 Challenge Tools 2026

Compare Certbot, acme.sh, lego, and Dehydrated for ACME DNS-01 challenge automation. Complete guide to wildcard certificates, DNS provider integration, and self-hosted TLS deployment in 2026.

OS
Editorial Team

When you need TLS certificates for domains that aren’t directly accessible via HTTP — wildcard certificates, internal services, or machines behind NAT — the ACME DNS-01 challenge is the only reliable option. Unlike the HTTP-01 challenge which requires a publicly accessible web server, DNS-01 proves domain ownership by creating a specific DNS TXT record that the Certificate Authority (CA) can verify.

This guide compares the four leading open-source ACME clients that support DNS-01 challenges: Certbot, acme.sh, lego, and Dehydrated. We’ll cover installation, Docker deployment, DNS provider integration, and automated renewal so you can pick the right tool for your infrastructure.

For a broader look at certificate automation including Kubernetes-native approaches, see our cert-manager vs lego vs acme.sh comparison.

What Is the ACME DNS-01 Challenge?

The ACME protocol (RFC 8555) defines how clients can automatically obtain TLS certificates from CAs like Let’s Encrypt, ZeroSSL, and Google Trust Services. The DNS-01 challenge works as follows:

  1. The ACME client requests a certificate for *.example.com
  2. The CA responds with a challenge token and a key authorization
  3. The client creates a TXT record at _acme-challenge.example.com with the key authorization
  4. The CA queries DNS for that TXT record
  5. If the record matches, the CA issues the certificate

This process is ideal for:

  • Wildcard certificates — the only challenge type that supports *.domain.com
  • Internal services — no HTTP endpoint needs to be publicly accessible
  • Load-balanced environments — avoids the need for shared storage or coordinated HTTP responses
  • Offline issuance — certificates can be generated on a separate machine and deployed later

Why Use DNS-01 Instead of HTTP-01?

The HTTP-01 challenge is simpler to set up but has significant limitations:

FeatureHTTP-01 ChallengeDNS-01 Challenge
Wildcard certificatesNot supportedFully supported
Requires public HTTP portYes (port 80)No
Works behind NAT/firewallNoYes
Requires web server accessYesNo
DNS provider API neededNoYes
Ideal for internal servicesNoYes
Supports multiple CAsYesYes

If you run a reverse proxy like nginx, Caddy, or Traefik and need wildcard certs for multiple subdomains, DNS-01 is the only practical choice.

Certbot vs acme.sh vs lego vs Dehydrated: Comparison

Here is how the four tools stack up as of April 2026:

FeatureCertbotacme.shlegoDehydrated
GitHub stars33,01246,4099,4966,198
LanguagePythonShellGoShell
LicenseApache 2.0GPL 3.0MITMIT
DNS-01 supportYes (via plugins)Yes (native)Yes (native)Yes (via hooks)
Wildcard certsYesYesYesYes
ZeroSSL supportYesYesYesNo
Google Trust ServicesYesYesYesLimited
Docker imageOfficialCommunityOfficialNone
DNS providers~20 plugins100+ built-in60+ built-inVia custom hooks
Auto-renewalsystemd timerBuilt-in cronsystemd/cronManual or cron
ConfigurationINI/CLI flagsEnvironment varsCLI flags + envConfig file
Key size optionsRSA + ECDSARSA + ECDSARSA + ECDSA + ED25519RSA + ECDSA
Multi-domain SANsYesYesYesYes
Staging environmentYesYesYesYes
ACME v2 supportYesYesYesYes
Last active2026-04-242026-04-262026-04-252026-02-03

Key takeaways:

  • Certbot is the most widely known ACME client with official Docker images and strong plugin support, but DNS plugins are separate packages that need individual installation.
  • acme.sh has the broadest DNS provider support with 100+ integrations built in, making it the easiest choice for uncommon DNS providers.
  • lego is the most lightweight binary (single Go executable) with strong ED25519 key support and clean API design.
  • Dehydrated is the simplest shell script but requires the most manual configuration for DNS hooks.

Installing Each Tool

Certbot

Certbot is available through system package managers on most distributions:

1
2
3
4
5
6
7
8
9
# Ubuntu/Debian
sudo apt update
sudo apt install certbot python3-certbot-dns-cloudflare

# RHEL/CentOS/Fedora
sudo dnf install certbot python3-certbot-dns-cloudflare

# macOS (Homebrew)
brew install certbot

For DNS-01 challenges, you also need the corresponding DNS plugin. Available plugins include python3-certbot-dns-cloudflare, python3-certbot-dns-route53, python3-certbot-dns-google, and more.

acme.sh

acme.sh installs to ~/.acme.sh and manages its own cron job:

1
2
3
4
5
6
7
8
9
curl https://get.acme.sh | sh -s email=your@email.com
source ~/.bashrc

# Set DNS provider credentials
export CF_Token="your_cloudflare_api_token"
export CF_Account_ID="your_account_id"

# Issue wildcard certificate
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'

lego

lego is distributed as a single static binary:

1
2
3
4
5
6
7
8
# Download from GitHub releases
curl -fsSL https://github.com/go-acme/lego/releases/download/v4.20.1/lego_v4.20.1_linux_amd64.tar.gz | tar xz
sudo mv lego /usr/local/bin/lego
chmod +x /usr/local/bin/lego

# Issue certificate with Cloudflare DNS
export CLOUDFLARE_DNS_API_TOKEN="your_token"
lego --email your@email.com --dns cloudflare --domains example.com --domains '*.example.com' run

Dehydrated

Dehydrated is a single bash script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
git clone https://github.com/dehydrated/dehydrated.git
cd dehydrated
sudo cp dehydrated /usr/local/bin/
sudo chmod +x /usr/local/bin/dehydrated

# Accept terms
dehydrated --register --accept-terms

# Create config
mkdir -p /etc/dehydrated
cat > /etc/dehydrated/config << 'EOF'
CA="https://acme-v02.api.letsencrypt.org/directory"
WELLKNOWN="/var/lib/dehydrated/acme-challenges"
HOOK="/etc/dehydrated/hook.sh"
EOF

Docker Deployment Examples

Certbot with Docker

Certbot provides official Docker images that handle certificate issuance and renewal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
services:
  certbot:
    image: certbot/certbot:latest
    container_name: certbot-dns
    volumes:
      - ./certs:/etc/letsencrypt
      - ./certbot-logs:/var/log/letsencrypt
    environment:
      - CLOUDFLARE_DNS_API_TOKEN=${CF_TOKEN}
    entrypoint: "/bin/sh -c"
    command: >
      "certbot certonly --dns-cloudflare
      --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini
      -d example.com -d '*.example.com'
      --agree-tos --email admin@example.com
      --non-interactive --keep-until-expiring"

  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
    volumes:
      - ./certs:/etc/letsencrypt:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - certbot

Create the Cloudflare credentials file:

1
2
# cloudflare.ini
dns_cloudflare_api_token = your_api_token_here

acme.sh with Docker

While acme.sh doesn’t have an official Docker image, the community maintains reliable images:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  acmesh:
    image: neilpang/acme.sh:latest
    container_name: acme-sh
    volumes:
      - ./certs:/acme.sh
    environment:
      - CF_Token=${CF_TOKEN}
      - CF_Account_ID=${CF_ACCOUNT_ID}
      - CF_Email=${CF_EMAIL}
    command: >
      --issue
      --dns dns_cf
      -d example.com
      -d '*.example.com'
      --force
      --log

To install certificates to a persistent volume after issuance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    command: >
      --issue
      --dns dns_cf
      -d example.com
      -d '*.example.com'
      --install-cert
      -d example.com
      --key-file /acme.sh/certs/privkey.pem
      --fullchain-file /acme.sh/certs/fullchain.pem
      --reloadcmd "cat /acme.sh/certs/fullchain.pem /acme.sh/certs/privkey.pem > /acme.sh/certs/combined.pem"

lego with Docker

lego provides official Docker images:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
services:
  lego:
    image: goacme/lego:latest
    container_name: lego-dns
    volumes:
      - ./certs:/data
    environment:
      - CLOUDFLARE_DNS_API_TOKEN=${CF_TOKEN}
      - LEGO_EMAIL=admin@example.com
    command: >
      --dns cloudflare
      --domains example.com
      --domains '*.example.com'
      --accept-tos
      --path /data
      run

The certificates are stored in /data/certificates/ within the container and mapped to ./certs/ on the host.

DNS Provider API Setup

Most DNS providers require an API token with specific permissions for DNS record management:

Cloudflare

Create an API token at the Cloudflare dashboard with these permissions:

  • Zone → DNS → Edit
  • Zone → Zone → Read
1
2
3
4
5
6
7
8
# acme.sh
export CF_Token="your_token"
export CF_Account_ID="your_account_id"
acme.sh --issue --dns dns_cf -d '*.example.com'

# lego
export CLOUDFLARE_DNS_API_TOKEN="your_token"
lego --dns cloudflare -d '*.example.com' run

AWS Route 53

Create an IAM policy with these permissions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "route53:GetChange",
        "route53:ChangeResourceRecordSets",
        "route53:ListHostedZonesByName"
      ],
      "Resource": "*"
    }
  ]
}
1
2
3
4
5
6
7
8
9
# Certbot
sudo certbot certonly \
  --dns-route53 \
  -d example.com -d '*.example.com'

# lego (uses AWS credentials from environment)
export AWS_ACCESS_KEY_ID="your_key"
export AWS_SECRET_ACCESS_KEY="your_secret"
lego --dns route53 -d '*.example.com' run

Google Cloud DNS

Create a service account with the DNS Administrator role and export the key:

1
2
3
4
5
6
7
8
9
# lego
export GCE_SERVICE_ACCOUNT_FILE="/path/to/service-account.json"
lego --dns gcloud -d '*.example.com' run

# Certbot
sudo certbot certonly \
  --dns-google \
  --dns-google-credentials /path/to/service-account.json \
  -d '*.example.com'

Automated Renewal Setup

Certbot (systemd timer)

Certbot includes a systemd timer that runs twice daily:

1
2
3
4
5
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer

# Test renewal (dry run)
sudo certbot renew --dry-run

For DNS plugins, add the credentials to /etc/letsencrypt/renewal/*.conf:

1
2
3
# /etc/letsencrypt/renewal/example.com.conf
[renewalparams]
dns_cloudflare_credentials = /etc/letsencrypt/cloudflare.ini

acme.sh (built-in cron)

acme.sh automatically installs a cron entry during setup:

1
2
3
4
5
6
7
8
# Check installed cron job
crontab -l | grep acme

# Manual test
acme.sh --renew -d example.com --force

# Upgrade acme.sh to latest version
acme.sh --upgrade

lego (systemd timer)

Create a systemd timer for automatic renewal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# /etc/systemd/system/lego-renew.service
[Unit]
Description=Lego ACME Certificate Renewal

[Service]
Type=oneshot
ExecStart=/usr/local/bin/lego --email admin@example.com \
  --dns cloudflare \
  --domains example.com --domains '*.example.com' \
  --path /etc/lego renew
EnvironmentFile=/etc/lego/lego.env
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /etc/systemd/system/lego-renew.timer
[Unit]
Description=Run Lego renewal twice daily

[Timer]
OnCalendar=*-*-* 00/12:00:00
Persistent=true

[Install]
WantedBy=timers.target
1
sudo systemctl enable --now lego-renew.timer

Dehydrated (cron)

1
2
3
# /etc/cron.daily/dehydrated
#!/bin/bash
/usr/local/bin/dehydrated --cron --hook /etc/dehydrated/hook.sh --config /etc/dehydrated/config

Which Tool Should You Choose?

Use CaseRecommended ToolReason
Quick setup with popular DNS providersacme.sh100+ providers built in, zero config
Kubernetes/infrastructure automationlegoSingle binary, clean API, ED25519 support
Enterprise/Compliance environmentsCertbotWell-audited, official packages, broad CA support
Minimal dependencies (bare bash)DehydratedSingle script, no runtime dependencies
Multi-cloud DNS providersacme.shBroadest provider coverage
CI/CD pipeline integrationlegoEasy to embed in CI, no Python/Shell deps
Existing Certbot infrastructureCertbotMigration path is seamless

If you are also interested in verifying your TLS configuration after deployment, check out our SSL/TLS scanning tools guide.

FAQ

What is the difference between HTTP-01 and DNS-01 ACME challenges?

HTTP-01 requires placing a file at http://yourdomain/.well-known/acme-challenge/ which the CA fetches over HTTP. DNS-01 requires creating a TXT record at _acme-challenge.yourdomain.com which the CA resolves via DNS. DNS-01 is the only method that supports wildcard certificates and works for services not accessible via the public internet.

Can I use DNS-01 challenges without exposing my DNS provider API credentials?

All DNS-01 tools require API access to create TXT records. However, you can minimize risk by using scoped API tokens (e.g., Cloudflare tokens limited to DNS edit on specific zones) rather than full account keys. Tools like acme.sh and lego support reading credentials from environment variables, Docker secrets, or HashiCorp Vault rather than plaintext config files.

How long does DNS propagation take for ACME challenges?

Most CAs poll for the DNS TXT record every 2-5 seconds and typically complete validation within 30-60 seconds. However, some DNS providers have higher propagation delays. If validation fails, all tools support configurable wait periods — acme.sh uses --dnssleep, Certbot uses --dns-cloudflare-propagation-seconds, and lego uses --dns-timeout.

Do these tools support multiple CAs beyond Let’s Encrypt?

Yes. Certbot, acme.sh, and lego all support Let’s Encrypt, ZeroSSL, Google Trust Services, and BuyPass. Dehydrated supports Let’s Encrypt and ZeroSSL with custom CA configuration. acme.sh allows switching CAs with --server zerossl or --server buypass. lego uses --server flag to specify any ACME v2 endpoint.

Can I automate DNS-01 challenges if my DNS provider is not natively supported?

Yes. All four tools support custom DNS hooks. For acme.sh, create a dns_myprovider.sh script following their API format. For Certbot, write a Python plugin or use the --manual-auth-hook flag. For lego, implement the DNS provider interface in Go or use the --dns flag with a custom executable. For Dehydrated, the hook script receives deploy_challenge and clean_challenge calls.

How do I renew certificates before they expire?

All tools handle renewal automatically when configured correctly. Certbot runs twice daily via systemd timer and only renews within 30 days of expiry. acme.sh checks certificates daily via cron and renews automatically. lego requires a systemd timer or cron entry to trigger renewal. Dehydrated needs a daily cron job with the --cron flag. You can always force a renewal manually for testing with the respective --force or --dry-run flags.

Advertise here
Advertise here