Certbot HTTP-01 renewal fails because Nginx redirects the challenge request to HTTPS
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:
- The redirect target serves the *expired* certificate you are trying to renew, so the TLS handshake fails before the token is read.
- The redirect rewrites the host (apex →
www) but the certificate being renewed only covers the apex. - Port 443 is filtered, or the vhost relies on SNI while the validator connects without it.
- A redirect loop:
http → https → http. - HSTS or a misconfigured
ssl_certificatechain on the target vhost.
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
- [ ] Every hop of the redirect chain returns 200 for the challenge path
- [ ] HTTPS hop tested with
-servername, not by bare IP - [ ] Challenge
locationplaced above the catch-alllocation / - [ ]
--dry-runpasses before the real renewal - [ ] DNS-01 considered if a proxy or CDN sits in front