HTTP-01 vs DNS-01 vs TLS-ALPN-01: Which ACME Challenge Should You Use?

When you ask Let's Encrypt (or any ACME-based CA) for a certificate, the CA needs to confirm you actually control the domain you're asking for. ACME (RFC 8555) defines three ways to prove that, and every CA picks the same names: HTTP-01, DNS-01, and TLS-ALPN-01. They are not interchangeable - each one has a sweet spot. Pick the wrong one and you either can't get a wildcard, or your renewals quietly stop working at 3 AM. Here is the decision tree, with real config for each.

Quick decision

  • Want a wildcard cert (*.example.com)? You must use DNS-01. Wildcards cannot be issued any other way.
  • Have a normal public site on ports 80/443? HTTP-01 is the default and works out of the box with certbot.
  • Cannot expose port 80 (load balancer terminates only 443, kiosk on private network)? TLS-ALPN-01 over port 443 only.
  • Issuing for an internal hostname that doesn't resolve from the public internet? DNS-01 - it's the only one the CA can validate without reaching your server.
  • After issuance: verify the live cert with our SSL certificate checker.

HTTP-01: the default

The ACME server gives your client a random token. Your client writes that token to a well-known path served over plain HTTP:

GET http://example.com/.well-known/acme-challenge/<random-token>
→ 200 OK
   <random-token>.<account-thumbprint>

The CA then fetches that URL from the public internet (with a couple of vantage-point retries). If it reads the expected value, the CA accepts that you control the domain and signs the cert.

When it's the right choice: a normal public website with port 80 reachable. This is what certbot --nginx and certbot --apache use by default, and what every CDN-backed origin uses. No DNS API integration required.

When it breaks: behind a firewall that blocks port 80; on a host where the load balancer only forwards 443; for any wildcard cert (HTTP-01 cannot prove control of *.example.com because there's no single host to fetch from). Also note: if you have an HTTP→HTTPS redirect, HTTP-01 follows redirects, so a 301 to /.well-known/acme-challenge/... over HTTPS is fine.

Minimal certbot command:

certbot certonly --webroot \
  -w /var/www/html \
  -d example.com -d www.example.com

For full Nginx + certbot setup on a fresh Ubuntu box, see our Nginx + Certbot on Ubuntu 24 guide.

DNS-01: the only path to wildcards

Instead of fetching an HTTP URL, the CA looks up a TXT record at a well-known DNS name:

dig TXT _acme-challenge.example.com
→ "<base64url(SHA256(token.account-thumbprint))>"

Your client writes that TXT record (via your DNS provider's API), waits for it to propagate, then tells the CA "ready". The CA queries authoritative DNS, verifies the record, and issues.

When it's the right choice: any wildcard cert (this is the only way), any host that isn't reachable from the public internet (the CA never has to talk to your server), bulk issuance across many hostnames, anywhere you want renewal to happen on a box other than the web server itself.

The catch: your DNS provider needs an API and your renewal client needs a plugin for that API. Cloudflare, Route 53, DigitalOcean, Google Cloud DNS, Vultr, Hetzner, Namecheap, and dozens more are supported out of the box by certbot via certbot-dns-* plugins (or use acme.sh, which has the widest provider list). Restricted-permission API tokens are the right move - scope the credential to just the one zone, not your whole account.

Minimal certbot command (Cloudflare):

# Install once
apt install python3-certbot-dns-cloudflare

# /root/.secrets/cloudflare.ini (chmod 600), with a Zone:DNS:Edit token:
# dns_cloudflare_api_token = <token>

certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  -d example.com -d '*.example.com'

CAA records still apply - make sure your CAA record permits Let's Encrypt (issuewild "letsencrypt.org" for wildcards specifically).

TLS-ALPN-01: when only port 443 is available

The CA opens a TLS connection to your server on port 443, advertising the special ALPN protocol acme-tls/1. Your ACME client (or the web server's ACME module) replies during that handshake with a self-signed certificate containing the validation token in a specific extension. No HTTP request, no DNS update - the whole challenge happens inside one TLS handshake on the port you already serve traffic on.

When it's the right choice: port 80 is closed (corporate firewall, load balancer that only forwards 443, embedded devices); you need to handle issuance entirely on the TLS-terminating layer with no HTTP path; you're running a non-web TLS service (SMTP submission, IMAP) and want to colocate validation on the same port.

The catch: client support is narrower than HTTP-01/DNS-01. Caddy, Traefik, HAProxy 2.5+, and a few standalone clients (lego, acme.sh) handle it natively. Nginx and Apache need a sidecar or extra module. It also cannot do wildcards - for that you still need DNS-01.

Comparison at a glance

ChallengeValidates viaWildcardsPort neededBest for
HTTP-01HTTP GETNo80Default for public web servers
DNS-01TXT recordYesNoneWildcards, internal hosts, bulk issuance
TLS-ALPN-01TLS handshakeNo443Port 80 closed, non-web TLS services

Renewal: same challenge, every time

Whichever challenge you start with, renewals use the same flow. Certbot stores the challenge type per cert in /etc/letsencrypt/renewal/<name>.conf, and the systemd timer just runs certbot renew twice a day. The 30-day-before-expiry safety net is shrinking fast - as certificate lifetimes drop to 47 days by 2029, automated renewal stops being optional. If you set up issuance manually once and forget about it, your monitoring should explicitly track when the last successful renewal ran, not just "days left on the cert".

Verify your cert

After issuance, confirm the live socket actually serves the new cert with a complete chain - wildcards in particular have a way of being issued correctly and then served against the wrong virtual host. Our checker shows issuer, validity, SANs, and the redirect/grade picture in one shot.