# letsencrypt certificate renewal failed: certbot Challenge failed (unauthorized)

## TL;DR

A Certbot renewal almost never fails because the certificate itself is broken. It fails because LetsEncrypt could not validate control of the domain this time: DNS moved, a bad AAAA record sends validators to a broken IPv6 path, port 80 got firewalled, a CDN or proxy started answering the challenge, or the DNS plugin credential expired. Run a dry-run renewal first, read the exact Type and Detail lines in the error, fix only that validation path, then renew for real and confirm your web server actually reloaded with the new certificate.

## Verbatim error

```text
Attempting to renew cert (example.com) from /etc/letsencrypt/renewal/example.com.conf produced an unexpected error: Challenge failed for domain example.com. Skipping.
All renewal attempts failed. The following certs could not be renewed:
  /etc/letsencrypt/live/example.com/fullchain.pem (failure)

Domain: example.com
Type:   unauthorized
Detail: 203.0.113.10: Invalid response from http://example.com/.well-known/acme-challenge/CHALLENGE-FILE: 404
```

The two lines that decide the whole fix are Type and Detail. unauthorized, dns, timeout, connection, tls, and rateLimited each have a different fix, and guessing wastes your failed-authorization budget.

## Steps

### 1. Reproduce against staging and capture the exact error type

A dry run exercises the full renewal against the LetsEncrypt staging environment, so it does not touch the live certificate and does not burn production rate limits the way repeated real renewals do.

```bash
sudo certbot certificates
sudo certbot renew --dry-run --cert-name example.com
sudo tail -n 100 /var/log/letsencrypt/letsencrypt.log
```

Success check: the dry run names one certificate and one ACME error Type (unauthorized, dns, timeout, connection, tls, or rateLimited). Write that Type down before changing anything. If the dry run succeeds but the real renewal failed earlier, suspect the deploy step instead (step 6) or a transient production rate limit (step 5).

### 2. Fix unauthorized with Invalid response 404 or 403 (HTTP-01)

This means LetsEncrypt reached a web server, but not the challenge file Certbot expected it to find. Work the list in order; the first three cause most cases.

```bash
dig +short A example.com
dig +short AAAA example.com
curl -I http://example.com/.well-known/acme-challenge/test.txt
```

1. A and AAAA must both point at this server. A stale AAAA record is the classic trap: if the domain publishes IPv6 but the IPv6 listener, firewall, or vhost is broken, validators preferring IPv6 fail while your own IPv4 tests pass. Fix the IPv6 serving, or remove the AAAA record until it works.
2. Port 80 must be open inbound. HTTP-01 validation arrives on port 80 even for sites that normally force HTTPS, and LetsEncrypt validates from multiple vantage points, so geo-blocking or an allowlist that only admits your own country can still fail it.
3. The webroot must actually serve the challenge path. With the webroot plugin, files under the configured webroot at .well-known/acme-challenge must be served by the matching vhost, not redirected to a different host, not behind authentication, and not rewritten by the application. A 404 in Detail means the request landed on a vhost or root that does not have the file; a 403 usually means a deny rule, a WAF, or a CDN answering instead.
4. If a CDN or reverse proxy sits in front, it must pass the challenge path through to the origin untouched, or you should switch this certificate to DNS-01 (step 3).

Success check: the curl above, run from outside the server if possible, returns the test file or reaches this server vhost, and a fresh dry run no longer reports unauthorized.

### 3. Fix dns errors and DNS-01 credential failures

A Type of dns, or Detail text about no valid A records, SERVFAIL, or DNS problem, means validation never got a usable answer from DNS.

```bash
dig +short NS example.com
dig A example.com @1.1.1.1
dig CAA example.com
dig +short TXT _acme-challenge.example.com
```

1. The A and AAAA records must exist at the authoritative nameservers shown by the NS lookup, not just in a registrar panel or an old DNS host the domain no longer delegates to. After a DNS move, wait for propagation and re-check against a public resolver before retrying.
2. Check CAA records. If any CAA record exists, it must permit LetsEncrypt to issue; a restrictive CAA left over from a previous CA blocks issuance outright.
3. For DNS-01 renewals (required for wildcard certificates), the usual failure is the plugin credential, not DNS itself: the provider API credential was rotated, expired, lacks permission for this zone, or the credentials file permissions tightened so the root-run renewal cannot read it. Confirm the renewal configuration names the right plugin and credentials file, and that the credential can edit TXT records in this exact zone.
4. If the plugin creates the TXT record but validation still fails, propagation is too slow for the default wait. Increase the plugin propagation delay (for example the dns-cloudflare style propagation-seconds option) to 60 or 120 seconds and retry.

Success check: during a dry run, the TXT query above shows the challenge record while Certbot waits, and the dry run completes for this certificate.

### 4. Fix timeout and connection errors

Timeout during connect or connection refused means the validator could not open a connection at all, which is a network problem, not a certificate problem. Check that the host is publicly reachable on port 80 for HTTP-01, that the firewall and any cloud security group admit inbound 80 from anywhere (validation sources are not a fixed IP list you can allowlist), and that a published AAAA address actually answers; a dead IPv6 route times out exactly like this. If the server only recently changed IP addresses, confirm DNS is not still sending validators to the old address.

