Certbot HTTP-01 renewal fails because Nginx redirects the challenge request to HTTPS

2026-11-14 · 3 min read

Certbot HTTP-01 renewal fails because Nginx redirects the challenge request to HTTPS

Your renewal timer fires at 03:14, certbot exits non-zero, and the certificate on the box has 9 days left. The log shows a 301 on /.well-known/acme-challenge/<token> and a validation error behind it. Nothing changed in your config — someone just "hardened" the port 80 vhost months ago, and the breakage only surfaced now, at renewal time.

Here is what is actually happening and how to fix it without leaving port 80 open to plain HTTP forever.

The redirect is not the bug by itself

Let's Encrypt's HTTP-01 validator follows redirects, including to HTTPS, up to a limited hop count [link needed]. A clean 301 to a working HTTPS vhost validates fine. The failure comes from what sits at the end of the redirect:

So the real question is never "should I redirect?" — it is "does the end of my redirect chain answer correctly for this hostname, right now?"

Why certbot http-01 redirect https challenge fails in Nginx specifically

Certbot's --nginx authenticator injects a location block for the challenge and reloads Nginx. If your vhost has a server-level return 301 https://...;, that directive runs in the server rewrite phase, *before* location matching — so certbot's injected location never gets a chance. Same story with --webroot when the redirect is server-level.

That is the mechanism behind most reports of *certbot http-01 redirect https challenge fails*: the plugin did its job, your config outranked it.

A server-level return beats any location, no matter how specific:


server {
    listen 80;
    server_name example.com;
    return 301 https://example.com$request_uri;   # wins over everything below
    location ^~ /.well-known/acme-challenge/ { root /var/www/acme; }  # never reached
}

Procedure

1. Reproduce the exact path the validator sees. curl -sSI http://example.com/.well-known/acme-challenge/test and follow each hop by hand: curl -sSI --resolve example.com:443:203.0.113.10 https://example.com/.well-known/acme-challenge/test.

2. Confirm the HTTPS hop with openssl s_client -connect example.com:443 -servername example.com (-servername matters; without it you get the default vhost and a false pass) [link needed].

3. Move the redirect from server scope into a location / block so the challenge location can win:


server {
    listen 80;
    server_name example.com;
    location ^~ /.well-known/acme-challenge/ { root /var/www/acme; try_files $uri =404; }
    location / { return 301 https://example.com$request_uri; }
}

4. Point certbot at the webroot explicitly: certbot renew --webroot -w /var/www/acme --dry-run. The dry run uses staging and does not burn rate limit [link needed].

5. Only after the dry run passes, drop --dry-run and confirm the timer works: systemctl list-timers | grep certbot.

When NOT to do this

Skip the webroot carve-out if the hostname is behind a CDN or load balancer that terminates TLS and rewrites paths — the redirect you see locally is not the one the validator sees. Use DNS-01 there instead. Also skip it if port 80 must be closed entirely (HSTS preload with no HTTP listener); HTTP-01 is then simply unavailable and DNS-01 is the only correct choice. And do not touch a working server-level redirect on a host where the HTTPS vhost is already proven valid — you would be trading a known-good config for a marginal one.

Checklist