
Most Let’s Encrypt renewals fail for the same unglamorous reasons. Port 80 isn’t reachable. DNS points to a different machine. Or the web server answers with the wrong vhost.
This SSL renewal troubleshooting tutorial gives you a repeatable workflow for fixing renewals on a VPS running Nginx, Apache, or cPanel/WHM. You’ll fix the cause without guesswork and without breaking production traffic.
If you’d rather stop thinking about certificates entirely, a managed VPS hosting plan can take routine ops off your plate. That usually includes renewals, updates, and basic monitoring, while keeping VPS isolation.
What you’re fixing: how Let’s Encrypt renewal actually works
Let’s Encrypt won’t issue a new certificate until it can verify you control the domain. In hosting setups, you’ll usually see one of these validation methods:
- HTTP-01: the CA requests
http://yourdomain/.well-known/acme-challenge/...on port 80. Your web server must serve the exact token. - DNS-01: the CA checks a TXT record under
_acme-challenge.yourdomain. Port 80 doesn’t matter, but you need to automate DNS edits (or do them manually).
HTTP-01 is the default for many stacks (Certbot and plenty of control panels). It’s also why “I force HTTPS” and “I blocked port 80” often turn into renewal failures.
Before you change anything: capture the exact error and current path
First, confirm which ACME client you’re using and how it runs on this server. On the VPS, start here:
sudo certbot --version
sudo certbot renew --dry-run -v
If your system uses systemd timers, verify the timer exists and is scheduled:
systemctl list-timers | grep -E 'certbot|acme'
When you review the error output, don’t stop at the one-line summary. Pull out the lines that mention the challenge type, the HTTP status, and the IP address Let’s Encrypt tried.
Quick diagnostic checklist (copy/paste into your ticket notes):
- Domain(s) failing:
- Validation method: HTTP-01 or DNS-01
- Reported IP address:
- HTTP status (404/403/503/timeout):
- Web server: Nginx/Apache/cPanel (EA4)
- Reverse proxy/CDN in front (Cloudflare, etc.): yes/no
Step 1 — Verify DNS is pointing to the server you’re renewing on
A lot of “renewal failures” are really “you’re renewing on the wrong box.” Check A/AAAA from outside the server:
dig +short A yourdomain.com
dig +short AAAA yourdomain.com
curl -4s https://api.ipify.org; echo
If dig returns an IP that doesn’t match your VPS public IPv4, fix DNS before touching Certbot or web configs.
If you’re doing a low-downtime move, follow a real cutover plan (TTL, validation, rollback). HostMyCode has a good walkthrough: DNS propagation tutorial for low-downtime cutovers.
Common DNS gotchas that break renewals:
- Old AAAA record points to a retired IPv6 address. Let’s Encrypt may try IPv6 first and fail. Either fix IPv6 or remove AAAA.
- Split-horizon DNS on an office network. Internal resolution hits one IP, external hits another. Renewals only care about external.
- CDN proxy mode (for example, “orange cloud”). HTTP-01 can work, but it’s easy to misroute. If you’re unsure, temporarily set DNS to “DNS only” while testing.
Step 2 — Confirm port 80 is reachable from the public internet
For HTTP-01, port 80 must answer. You can redirect everything to HTTPS, but the CA still needs to reach port 80 to fetch the challenge file.
From a machine outside your VPS (or an online port checker), try:
curl -I http://yourdomain.com/.well-known/acme-challenge/ping-test
You’re not expecting a valid token here. You’re checking whether HTTP is reachable at all.
If it times out, check the listener and firewall rules:
sudo ss -lntp | grep ':80'
sudo ufw status verbose 2>/dev/null || true
sudo iptables -S 2>/dev/null | head
If a recent rules change broke web access (or locked you out), unwind it methodically. This guide is a solid playbook: firewall troubleshooting tutorial.
Rule of thumb: allow inbound 80/tcp and 443/tcp on the interface with the public IP. Avoid “any to any,” but don’t block port 80 entirely if you’re using HTTP-01.
Step 3 — Identify the validation mode your client uses (webroot vs standalone vs Nginx/Apache plugin)
Certbot usually renews in one of these modes:
- webroot: writes token files into your site’s document root. Your vhost must serve
/.well-known/acme-challenge/from that root. - standalone: starts a temporary web server on port 80. Your existing web server must stop or free port 80.
- nginx/apache plugins: adjusts vhost config (or uses a temporary config) to route the challenge.
To confirm what you’re running today, inspect the renewal config:
sudo ls -1 /etc/letsencrypt/renewal/
sudo sed -n '1,160p' /etc/letsencrypt/renewal/yourdomain.com.conf
Look for lines like authenticator = webroot and webroot_path = /var/www/.... That’s your ground truth.
Fix pattern A — 404 on the ACME challenge (wrong webroot or vhost mismatch)
A 404 almost always means the request reached your server. The web server just didn’t serve the token from the path Certbot expects.
1) Create a test file in the webroot (use the same path Certbot thinks is correct):
sudo mkdir -p /var/www/yourdomain/public_html/.well-known/acme-challenge
printf 'ok
' | sudo tee /var/www/yourdomain/public_html/.well-known/acme-challenge/ping
2) Fetch it over HTTP:
curl -s http://yourdomain.com/.well-known/acme-challenge/ping
If you don’t see ok, the problem is usually one of these:
- Your vhost for
yourdomain.compoints at a different document root. - A catch-all vhost is responding instead of the domain’s vhost.
- Rewrite rules intercept
.well-knownor route requests into an app that returns 404.
Nginx fix (explicit location block). Add this inside the server { } for the domain, typically under /etc/nginx/sites-available/yourdomain:
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
root /var/www/yourdomain/public_html;
try_files $uri =404;
}
Then reload:
sudo nginx -t && sudo systemctl reload nginx
Apache fix (don’t redirect or gate the challenge path). In the vhost file (for example, /etc/apache2/sites-available/yourdomain.conf):
Alias /.well-known/acme-challenge/ "/var/www/yourdomain/public_html/.well-known/acme-challenge/"
<Directory "/var/www/yourdomain/public_html/.well-known/acme-challenge/">
Options None
AllowOverride None
Require all granted
</Directory>
Reload Apache:
sudo apachectl configtest && sudo systemctl reload apache2
Fix pattern B — 403 on the challenge (permissions, auth, or security rules blocking it)
A 403 means the file may exist, but the web server refuses to serve it. The usual suspects:
- Basic auth on the whole site (staging protection) without an exception for
/.well-known - WAF/mod_security rules blocking “dot well-known” paths
- Filesystem permissions that prevent the web user (www-data/apache) from reading the token file
Permission baseline for a straightforward VPS site:
- Directories:
755 - Files:
644 - Owner: your deploy user; group: web server group (or the reverse) depending on your workflow
Example fix:
sudo find /var/www/yourdomain/public_html -type d -exec chmod 755 {} \;
sudo find /var/www/yourdomain/public_html -type f -exec chmod 644 {} \;
Nginx basic auth exception (keep the site protected, but let ACME through):
location ^~ /.well-known/acme-challenge/ {
auth_basic off;
root /var/www/yourdomain/public_html;
try_files $uri =404;
}
If you’re on WordPress and running security plugins or custom rules, keep exceptions narrow.
You’ll get fewer surprises if you scope auth and blocking to specific paths, not the whole site. For a staging approach that won’t trip renewals, see: WordPress staging site tutorial.
Fix pattern C — Timeout or connection refused (routing, proxy, or wrong IP on multi-site servers)
Timeouts look like “firewall” at first glance. On hosting servers, they’re often caused by a proxy chain, a wrong listener, or traffic landing on a different IP than you expected.
Check the IP Let’s Encrypt used (from the Certbot output). Then confirm your server actually owns that address:
ip -br a
curl -s4 https://ifconfig.me; echo
If you use a reverse proxy (HAProxy, CDN, load balancer), verify that port 80 reaches a backend that can serve the challenge.
If you terminate TLS on HAProxy, you still need HTTP routing for ACME unless you switch to DNS-01. A clean fix is a dedicated ACL for /.well-known/acme-challenge/ that forwards to the correct backend.
If you’re improving your TLS front-end as part of a bigger hosting setup, this guide lines up well with renewal hygiene: TLS termination setup guide.
Fix pattern D — Standalone mode fails because port 80 is already in use
If your renewal file shows authenticator = standalone, Certbot needs to bind to port 80. That fails if Nginx or Apache is already listening.
Options:
- Switch to webroot (the safest choice for most hosting servers).
- Stop the web server during renewal (works, but it’s a production risk).
Switch an existing cert to webroot (example):
sudo certbot certonly --webroot \
-w /var/www/yourdomain/public_html \
-d yourdomain.com -d www.yourdomain.com
After it succeeds, re-run:
sudo certbot renew --dry-run
cPanel/WHM: troubleshoot AutoSSL renewal failures without breaking accounts
On cPanel servers, renewals usually run through AutoSSL (cPanel’s built-in issuance and renewal workflow). The root causes look familiar, but the controls live in WHM.
1) Check AutoSSL status and last run:
- WHM → SSL/TLS → Manage AutoSSL
- WHM → SSL/TLS → AutoSSL (logs and configuration)
2) Validate DNS for each domain.
In reseller-heavy environments, one account with bad DNS can fail every day and fill your logs. Look closely for:
- Domain points to an old IP after a migration
- Domain uses external DNS but the customer didn’t update the A record
- CAA records blocking issuance (look for restrictive CAA)
3) Check for HTTP→HTTPS→HTTP loops caused by misordered redirects or proxy settings.
If users enforce HTTPS at the app layer (WordPress, Laravel), confirm they aren’t forcing the wrong scheme behind a proxy.
If you want a tighter baseline for securing the control panel itself (and fewer “someone changed it at 2am” surprises), pair this with: cPanel hardening tutorial.
CAA records: the DNS setting that silently blocks issuance
CAA is a DNS record that tells CAs which certificate authorities are allowed to issue for your domain. If it’s too restrictive, Let’s Encrypt can be blocked even when everything else looks correct.
Check CAA:
dig +short CAA yourdomain.com
If the records only allow another CA, you have two realistic paths:
- Update CAA to include Let’s Encrypt (issue/issuewild)
- Use the allowed CA for issuance (common in enterprise environments)
Example CAA allowing Let’s Encrypt:
yourdomain.com. 3600 IN CAA 0 issue "letsencrypt.org"
HTTP->HTTPS redirects: safe rules that don’t break renewals
You can redirect all HTTP traffic to HTTPS and still pass HTTP-01. The key is to keep /.well-known/acme-challenge/ reachable over plain HTTP.
Nginx pattern (redirect everything except ACME):
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/yourdomain/public_html;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
Apache pattern (RewriteRule with exception):
RewriteEngine On
RewriteRule ^\.well-known/acme-challenge/ - [L]
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Make failures obvious: log locations and what to grep
If multiple domains are failing across multiple vhosts, speed matters. Go straight to the logs that show what was requested, where it landed, and what status came back.
- Certbot:
/var/log/letsencrypt/letsencrypt.log - Nginx:
/var/log/nginx/access.log,/var/log/nginx/error.log - Apache (Debian/Ubuntu):
/var/log/apache2/access.log,/var/log/apache2/error.log
During a failure window, this is usually enough to pinpoint the break:
sudo tail -n 200 /var/log/letsencrypt/letsencrypt.log
sudo grep -R "acme-challenge" -n /var/log/nginx/access.log /var/log/nginx/error.log 2>/dev/null | tail -n 50
If you manage more than one server, centralizing logs saves time during incidents. This setup guide keeps it lightweight: VPS log auditing tutorial.
Prevent repeat issues: add a renewal health check and alert
Renewal failures only become emergencies when you discover them on day 89. A simple daily check that warns you early removes the drama.
1) Check expiry locally:
echo | openssl s_client -servername yourdomain.com -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -dates
2) Create a simple cron that warns at 14 days:
sudo tee /usr/local/sbin/check-cert-expiry.sh >/dev/null <<'EOF'
#!/bin/sh
set -eu
DOMAIN="$1"
DAYS_LEFT=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN:443" 2>/dev/null \
| openssl x509 -noout -enddate \
| sed 's/^notAfter=//' \
| xargs -I{} date -d "{}" +%s)
NOW=$(date +%s)
LEFT=$(( (DAYS_LEFT - NOW) / 86400 ))
if [ "$LEFT" -lt 14 ]; then
echo "WARNING: $DOMAIN cert expires in ${LEFT} days"
fi
EOF
sudo chmod +x /usr/local/sbin/check-cert-expiry.sh
Then add a cron entry (example):
sudo crontab -e
15 6 * * * /usr/local/sbin/check-cert-expiry.sh yourdomain.com | /usr/bin/logger -t cert-expiry
For real alerts (email/Slack), wire this into monitoring. If you want a dashboard a non-Linux teammate can use without fear, start here: server monitoring setup guide.
Dedicated servers vs VPS vs shared hosting: where renewals usually fail
The troubleshooting path is the same, but your constraints change depending on the platform.
- Shared hosting: you typically won’t touch Nginx/Apache. Failures are usually DNS mispointing, expired domains, or account mapping issues. Your host should handle issuance.
- VPS: most failures come from web server routing, firewall rules, IPv6 AAAA mismatches, or redirect/auth rules at the web/app layer.
- Dedicated servers: similar to VPS, but you’ll see more multi-IP setups and more complex proxy chains.
If you run revenue sites and want fewer SSL surprises, a properly sized HostMyCode VPS gives you predictable networking and full control over web server configuration.
If you’d rather focus on the app, step up to managed service.
Wrap-up: a repeatable workflow that resolves most renewals
When renewal breaks, don’t start by reinstalling Certbot. Follow the chain. You’ll usually find the failure quickly:
- Confirm DNS A/AAAA points to the renewing server.
- Confirm port 80 is reachable (HTTP-01) and not blocked.
- Confirm the challenge path is served by the correct vhost.
- Resolve 404/403 based on webroot, auth, and rewrite behavior.
- Check CAA if issuance is denied even though HTTP works.
- Add an expiry check so the next failure is noisy on day 1, not day 89.
If renewals started failing right after a migration, treat it as DNS + vhost mapping first.
HostMyCode can also handle the heavy lifting via website and VPS migrations, including DNS and SSL validation so you don’t cut over into an outage.
If you’re tired of chasing renewal errors across multiple sites, consider a managed VPS hosting plan where SSL renewals and routine server hygiene are handled as part of normal operations. If you prefer DIY control with predictable performance, start with a HostMyCode VPS and keep this workflow nearby for quick, low-risk fixes.
FAQ: SSL renewal troubleshooting (practical cases)
Can I block port 80 if I only want HTTPS?
Not if you use HTTP-01 challenges. Keep port 80 open and redirect to HTTPS, but allow /.well-known/acme-challenge/ to be served.
Why does renewal fail only for some subdomains?
Usually the subdomain resolves to a different IP, answers from a different vhost, or has separate auth/rewrite rules. Test each hostname with dig and curl -I.
Let’s Encrypt says “unauthorized” but the token file exists. What now?
That often indicates the wrong server answered the request (DNS mismatch, CDN proxy misrouting, or IPv6 AAAA pointing elsewhere). Confirm the CA-reported IP matches your server.
Does cPanel AutoSSL need anything special to renew?
AutoSSL still depends on correct DNS and reachable HTTP validation. Most “AutoSSL failures” are customer DNS pointing away, restrictive CAA records, or redirect/auth rules interfering.
Should I use DNS-01 instead?
DNS-01 is a good fit if you cannot expose port 80 or you need wildcard certificates. It’s only pleasant if you can automate DNS updates through your DNS provider’s API.