Success check: from an external network, a plain HTTP request to the domain connects quickly, and the dry run moves past the connection stage.

### 5. Stop and wait out rateLimited instead of retrying

Repeated failed renewals consume a small failed-authorization budget (per account, per hostname, per hour), and too many certificates for the same domain in a week can block even a correct renewal. If Detail says rateLimited, do not keep retrying and do not switch to force-renewal as a workaround; every extra failure extends the pain. Do all further testing with dry runs against staging, fix the underlying validation problem there, and only run the production renewal once the dry run passes and the limit window has cleared.

Success check: staging dry runs pass consistently, so the single production attempt is spent on a configuration already proven to work.

### 6. Renew for real and prove the server is serving the new certificate

```bash
sudo certbot renew --cert-name example.com
echo | openssl s_client -connect example.com:443 -servername example.com | openssl x509 -noout -dates
```

Success check: Certbot reports the certificate renewed, and the dates printed from the live TLS connection show the new expiry, roughly 90 days out. If the file on disk renewed but the served dates did not change, your web server never reloaded: check for a deploy hook or reload step (nginx or apache reload after renewal) and run it, because a renewed certificate that is never loaded still expires in production.

### 7. Fix the automation so the next renewal does not fail the same way

```bash
systemctl list-timers | grep -i certbot
sudo grep -E "authenticator|webroot_path|server" /etc/letsencrypt/renewal/example.com.conf
```

Success check: a certbot timer or cron entry exists and is enabled, and the renewal configuration uses an unattended-capable authenticator (webroot, nginx, apache, standalone, or a DNS plugin). If the configuration says the manual authenticator, automatic renewal can never work unattended: manual certificates require a person or an auth hook script for every renewal. Reissue that certificate once with an automated authenticator or add manual auth and cleanup hooks, then dry-run again.

## When to use

Use this skill when certbot renew or an unattended timer reports a LetsEncrypt certificate renewal failed, when output says Challenge failed, All renewal attempts failed, or 1 renew failure(s), and when the error Type is unauthorized, dns, timeout, connection, tls, or rateLimited for a certificate that issued successfully before.

## When not to use

Not for a first-time issuance failure on a domain that has never had a certificate (start with challenge selection and initial issuance instead), not for a certificate that already expired because no renewal was ever attempted (that is a renewal-automation setup problem), not for client-side trust errors in an application calling an HTTPS API (that is a CA bundle problem on the client), and not for non-LetsEncrypt CAs or non-Certbot ACME clients, although the challenge diagnosis transfers in spirit.

### Variant: unauthorized with 403 instead of 404

Treat 403 as something actively refusing: a deny rule on the challenge directory, a WAF or bot filter, basic authentication on the vhost, or a CDN answering in place of the origin. Test the challenge URL from outside and remove or bypass the refusal for that path only.

### Variant: renewal worked until the site moved behind a CDN

The CDN now terminates HTTP and may serve its own response for the challenge path, or may redirect in a way the validator does not follow for this vhost. Either make the CDN pass the challenge path to the origin, use the CDN own certificate product, or move this certificate to DNS-01 validation which never touches the web server.

### Variant: only one domain on a multi-domain certificate fails

Certbot renews the whole lineage together, so one dead subdomain in the certificate (a decommissioned host still listed in the renewal configuration) fails the entire renewal. Remove the dead name by reissuing the certificate with only the live domains, then dry-run.

### Variant: dry run passes, real renewal fails

Staging and production are different environments with different limits. Suspect a production rate limit from earlier failed attempts (step 5), or a deploy hook that staging does not run the same way. Check the log timestamps and the hook output before touching DNS or firewall rules that staging just proved correct.

## Why it happens

LetsEncrypt certificates are short-lived, and every renewal re-validates domain control from scratch using the challenge type stored in the renewal configuration. Nothing about a successful issuance three months ago guarantees validation still works today: DNS delegations move, AAAA records get published before IPv6 serving is ready, firewalls and CDNs change, provider credentials rotate, and subdomains get decommissioned. The renewal is simply the first automated check that notices the drift, which is why the error names a validation Type and Detail rather than a certificate defect.

## Edge cases

- Certbot only attempts renewal when the certificate is near expiry by default, so a broken setup can sit silent for weeks; the dry run is how you surface it early.
- Force-renewal does not bypass validation or rate limits; it only skips the near-expiry check, and using it while validation is still broken just burns budget faster.
- A certificate covering both the apex and www names fails if either name fails validation; test both names in steps 2 and 4.
- Clock skew on the server breaks TLS and ACME account operations in confusing ways; if errors mention certificate time validity on connections to the ACME server itself, check the system clock first.
- Snap, distro package, and pip installs of Certbot store configuration in the same conventional locations, but plugin availability differs; a renewal configuration naming a DNS plugin that is no longer installed fails before validation starts, which the log shows as a plugin error rather than a challenge error.

## Compatibility

Certbot 1.x and 2.x on Linux with nginx, apache, webroot, standalone, or DNS plugin authenticators; LetsEncrypt production and staging ACME v2 endpoints. The challenge diagnosis also applies to other ACME clients (acme.sh, Caddy, Traefik) with their own command syntax.
