Let's Encrypt fetched your challenge URL and got the wrong answer: another server answered, a proxy intercepted it, or your web server doesn't serve the file. Follow the path the challenge takes.
A redirect to HTTPS (creating a loop), an auth wall, a SPA catch-all swallowing /.well-known/acme-challenge/*, or a proxy returning a 404. curl the URL yourself: it should return the raw token text.
The challenge MUST arrive on port 80 of the host the DNS points to. A firewall, CDN in front (check proxy/origin rules), or another service on 80 answering instead.
curl -s -o - -w '\n%{http_code}\n' http://<domain>/.well-known/acme-challenge/test123 # should serve the literal token (200)
# location /.well-known/acme-challenge/ { root /var/www/certbot; } — and EXCLUDE it from the 301-to-https redirect
nc -zv -w 5 <domain> 80 ; sudo ss -ltnp | grep ':80 '
# Cloudflare: a Page Rule / cache rule bypassing /.well-known/acme-challenge/* solves cached-edge answers
http-01 must use port 80 — no way around it. If 80 is impossible, switch to dns-01. curl-ing the challenge URL and reading what ACTUALLY comes back (SPA HTML? 301 loop? 404?) identifies the intercepting layer instantly.
The site works on 443 through your normal stack, but the challenge comes in on port 80 and hits whatever answers THERE — a redirect, an auth prompt, or a catch-all that wasn't part of your testing.
Yes — dns-01 avoids port 80 entirely and is required for wildcards anyway. It needs DNS API credentials (certbot-dns-cloudflare etc.) but is the more robust long-term choice.
Our most-documented failures, packaged as ready-to-ship starter kits: Docker, Kubernetes, and Terraform.
Browse the template store →One-time. Yours to modify. Instant download from the NinjaOps template store.