If certificate renewal fails with a 404 on /.well-known/acme-challenge/, the problem is usually not Let's Encrypt itself. Nginx is not serving the validation file from the webroot Certbot expects.
Operational context
- When to use this: you run Certbot in webroot mode behind Nginx and renewals fail during HTTP-01 validation.
- What it fixes: requests to
/.well-known/acme-challenge/returning404instead of the challenge token file. - Tradeoff: webroot validation depends on Nginx serving the exact challenge path. A misconfigured
locationblock or wrong webroot path will break renewals silently until expiry approaches.
Typical Certbot command
certbot certonly \
--webroot \
--webroot-path=/var/www/letsencrypt \
--email email@domain.com \
--agree-tos \
-d domain.com \
-d www.domain.com
Older guides may use the letsencrypt command name. On current systems the command is usually certbot.
Nginx webroot configuration
Create a dedicated challenge directory:
sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
sudo chown -R www-data:www-data /var/www/letsencrypt
Then add an explicit location block to the HTTP server:
server {
listen 80;
server_name domain.com www.domain.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type "text/plain";
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
The root directive matters. With the configuration above, a request for:
/.well-known/acme-challenge/test-token
maps to:
/var/www/letsencrypt/.well-known/acme-challenge/test-token
WordPress or application catch-all conflict
WordPress and many PHP applications use a catch-all route such as:
try_files $uri $uri/ /index.php?$args;
If the ACME challenge path reaches that block, the application may return a 404, redirect, or themed error page. The ACME location ^~ /.well-known/acme-challenge/ block must appear before generic application handling so Nginx serves the token file directly.
Local verification before renewal
Create a temporary token:
echo "ok" | sudo tee /var/www/letsencrypt/.well-known/acme-challenge/ping
Reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Then test from outside the server:
curl -i http://domain.com/.well-known/acme-challenge/ping
You should see HTTP/1.1 200 OK and the body ok.
What to verify in production
- The webroot path in Certbot matches the directory Nginx serves for the domain.
- Nginx has an explicit location for
/.well-known/acme-challenge/. - The challenge file is readable by the Nginx worker process.
- No catch-all rewrite sends ACME requests to WordPress or another application route.
- HTTP traffic on port 80 reaches this server from the public internet.
Renewal dry run
After configuration changes, run:
sudo certbot renew --dry-run
A successful dry run confirms the challenge path and renewal automation work before the real certificate is close to expiry.
When HTTP-01 is not the right choice
Use DNS-01 validation instead when:
- port 80 is blocked
- the site sits behind a CDN or WAF rule that interferes with challenge files
- you need wildcard certificates
- multiple origin servers serve the same hostname without a shared challenge directory
HTTP-01 is simple when one Nginx origin owns the hostname. DNS-01 is usually better for wildcard and multi-origin setups.
Useful references
- Nginx ACME challenge configuration examples
- Certbot webroot validation notes
- Alternative Nginx ACME location patterns
Verification
After updating Nginx, request a dry-run renewal or issue a test certificate. A successful validation confirms that Nginx is serving the challenge path correctly and that automated renewals should continue to work.
Related work
This TLS automation pattern supports Golden image and hardening pipeline and secure platform baselines in Selected operational work.