Back to tutorials
Tutorial

TLS certificate renewal troubleshooting tutorial (2026): Fix failed renewals, ACME challenges, and broken chains on a VPS

TLS certificate renewal troubleshooting tutorial for VPS: diagnose ACME errors, fix challenge failures, and verify full chain.

By Anurag Singh
Updated on Jul 17, 2026
Category: Tutorial
Share article
TLS certificate renewal troubleshooting tutorial (2026): Fix failed renewals, ACME challenges, and broken chains on a VPS

Your TLS setup looks fine—until renewal night. Then browsers show warnings and API clients fail handshakes. You check the clock and realize the certificate never renewed at 02:00.

This TLS certificate renewal troubleshooting tutorial is a practical runbook. It helps you pinpoint the exact failure (DNS, HTTP routing, firewall, rate limits, chain files, or cron/systemd timers). It also shows how to fix it with evidence, not guesses.

The workflow assumes a Linux VPS (Ubuntu 24.04/26.04 LTS or Debian 12/13) using Let’s Encrypt/ACME. The same checks apply to other CAs and most stacks (Nginx, Apache, LiteSpeed, or a reverse proxy in front of a control panel).

What you’ll check (and why renewals fail)

  • ACME challenge path is blocked or misrouted (HTTP-01), or your DNS provider isn’t publishing TXT records fast enough (DNS-01).
  • Port 80/443 reachability breaks after firewall changes, security tooling updates, or a new reverse proxy is added.
  • Wrong server answers because DNS points elsewhere, or you’re behind a load balancer and only one node renews.
  • Renewal succeeds but deployment fails because a reload hook errors out or your config references the wrong cert paths.
  • Chain/bundle issues (missing intermediate) cause browsers or strict clients to reject an otherwise “valid” cert.

Prerequisites and safe staging

You’ll need SSH access and sudo. If SSH access is still loose, tighten it first. Avoid last-minute changes from an insecure login. Use this guide for a clean baseline: SSH key setup guide tutorial (2026).

Two safety rules before you change anything:

  • Don’t delete /etc/letsencrypt unless you fully understand the impact. It contains account keys and renewal configs.
  • Test in staging first so you don’t burn production rate limits while debugging.

Confirm which ACME client you’re using

Many VPS setups use Certbot. Others use acme.sh or a control panel’s AutoSSL. First, confirm what’s actually running.

which certbot || true
which acme.sh || true
systemctl list-timers | grep -E 'certbot|acme' || true
ls -1 /etc/letsencrypt/renewal 2>/dev/null | head

If you’re on cPanel/WHM, renewals typically run through AutoSSL instead of Certbot. In that world, you’ll usually troubleshoot DCV and deployment inside WHM. Keep this open: cPanel AutoSSL troubleshooting tutorial (2026).

TLS certificate renewal troubleshooting tutorial: reproduce the failure on demand

Don’t wait for the next nightly run. Trigger a dry-run renewal now. That turns the problem into a specific error message you can act on.

Certbot dry-run (recommended first step)

sudo certbot renew --dry-run

If this passes but your production cert still expires, focus on timers, hooks, and deployment. Don’t start with ACME validation.

Force a renewal for one name (staging)

Use staging while you iterate:

sudo certbot certonly --staging -d example.com -d www.example.com --preferred-challenges http

Typical failures and what they usually mean:

  • Timeout during connect → firewall, wrong IP, or port 80 closed.
  • Invalid response → the wrong vhost/app is serving the challenge URL.
  • NXDOMAIN / SERVFAIL → broken DNS records or resolver issues.
  • 403/404 → rewrite rules, app routing, or permissions are blocking /.well-known/.

Step 1: verify DNS points to the server you’re actually fixing

Many “renewal issues” are just DNS drift. For example, www may still point to an old host. Or the apex may now go through a CDN.

Confirm the basics before touching Nginx/Apache.

domain=example.com

# What the public internet sees
dig +short A $domain
dig +short AAAA $domain

dig +short A www.$domain

dig +short CNAME www.$domain

Then compare those results to your VPS public IP:

curl -4s https://ifconfig.co; echo
curl -6s https://ifconfig.co 2>/dev/null; echo

If you’re in the middle of a migration or cutover, keep it controlled and reversible. This guide walks through staging, cutover, and rollback planning: web hosting migration tutorial (2026).

If DNS is wrong, fix it first (A/AAAA, proxy/CDN mode, or delegation). If you run your own nameservers, double-check glue and delegation: cPanel nameserver setup guide tutorial (2026).

Step 2: confirm ports 80 and 443 are reachable externally

HTTP-01 validation typically requires port 80 from the public internet. Some clients can use TLS-ALPN-01 on 443. Don’t assume that’s what you have configured.

Let the error message tell you which challenge method is failing.

Check listening sockets on the VPS

sudo ss -lntp | awk 'NR==1 || $4 ~ /:80$|:443$/'

Check firewall rules quickly

Ubuntu/Debian with UFW:

sudo ufw status verbose

RHEL/AlmaLinux/Rocky with firewalld:

