HTTP-01 died after you forced HTTPS.

Certbot needs /.well-known/acme-challenge/ on port 80. A blanket HTTP-to-HTTPS redirect or auth wall returns 404 or unauthorized. Prove the path, carve an exception, renew, then watch expiry off-box.

Cover Image for HTTP-01 died after you forced HTTPS.

Certbot asked Let's Encrypt to fetch http://yourdomain.com/.well-known/acme-challenge/<token>.

Your vhost answered with a 301 to HTTPS, a 404, or a login page.

The renew log shows unauthorized or 404. Exit is not 0. No new files under /etc/letsencrypt/live/. The cert on :443 keeps ageing.

HTTP-01 is an HTTP on port 80 check. A site-wide "force HTTPS" rule that also catches /.well-known/acme-challenge/ breaks renewals the day the redirect went live, not the day you first enabled TLS.

Prove it

1. Read the failure (on the box)

# last renew attempt
sudo tail -n 80 /var/log/letsencrypt/letsencrypt.log

# or a dry-run that prints the same class of error
sudo certbot renew --dry-run

Look for lines naming http-01, unauthorized, 404, or the challenge URL. If the log instead shows success and disk is fresh but browsers still see the old cert, stop here and use the nginx reload write-up. That is disk vs wire, not ACME.

2. Hit the challenge path yourself (from a laptop, not only localhost)

Certbot writes a token file only during an active attempt. Between runs the path may 404 for a different reason. During a dry-run or renew, in another terminal:

# replace TOKEN with the value certbot is serving this attempt
curl -sS -D- -o /dev/null http://yourdomain.com/.well-known/acme-challenge/TOKEN

What you want: HTTP 200 on port 80, body equal to the token file contents.

What breaks renewals:

ResponseLikely cause
301 / 302 to https://…Force-HTTPS caught the challenge path
404Root/alias wrong, or the request never reached the web root certbot uses
401 / 403Auth wall, IP allowlist, WAF, or deny all on .well-known
Connection refused on :80Nothing listening; firewall; "HTTPS only" at the edge
200 on HTTPS onlyThe challenge must work on HTTP for classic HTTP-01

3. Confirm what owns port 80

sudo ss -lptn 'sport = :80'
# or
sudo lsof -i :80

If the process is not the nginx or Apache you edited, you fixed the wrong edge (CDN, load balancer, another host).

Same class of check without SSH while a cert is still on the wire: paste the hostname into the SSL expiry checker and read days left. That does not debug ACME, but it shows how close the served leaf is to becoming a browser warning while renew stays red.

For the full failure map (reload, timer, rate limit, root mail), use the disk vs wire hub.

Fix now (carve out ACME on port 80)

Goal: /.well-known/acme-challenge/ is served in plain HTTP before any HTTPS redirect or auth.

nginx

Place the challenge location above the blanket redirect. Use the same webroot certbot expects (often /var/www/html or a dedicated /var/www/certbot).

server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;

    # ACME HTTP-01 must stay on HTTP
    location ^~ /.well-known/acme-challenge/ {
        default_type "text/plain";
        root /var/www/certbot;   # parent of /.well-known/...
    }

    location / {
        return 301 https://$host$request_uri;
    }
}
sudo nginx -t && sudo systemctl reload nginx

Webroot plugin example (paths must match root above):

sudo certbot certonly --webroot \
  -w /var/www/certbot \
  -d yourdomain.com -d www.yourdomain.com

If you use certbot --nginx, still verify the generated server block did not put a catch-all redirect ahead of the challenge location after a later manual edit.

Apache

<VirtualHost *:80>
    ServerName yourdomain.com
    ServerAlias www.yourdomain.com

    DocumentRoot /var/www/html

    # Allow ACME without auth
    <Location "/.well-known/acme-challenge/">
        Require all granted
    </Location>

    # Redirect everything else to HTTPS
    RewriteEngine On
    RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
sudo apache2ctl configtest && sudo systemctl reload apache2
# Debian/Ubuntu; on RHEL-like: httpd + systemctl reload httpd

CDN / load balancer / "HTTPS only" at the edge

If Cloudflare (or similar) forces HTTPS at the zone before traffic hits origin, HTTP-01 never reaches your carve-out.

Options that ops teams actually use:

  1. DNS-01 instead of HTTP-01 (API token to your DNS provider; no port 80 needed).
  2. Edge rule: do not force HTTPS for /.well-known/acme-challenge/*, and do not lock that path behind a bot challenge in a way that blocks the CA.
  3. Issue and terminate certificates at the edge, then monitor the edge hostname visitors hit, not only origin.

Pick one model. Mixed "origin certbot HTTP-01 plus edge force-HTTPS with no exception" stays red.

Firewall

# port 80 must accept the CA (and your own curl check)
sudo iptables -L -n | head   # or nft list ruleset; or cloud SG/NSG UI

If :80 is closed world-wide, HTTP-01 cannot pass. Open 80 for the challenge path, or move to DNS-01.

Re-run renew

sudo certbot renew --dry-run
# when dry-run is clean:
sudo certbot renew

Then confirm disk has a new notAfter:

openssl x509 -noout -enddate \
  -in /etc/letsencrypt/live/yourdomain.com/fullchain.pem

And wire matches after the TLS process reloads (see the nginx reload write-up if disk moves and wire does not):

echo | openssl s_client -connect yourdomain.com:443 \
  -servername yourdomain.com 2>/dev/null \
  | openssl x509 -noout -enddate

Make it permanent

  1. Keep the ACME location permanent in config management. A one-off edit dies on the next "force HTTPS" PR.
  2. Prefer a dedicated webroot for challenges (/var/www/certbot) so app deploys do not wipe token files.
  3. After any change to redirects, HSTS assumptions, or CDN HTTPS mode, run certbot renew --dry-run in CI or as a scheduled check.
  4. If port 80 is a long-term liability, switch the authenticator to DNS-01 and document the API credential rotation.

HSTS on HTTPS does not replace a working HTTP-01 path. Browsers honor HSTS; the Let's Encrypt validation client still speaks HTTP for HTTP-01.

How you catch it off-box

On the box, renew logs go unread for weeks. Root mail never leaves. The next human signal is a browser warning or a monitoring page that only pings HTTP 200.

  • After the fix: SSL expiry checker on the public hostname. Confirm days left on the served leaf.
  • Any port, quick wire read: SSL checker.
  • Ongoing: CertPost watches the served cert (3 free, no card) and emails at 30/14/7/1 days. When HTTP-01 breaks again, you still see the leaf age even if nobody opens letsencrypt.log.

Watch the wire. Fix ACME so disk can catch up.