Skip to content
ssldays

Guide

ACME HTTP-01 challenge failed 403

Fix HTTP-01 ACME 403 errors: unblock /.well-known/acme-challenge on port 80 from the internet and CDN WAF rules.

Updated Apr 2, 2026 884 words

HTTP-01 failed with 403 because the challenge URL is denied—not because TLS on 443 is broken.

When this happens

Let's Encrypt logs show connection refused or wrong content less often than invalid response: 403 for /.well-known/acme-challenge/. Security teams tighten OWASP rules monthly; ACME is often the first anonymous GET blocked on Monday morning renewals.

Certbot webroot mode wrote a token file, but security plugins or nginx auth_basic still block anonymous GETs to dot-directories. A/B routing may send challenge GETs to canary pools without token files.

First response

Fix HTTP-01 ACME 403 errors: unblock /.well-known/acme-challenge on port 80 from the internet and CDN WAF rules.

Searches for acme challenge failed 403 usually mean automated renewal failed, succeeded locally but never reached the TLS listener, or hit Let's Encrypt rate limits. The fix is almost always permissions, challenge reachability, or a missing reload—not buying a longer-lived commercial cert as the first move.

For ACME HTTP-01 challenge failed 403, read the ACME or panel error verbatim, run a dry-run renewal if your stack supports it, then confirm what certificate strangers receive on port 443 after you deploy. Certificate files on disk lie; only live handshakes matter for users.

HTTP-01 failures with 403 often mean the challenge token path is reachable but denied by WAF rules, basic auth on /.well-known, or a CMS plugin blocking dot-directories. Capture the full URL the CA requested from logs, curl it from an external host without cookies, and compare status codes to what Boulder sees. Fixing 403 is authorization and routing, not buying a wildcard cert to skip validation. Triage should capture timestamps, account URLs, and the last Boulder error type before infrastructure changes widen blast radius.

See also ACME DNS-01 challenge propagation delays and acme.sh renewal failures and logs.

Why renewal failed

Boulder performs an unauthenticated HTTP GET on port 80. Any WAF, IP allowlist, or CMS rule that returns 403 prevents issuance even when / returns 200. Map the challenge URL to the exact nginx server_name and Apache VirtualHost that answers default traffic—certificate users type www. while HTTP-01 uses the identifier on the order, which may be apex-only. Challenge path policy should live in version control beside firewall rules so diffs show accidental lockdowns.

Reverse proxies must forward the challenge path to the same document root certbot uses. A second vhost catching default_server can swallow challenges. If you terminate HTTP at a CDN, remember Boulder does not send your origin cookies; per-session WAF scores can still block the user-agent string classified as bot traffic. Security reviews need an explicit HTTP-01 exception documented with business owner sign-off.

CDNs may block automated clients. Temporary grey-cloud or a dedicated validation hostname reduces 403 noise during renewal week. Treat temporary IP allowlists for Boulder netblocks as a last resort; fixing webroot routing scales better.

The ACME server performs an HTTP GET on http://token-host/.well-known/acme-challenge/key-auth without session cookies. A 403 means the TCP connection succeeded and something actively refused the token response. Misconfigured location blocks, Require all denied, or CDN security levels can all return 403 while marketing pages on / still return 200. Clarify in tickets whether failure is validation, issuance, deploy, or listener presentment—teams too often mix these phases when reading logs.

ACME HTTP-01 403 is not cured by renewing DNS nameservers alone when validation type is HTTP. It is not identical to TLS certificate expiry on port 443. It is not fixed by reloading nginx until the challenge location is allowed. It is not proof Let's Encrypt is down if external curl also returns 403.

Common causes

These patterns drive most acme challenge failed 403 tickets. They explain why automation logs look green while browsers or APIs still break.

Nginx location ^~ /.well-known/acme-challenge/ accidentally inherited auth_basic from the parent server block. Apache RewriteRule sent challenge URLs to WordPress, which answered 403 for unknown paths. Cloudflare Bot Fight Mode blocked the bare UA Boulder uses.

Certbot wrote tokens under /var/www/html but the active vhost uses a different document root on another path. A reverse proxy terminated HTTP and forwarded only /api. Kubernetes ingress routed HTTP-01 traffic to a default backend that denies all. Application firewalls terminating HTTP inspect /.well-known with the same OWASP CRS rules as login forms.