sudo firewall-cmd --list-all

If you need a clean, methodical flow for proving what’s blocked (and where), use: firewall troubleshooting tutorial (2026).

External probe from a different network

From your laptop (or another machine not on the same network), run:

curl -I http://example.com/.well-known/acme-challenge/ping
curl -I https://example.com/

You’re not validating page content here. You’re confirming you hit the expected server and get a consistent response.

Step 3: fix HTTP-01 challenge routing (Nginx and Apache patterns)

Renewals often break after a “harmless” redirect or rewrite change. Apps that force HTTP→HTTPS, add language prefixes, or route everything through an index file can swallow /.well-known/acme-challenge/.

That path must stay reachable.

Nginx: allow the challenge path explicitly

Edit the server block for port 80 (or whichever block handles the domain). Add a dedicated location for the challenge:

# /etc/nginx/sites-available/example.com
server {
  listen 80;
  server_name example.com www.example.com;

  location ^~ /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
    default_type "text/plain";
    try_files $uri =404;
  }

  # Keep your normal redirects below
  location / {
    return 301 https://$host$request_uri;
  }
}

Create the webroot and a test file:

sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
printf 'ok' | sudo tee /var/www/letsencrypt/.well-known/acme-challenge/ping
sudo nginx -t && sudo systemctl reload nginx
curl -s http://example.com/.well-known/acme-challenge/ping; echo

Apache: ensure Alias bypasses rewrites

On Apache, use an Alias plus a directory stanza. This prevents rewrites from intercepting the challenge request:

# /etc/apache2/sites-available/example.com.conf
Alias /.well-known/acme-challenge/ "/var/www/letsencrypt/.well-known/acme-challenge/"

<Directory "/var/www/letsencrypt/.well-known/acme-challenge/">
  Options None
  AllowOverride None
  Require all granted
</Directory>

Reload and test:

sudo apachectl configtest && sudo systemctl reload apache2
curl -s http://example.com/.well-known/acme-challenge/ping; echo

Common pitfall: reverse proxy or CDN intercepts the challenge

If traffic flows through a proxy/CDN, make sure it forwards the challenge path unchanged. With Nginx in front of Apache/cPanel, one misordered location block can send the challenge to the wrong backend.

If you’re running a proxy layer, verify routing order and vhost matching: reverse proxy setup guide tutorial (2026).

Step 4: troubleshoot DNS-01 challenges (slow propagation, wrong TXT, split-horizon DNS)

DNS-01 is ideal for wildcard certificates and locked-down servers. It also fails in a specific way: the correct TXT record must be visible publicly.

Don’t treat your DNS provider UI as proof. Query the authoritative nameservers.

Find authoritative nameservers and query them directly

domain=example.com
challenge_host=_acme-challenge.$domain

dig +short NS $domain
# Pick one NS and query it explicitly:
ns1=$(dig +short NS $domain | head -n1)
dig @$ns1 +short TXT $challenge_host

If you run BIND9 on your VPS, stale zones or signing mistakes can break TXT validation. DNSSEC misconfiguration can also show up as SERVFAIL.

If DNSSEC is in the mix, keep this handy: BIND9 DNSSEC setup guide tutorial (2026).

Fix split-horizon DNS during renewals

If internal DNS differs from public DNS, your server may resolve one address while Let’s Encrypt resolves another. The usual symptom is that it “works on the LAN” but fails at the CA.

Resolve from multiple perspectives:

getent ahosts example.com
resolvectl query example.com 2>/dev/null || true

For DNS-01 renewals, API-based plugins or acme.sh DNS integrations are the safest option. Manual TXT updates work for one-off issuance. They often fail quietly on automated renewals.

Step 5: diagnose “renew succeeded, but site still shows old cert”

This isn’t an issuance problem. The files on disk updated, but the service didn’t reload. Another common cause is that your config points somewhere else.

Also consider SNI: clients may be landing on a different vhost than you expect.

Check the live certificate presented to clients

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

Now compare that output to the certificate file you expect the server to be using.

Find configured cert paths

Nginx:

sudo nginx -T 2>/dev/null | grep -nE 'server_name example.com|ssl_certificate ' | head -n 40

Apache:

sudo apachectl -S 2>/dev/null | grep -n "example.com" -n
sudo grep -R "SSLCertificateFile" -n /etc/apache2/sites-enabled | head

Reload the right service (and verify the hook)

Certbot usually reloads Nginx/Apache via hooks. Those hooks can break after upgrades, service renames, or config cleanup.

sudo systemctl reload nginx 2>/dev/null || sudo systemctl reload apache2
sudo systemctl status nginx --no-pager -l 2>/dev/null || true

If your renew command uses a deploy hook, inspect it directly:

sudo ls -la /etc/letsencrypt/renewal-hooks/deploy 2>/dev/null || true
sudo grep -R "reload" -n /etc/letsencrypt/renewal-hooks 2>/dev/null || true

Step 6: fix broken chain / intermediate problems (the “valid but rejected” scenario)

