Security

Automatic Let's Encrypt Certificate Renewal and Troubleshooting Errors

If automatic Let's Encrypt renewal fails, the site won't load due to an expired certificate error. Here you'll see the real causes of renewal failure and how to fix them.

Security

The site loads, but the browser shows a red page and says the certificate has expired. If everything was fine until yesterday and no one touched the server manually, automatic Let's Encrypt renewal has probably failed and you only noticed a few hours after expiration. This article is about that exact moment: why automatic renewal doesn't work, how the DNS challenge for a wildcard certificate is configured, and what you should check today.

First, find out whether renewal actually ran

Before making any changes, read the log. Certbot usually writes to /var/log/letsencrypt/letsencrypt.log, and the real error is there, not in the cron output.

grep -i "error\|failed\|challenge" /var/log/letsencrypt/letsencrypt.log | tail -40

If you see Timeout during connect or Invalid response from http://example.com/.well-known/acme-challenge/..., the problem is HTTP access. If you see DNS problem: NXDOMAIN, the problem is a DNS record. These are two completely different paths, and confusing them wastes a lot of time.

A dry run is also useful before anything else:

certbot renew --dry-run

This command doesn't issue any certificate; it just tests the challenge path all the way through. If the dry-run passes but the site is still expired, the problem is in the cron schedule, not the configuration.

The most common cause of Let's Encrypt renewal failure

In my experience, most failures have one cause: the web server on port 80 doesn't respond to the HTTP-01 challenge. Let's Encrypt makes a request from the outside to http://your-domain/.well-known/acme-challenge/<token> and expects a specific string. If this path is redirected to HTTPS, or the firewall has port 80 closed, or there's a reverse proxy in front of the server that doesn't recognize this path, the challenge fails.

Check three cases separately:

  • A forced HTTP-to-HTTPS redirect in nginx or Apache that also swallows the /.well-known/acme-challenge/ path.
  • A firewall or cloud Security Group that has closed port 80 from the outside while 443 is open.
  • A CDN or proxy in front of the site that doesn't forward the challenge request to the origin.

The standard solution is to exclude the challenge path from the redirect. In nginx, a location before the general redirect:

location ^~ /.well-known/acme-challenge/ {
    root /var/www/html;
    default_type "text/plain";
    allow all;
}

In Apache, the equivalent is written with RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/ before the redirect rule. If you're behind a CDN, either exclude the path from cache and proxy, or migrate to the DNS challenge entirely.

This is where people go wrong

The common mistake is that after seeing the error, the site admin manually runs certbot renew --force-renewal and reissues the certificate. The site comes back up, everyone is happy, and three months later exactly the same thing happens. Because the root cause — the cron job or systemd timer — was never fixed. The telltale sign is that in the log, the dates of successful renewals are always recorded a few hours from expiration, not within the thirty-day window before it. If renewals have always happened at the last minute, it means your automation isn't working and you're just manually rescuing it.

The DNS challenge for a wildcard certificate

A wildcard certificate like *.example.com can't be issued with HTTP-01. You have to use DNS-01, meaning you create a TXT record named _acme-challenge.example.com whose value is the token generated by the ACME client. This method has an important advantage: port 80 doesn't need to be open, and it works for servers that are internal-only.

The record structure looks like this:

_acme-challenge.example.com. 300 IN TXT "gfj9Xq...Rg85nM"

The point that trips up a lot of people is TTL. If you set TTL to 86400 and later want to change the token, resolvers may cache the old version for up to a day and validation will fail. For the challenge record, set TTL to 60 or 120 seconds and delete it after issuance.

For automation, use DNS plugins. For example, with Cloudflare:

certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cf.ini \
  -d example.com -d "*.example.com"

The credentials file must have 600 permissions. If you leave this file with the default 644 permissions, any user on the server can read your API token and change the domain's DNS records. This is one of those places where a small mistake in file permissions leads to a complete domain takeover.