IPv6 AAAA records pointed at a host that still enforced an old fail2ban rule on Let us Encrypt netblocks. Rate-based WAF rules treated repeated challenge GETs as abusive. Staging and production shared an IP but only production vhost served the token file.

Diagnose

Hit the logged challenge URL from a host on the public internet without cookies or Authorization headers.

curl -v "http://example.com/.well-known/acme-challenge/TOKEN_FROM_LOG"

From an external VPS, run curl against the exact challenge URL logged in certbot or cert-manager events. Note redirects: Boulder follows only limited redirects and may fail if the chain lands on HTTPS with the wrong vhost. Match Server header and document root to the process that wrote the token. Record Server and Via headers from external curl; mismatches reveal hidden proxies returning 403 before traffic reaches the intended webroot.

Fix step-by-step

Follow this sequence for acme http-01 challenge failed 403. Capture command output after each step so the next responder can pick up without rerunning destructive changes.

  1. Copy the challenge URL from certbot or cert-manager events.
  2. curl it from an external VPS; expect 200 and key authorization body.
  3. Exempt /.well-known/acme-challenge/ from auth, hotlink protection, and Bot Fight.
  4. Align nginx or Apache docroot with certbot -w webroot.
  5. Ensure orange-cloud hosts allow HTTP-01 or move renewal to DNS-01.
  6. Re-run dry-run renewal.
  7. After success, verify HTTPS presents a new serial externally.
  8. Add IaC for the well-known location so rebuilds keep the exemption.
  9. Archive redacted ACME logs and openssl transcripts in the ticket before closing.
  10. Schedule a follow-up dry-run before the next natural renewal window.

Verify the live certificate

OpenSSL

echo | openssl s_client -connect HOST:443 -servername HOST 2>/dev/null | openssl x509 -noout -dates -subject -issuer

Replace HOST with the public hostname on the certificate.

How to read the output

HTTP-01 fails before a new cert exists, so openssl may still show the old or an expired leaf. After fixing 403 and reissuing, validate notAfter with openssl against the public hostname. During 403 triage, prioritize curl -v http://HOST/.well-known/acme-challenge/TOKEN from outside the datacenter.

curl

curl -vI --connect-timeout 10 https://HOST/ 2>&1 | grep -Ei 'expire|SSL certificate|subject:|issuer:'

A 403 here usually matches what the CA saw—fix server config before reissuing.

Roll out and verify

Add or restore a dedicated webroot mapping for /.well-known/acme-challenge/ on every listener that terminates HTTP for the name on the certificate. Disable auth and hotlink protection on that path only. If using CDN orange-cloud, allow HTTP-01 or switch those names to DNS-01.

Place the token file certbot generates, or configure the standalone authenticator on port 80 during maintenance. Purge CDN cache for the challenge path. Re-run certbot certonly --webroot -w /path -d host or the equivalent ingress shim. Temporarily lower CDN security level on maintenance windows only after change approval, then restore settings post-issuance.

Confirm HTTP GET returns 200 with the key authorization body from three external networks. Complete issuance and verify HTTPS presents the new chain. Encode the webroot path in infrastructure as code so rebuilds keep the exemption.

What to automate

Synthetic HTTP checks on a throwaway token path catch 403 regressions when WAF rules change. Synthetic GETs against random token paths catch new WAF regressions before Boulder does.

ssldays live TLS monitoring proves the cert issued; add weekly HTTP-01 probes if you stay on port 80 validation. ssldays monitors resulting HTTPS certificates once HTTP validation succeeds again.

Frequently asked questions

Can I use HTTPS-only sites?

HTTP-01 needs port 80 reachable for each name. TLS-ALPN-01 or DNS-01 are alternatives if you cannot open port 80. Note whether failure is 403 versus 404 before changing DNS.

Does Kubernetes ingress break HTTP-01?

Only if ingress routes /.well-known to the wrong backend or forces HTTPS redirect before the challenge is served. Note whether failure is 403 versus 404 before changing DNS.

Will rate limits reset after fixing 403?

Failed validations count toward limits; fix the root cause on staging, then retry production calmly. Note whether failure is 403 versus 404 before changing DNS.

Can I whitelist only Let's Encrypt IPs?

Temporary allowlists are brittle because Boulder does not publish a single static IP list for HTTP-01. Fixing routing and authorization is durable. If you must use IP rules, automate updates from published ACME validation documentation and remove them after issuance to avoid permanent holes.

Monitor this hostname on a schedule

Live certificate checks and renewal reminders — not guesswork from PEM files.