Browsers often mask chain issues. Corporate proxies, Java clients, and strict SMTP/IMAP stacks often won’t.

This shows up a lot after someone copies cert.pem instead of fullchain.pem into a server config.

Quick chain verification

# On the server
sudo openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /etc/letsencrypt/live/example.com/fullchain.pem

Correct file choices (common defaults)

  • Nginx typically uses ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  • Nginx key: ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  • Apache often uses SSLCertificateFile with fullchain.pem too (depending on distro conventions).

If the failure shows up as “unknown CA” or “unable to get local issuer certificate,” use a handshake-focused workflow: TLS handshake troubleshooting tutorial (2026).

Step 7: find timer/cron problems so renewals keep working

If dry-run succeeds but renewals aren’t happening automatically, suspect the scheduler. On current Ubuntu/Debian images, Certbot usually runs via systemd timers.

Systemd timer checks

systemctl list-timers --all | grep -E 'certbot|letsencrypt' || true
systemctl status certbot.timer --no-pager -l 2>/dev/null || true
journalctl -u certbot.service -S "7 days ago" --no-pager | tail -n 120

Cron checks (still common on older images and some panels)

sudo ls -la /etc/cron.d | grep -E 'certbot|letsencrypt' || true
sudo crontab -l 2>/dev/null || true
sudo ls -la /var/log/cron* 2>/dev/null || true

Make failure visible (email/logging)

Don’t rely on silent automation. Log renewal output somewhere you’ll actually notice it.

A simple approach is a daily systemd service that runs certbot renew and writes to journald, plus a monitoring alert when it fails.

If you already monitor uptime and resources, add a certificate-expiry check to the same system. This pairs well with a basic monitoring setup: server monitoring tutorial (2026).

Step 8: cPanel/WHM and multi-tenant hosting considerations

On shared hosting servers and reseller nodes, failures often come from the environment rather than your vhost config. Common causes include DNS delegation problems, AutoSSL DCV limits, service restarts, and account-level routing quirks.

  • AutoSSL DCV failures often trace back to wrong A records, proxy/CDN mode, or HTTP→HTTPS redirects that break DCV paths.
  • Nameserver glue mistakes can cause intermittent SERVFAIL and inconsistent validation across regions.
  • Account isolation and permissions can block challenge file creation in certain custom setups.

If you run a WHM box, keep DNS boring and consistent. A bad delegation can break renewals and email delivery in the same week.

Hardening checklist: reduce renewal failures going forward

  • Keep port 80 open if you rely on HTTP-01. If you must close 80, switch to DNS-01 or TLS-ALPN-01 (only if your client supports it cleanly).
  • Pin the ACME challenge route with an explicit Nginx/Apache rule that bypasses app rewrites.
  • Use fullchain.pem in web server configs to avoid intermediate issues.
  • Verify renewal hooks reload the correct service and don’t silently fail.
  • Add certificate-expiry monitoring (alert at 14 days, then 7 days).
  • Document DNS ownership (who controls A/AAAA/TXT) so renewals don’t break during migrations.

Where HostMyCode fits (practical hosting choices)

If renewals keep failing because your current host is too constrained (no root access, blocked ports, limited logs), a VPS makes the problem solvable. A HostMyCode VPS gives you direct control of Nginx/Apache, firewall rules, and ACME automation.

For production sites where you’d rather reduce operational overhead, managed VPS hosting can make sense. You keep the flexibility of a VPS, with help on updates, service restarts, and recurring TLS issues.

If you’re stuck in a renewal loop—failed challenges, the wrong vhost answering, or certificates that issue but never deploy—work through this on a VPS where you can control DNS, firewall, and web server config. Start with a HostMyCode VPS, or choose managed VPS hosting if you want help verifying the setup.

FAQ

Do I need port 80 open for Let’s Encrypt renewal?

For HTTP-01 validation, yes. If you can’t keep port 80 open, switch to DNS-01 (TXT records) or a supported alternative challenge method.

Certbot says renewal succeeded, but the browser still shows the old cert. What now?

Check the live cert with openssl s_client, then verify your vhost points to the expected live/ path. Reload the correct service and confirm deploy hooks aren’t failing.

What’s the fastest way to see the exact ACME error?

Run sudo certbot renew --dry-run. If it fails, the output usually identifies routing vs DNS vs reachability.

Should Nginx/Apache use cert.pem or fullchain.pem?

In most hosting setups, use fullchain.pem so clients receive the intermediate certificate. Serving only cert.pem is a common cause of “chain incomplete” failures.

How do I avoid rate limits while troubleshooting?

Use the ACME staging environment while you iterate. Once dry-run works consistently, switch back to production issuance.

Summary: a renewal runbook you can reuse

Reliable renewals usually come down to four checks: DNS points to the right place, ports are reachable, the ACME challenge path is stable, and your deploy/reload hook works.

Standardize those checks. Then renewals stop being a surprise.

If you want an environment where you can see logs, control routing, and fix renewals quickly, run your sites on a HostMyCode VPS. That level of control is often the difference between “random TLS outages” and routine maintenance.