Hooks and service reload

The certificate renews but the web server doesn't re-read it. The result is that the file on disk has a new date but the browser still sees the old certificate. You must reload the service after every renewal:

certbot renew --deploy-hook "systemctl reload nginx"

For services like HAProxy or Postfix that keep the certificate in memory, reload isn't enough and you have to restart. There's a real trade-off here: restart drops active connections, so if you have heavy traffic, prefer reload and only restart for services that truly need it.

Checking certificate health from the outside

From inside the server everything looks healthy, but not from the user's point of view. Always check from the outside:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject

Compare the notAfter output with today's date. If less than 15 days remain and automatic renewal is enabled, your automation has failed and you need to look at the log. To check DNS records and the challenge path, DNS and network checking tools make the job quick and you don't need to test from another server.

One note about the certificate chain: if your server doesn't send the intermediate certificate in the TLS response, some browsers and most mobile clients will throw a chain error, even when the main certificate is valid. Use the fullchain file, not cert alone.

Set up automation correctly

Certbot has had a systemd timer since version 0.21, and manual cron is no longer needed. If both cron and the timer are active, two instances run at the same time and you may hit the Let's Encrypt rate limit. Disable one:

systemctl list-timers | grep certbot
systemctl status certbot.timer

If the timer is active, remove the cron line from /etc/cron.d/certbot. The Let's Encrypt rate limit for a repeated domain is strict, and if you fail several times in a row, you'll be locked out for several days. This is exactly the moment when a site admin turns to a commercial certificate, when the problem was just a duplicate timer.

For high-traffic environments with several servers behind a load balancer, manage the certificate on one server and distribute the file to the rest with a tool like Ansible or rsync. Issuing the certificate separately on each server both brings you closer to the rate limit and is a management nightmare. If you're looking for more security layers on this same infrastructure, ServerNet's security services cover complementary options like managed firewall and monitoring.

And one practical warning: take certificate expiration monitoring seriously. A simple script that checks notAfter every morning and alerts if less than 20 days remain will save you from 2 a.m. and the red page. If you also keep system logs intact and citable, during incident review you can say exactly when and why renewal failed.

Frequently asked questions

Why has my Let's Encrypt certificate expired despite automatic renewal?

Because automatic renewal didn't actually run or failed during execution. The most common reason is port 80 being closed or the /.well-known/acme-challenge/ path being redirected to HTTPS. Check the /var/log/letsencrypt/letsencrypt.log log to find the exact error.

The second reason is the systemd timer or cron being disabled. Check the status with systemctl status certbot.timer.

How does the DNS challenge for a wildcard certificate work?

In this method, you create a TXT record named _acme-challenge.example.com whose value is the token generated by the ACME client. Let's Encrypt reads this record via DNS, and if it matches, it issues the certificate.

Its advantage is that port 80 doesn't need to be open and it works for internal servers too. Just set a short TTL so token changes propagate quickly.

Should I use HTTP-01 or DNS-01?

If you only have a simple domain and port 80 is open, HTTP-01 is simpler and requires less configuration. If you want a wildcard certificate, the server is behind a firewall, or you use a CDN, DNS-01 is the right choice.

I prefer DNS-01 for anything with more than one subdomain, because it's configured once and after that works independently of port and proxy status.

After renewing the certificate, why does the browser still show the old certificate?

Because the web server hasn't re-read the new certificate. You must reload the service after renewal. With --deploy-hook "systemctl reload nginx" this becomes automatic.

If you use HAProxy or Postfix, reload isn't enough and you have to restart. Note that restart drops active connections.

ServerNet Support

ServerNet engineering & editorial team — specialists in infrastructure, networking and web hosting.

Security Services
Share:

Comments 0

No comments yet — be the first!

Leave a comment

Related service

Security Services

Penetration testing by OSCP-certified specialists, infrastructure hardening and 24/7 security monitoring — reports managers understand and engineers can act on.