Linux  ·  critical  ·  Web, PHP & databases

Certificate renewal fails and the site goes to expired HTTPS

The renewal ran and the validation failed, so the old certificate stayed until it expired.

What you see

Browsers refuse the site with a date error. The renewal timer has been running for weeks with failures nobody read.

What is actually wrong

The HTTP challenge path is being redirected or blocked, the DNS record has changed, the web root in the renewal configuration no longer matches the site, or a rate limit has been reached after repeated failures.

Codes and articles

certbot renew failedurn:ietf:params:acme:error:unauthorizedChallenge failedNET::ERR_CERT_DATE_INVALIDtoo many certificates

Fixes (3)

Repair the HTTP challenge path
Root shell30 minutesmedium riskreversible

HTTP-01 validation. The challenge file has to be reachable over plain HTTP from the internet.

  1. Do a dry run — it tests everything without consuming a rate limit.

    Shell
    sudo certbot renew --dry-run

    The dry run against the staging environment is the correct way to debug this. Repeatedly retrying against production is how a domain hits the failed-validation rate limit and is locked out for an hour on top of everything else.

  2. Read the renewal configuration to see what it believes.

    Shell
    sudo cat /etc/letsencrypt/renewal/example.com.conf
  3. Test that the challenge path is actually served over HTTP with no redirect to HTTPS.

    Shell
    sudo mkdir -p /var/www/html/.well-known/acme-challengeecho ok | sudo tee /var/www/html/.well-known/acme-challenge/testcurl -sIL http://example.com/.well-known/acme-challenge/test | head -12
  4. A redirect to HTTPS is fine as long as it follows through; a redirect to a broken HTTPS is not. Add an explicit exception in nginx so the challenge is always served over plain HTTP.

    Shell
    printf 'location ^~ /.well-known/acme-challenge/ {\n  root /var/www/html;\n  default_type "text/plain";\n  allow all;\n}\n' | sudo tee /etc/nginx/snippets/acme.confsudo nginx -t && sudo systemctl reload nginx
  5. Confirm DNS points here and that port 80 is open — Let's Encrypt validates from the internet, not from the machine.

    Shell
    dig +short example.comcurl -sI http://example.com/ | head -3
Confirm it workedThe dry run succeeds, then run the real renewal.
Shell
sudo certbot renew --dry-run && sudo certbot renew && sudo systemctl reload nginx
If you need to undo itThe existing certificate is untouched until a renewal succeeds.
Repair DNS-01 validation
Root shell30 minutesmedium riskreversible

DNS-01, which is what wildcard certificates require.

  1. Run the dry run and read which record it could not set or find.

    Shell
    sudo certbot renew --dry-run 2>&1 | tail -30
  2. Check the API credentials for the DNS plugin are still valid — an expired token is the usual cause and the error is not always clear.

    Shell
    sudo ls -l /etc/letsencrypt/*.inisudo chmod 600 /etc/letsencrypt/*.ini
  3. Confirm the TXT record can be seen from outside after a manual attempt.

    Shell
    dig +short TXT _acme-challenge.example.com @1.1.1.1

    Querying a public resolver rather than the local one is the point. The record can exist on the authoritative server and not yet be visible where Let's Encrypt will look, which is a propagation delay rather than a failure.

  4. Increase the propagation wait if the provider is slow.

    Shell
    sudo certbot renew --dns-cloudflare-propagation-seconds 60 --dry-run
  5. Check the CAA record permits the issuer — a CAA record naming a different authority blocks issuance entirely.

    Shell
    dig +short CAA example.com
Confirm it workedThe dry run completes and the real renewal issues a certificate with a new expiry.
Shell
sudo certbot certificates
If you need to undo itNothing is replaced until issuance succeeds.
Make sure renewal is running and being watched
Root shell20 minuteslow riskreversible

After fixing it. This is what stops it happening again.

  1. Check the timer is enabled and when it last ran.

    Shell
    systemctl list-timers certbot* snap.certbot* --allsudo systemctl status certbot.timer --no-pager
  2. Add a reload hook so a renewed certificate is actually served — a renewal that does not reload nginx leaves the old certificate in memory.

    Shell
    printf '#!/bin/sh\nsystemctl reload nginx\n' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shsudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

    This is a genuinely common cause of an expired certificate on a machine where renewal has been succeeding for months. The file on disk is current and the running process is serving one from ninety days ago.

  3. Add monitoring that checks the expiry from outside, so a failure is noticed before the browser notices it.

    Shell
    echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate
  4. Register an email address with the ACME account so expiry warnings arrive.

    Shell
    sudo certbot update_account --email admin@example.com
Confirm it workedThe timer is active, and the served certificate's expiry matches the file on disk.
Shell
sudo certbot certificates | grep -A2 'Certificate Name'
If you need to undo itRemove the deploy hook script to revert.

Where this stops. This write-up was written and checked by hand. It says what each step changes, how to confirm it worked and how to reverse it, and anything destructive is flagged before you reach it. If it does not match what your machine is doing, search the Support Centre for the exact code or message — and when something needs a person, get in touch.