How to Fix SSL Certificate Errors: Expired Certs, Mixed Content & HTTPS Redirects
Diagnose and fix SSL certificate issues. Renew Let's Encrypt, fix mixed content warnings, resolve HTTPS redirect loops.
TL;DR
SSL certificate errors fall into three categories: expired or invalid certificates, mixed content warnings (HTTP resources loaded on HTTPS pages), and HTTPS redirect loops. Diagnose the exact problem with openssl s_client and browser dev tools, renew certificates with Certbot, fix mixed content by replacing http:// references, and resolve redirect loops by checking your web server configuration. Set up auto-renewal so you never deal with expired certs again.
Prerequisites
- Root or sudo access to the web server
- Certbot installed (
apt install certbotoryum install certbot) - OpenSSL installed (ships with most Linux distributions)
- Access to DNS records for your domain (for wildcard certs)
- Basic familiarity with your web server (Apache or Nginx) configuration
Step-by-Step Guide
1. Diagnosing the Exact SSL Issue
Before fixing anything, determine exactly what is broken. Start with openssl s_client, which gives you raw certificate details from the command line:
# Connect and show certificate details
openssl s_client -connect example.com:443 -servername example.com /dev/null | openssl x509 -noout -dates -subject -issuer
This outputs the certificate's subject, issuer, and validity dates. Check whether the notAfter date is in the past.
# Check the full certificate chain
openssl s_client -connect example.com:443 -servername example.com -showcerts &1 | grep -E '(Certificate chain|s:|i:| [0-9]+ s:)'
In the browser, open Developer Tools (F12), go to the Security tab (Chrome) or click the padlock icon. This tells you whether the issue is an expired cert, a name mismatch, a missing intermediate, or mixed content.
For a thorough external audit, submit your domain to SSL Labs. It grades your configuration and highlights specific problems like weak cipher suites, incomplete chains, or protocol issues.
# Quick check: does the cert match the domain?
openssl s_client -connect example.com:443 -servername example.com /dev/null | openssl x509 -noout -text | grep -A1 'Subject Alternative Name'
2. Renewing Let's Encrypt Certificates
If your certificate is expired or about to expire, Certbot makes renewal straightforward.
# Try automatic renewal of all certs managed by Certbot
sudo certbot renew --dry-run
# If the dry run succeeds, run the actual renewal
sudo certbot renew
If automatic renewal fails, re-issue the certificate manually. Choose the method that matches your setup:
# For Nginx
sudo certbot certonly --nginx -d example.com -d www.example.com
# For Apache
sudo certbot certonly --apache -d example.com -d www.example.com
# Webroot method (works with any server)
sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
After renewal, reload your web server:
# Nginx
sudo systemctl reload nginx
# Apache
sudo systemctl reload apache2 # Debian/Ubuntu
sudo systemctl reload httpd # RHEL/CentOS
Verify the new certificate is active:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
3. Fixing Mixed Content Warnings
Mixed content occurs when an HTTPS page loads resources (images, scripts, stylesheets) over plain HTTP. Browsers block "active" mixed content (scripts, iframes) and warn about "passive" mixed content (images).
First, find the offending references:
# Search your codebase for hardcoded http:// URLs
grep -rn 'http://' /var/www/html --include='*.html' --include='*.php' --include='*.css' --include='*.js' | grep -v 'http://localhost' | grep -v '# http://'
# In a database-backed CMS like WordPress
wp search-replace 'http://example.com' 'https://example.com' --dry-run
wp search-replace 'http://example.com' 'https://example.com'
For a quick fix while you clean up references, add a Content-Security-Policy header that automatically upgrades HTTP requests:
# Nginx — add to server block
add_header Content-Security-Policy "upgrade-insecure-requests" always;
# Apache — add to .htaccess or vhost config
Header always set Content-Security-Policy "upgrade-insecure-requests"
You can also add this as a meta tag in your HTML:
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
Note: upgrade-insecure-requests is a stopgap. The correct fix is to update all references to use https:// or protocol-relative URLs (//).
4. Correcting HTTPS Redirect Loops
A redirect loop typically happens when both the web server and an application (or a reverse proxy) are each trying to force HTTPS, causing an infinite back-and-forth.
Nginx — correct HTTP-to-HTTPS redirect:
# Separate server block for port 80 — redirect only
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# HTTPS server block — do NOT redirect here
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# your config here
}
If you are behind a load balancer or reverse proxy that terminates SSL, the backend sees plain HTTP. Use the X-Forwarded-Proto header instead of checking the port:
# Only redirect when the original request was HTTP
server {
listen 80;
server_name example.com;
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
}
Apache — correct .htaccess redirect:
# Correct: use HTTPS environment variable
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Behind a load balancer, check the forwarded proto instead
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} =http
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Test your redirect to make sure it terminates:
curl -ILs http://example.com 2>&1 | grep -E '(HTTP/|Location:)'
You should see exactly one 301 redirect followed by a 200 OK.
5. Certificate Chain Issues
A missing intermediate certificate is one of the most common SSL problems. Your browser may trust the cert (because it caches intermediates), but other clients — mobile apps, API consumers, curl — will reject it.
# Verify the certificate chain
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /etc/letsencrypt/live/example.com/fullchain.pem
# Check what the server actually sends
openssl s_client -connect example.com:443 -servername example.com &1 | grep -E '(depth|verify)'
With Let's Encrypt, always use fullchain.pem (which includes the intermediate), not cert.pem alone:
# Correct
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
# Wrong — missing intermediate
# ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
For non-Let's Encrypt certs, concatenate the certificate and intermediate(s) into one file:
cat your_domain.crt intermediate.crt > combined.crt
6. Wildcard vs. Individual Certificates
A wildcard certificate (*.example.com) covers all single-level subdomains (e.g., app.example.com, api.example.com), but not the apex domain itself or nested subdomains (deep.sub.example.com).
To get a wildcard cert from Let's Encrypt, you must use DNS-01 challenge validation:
# Wildcard cert with DNS challenge
sudo certbot certonly --manual --preferred-challenges dns \
-d example.com -d '*.example.com'
# With a DNS plugin (e.g., Cloudflare) for automation
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d example.com -d '*.example.com'
Use wildcards when you have many subdomains or create them dynamically. Use individual certs when you have few, stable subdomains — it reduces blast radius if a key is compromised.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
NET::ERR_CERT_DATE_INVALID | Expired certificate | Run certbot renew and reload the web server |
NET::ERR_CERT_COMMON_NAME_INVALID | Cert issued for wrong domain | Re-issue the cert with the correct -d flags |
NET::ERR_CERT_AUTHORITY_INVALID | Missing intermediate or self-signed cert | Use fullchain.pem; check with openssl verify |
ERR_TOO_MANY_REDIRECTS | HTTPS redirect loop | Check that only one layer (server config OR app) does the redirect |
| Mixed content warnings in console | HTTP resources on HTTPS page | Replace http:// references; add upgrade-insecure-requests |
| Certbot renewal fails with 403 | .well-known/acme-challenge not accessible | Check webroot path, ensure no rewrite rules block it |
SSL works in browser but curl fails | Incomplete certificate chain | Serve the full chain including intermediates |
| Cert renewed but old cert still served | Web server not reloaded | Run systemctl reload nginx (or apache2/httpd) |
When Certbot renewal fails, check the logs:
sudo cat /var/log/letsencrypt/letsencrypt.log | tail -50
# Verify the ACME challenge path is accessible
curl -I http://example.com/.well-known/acme-challenge/test
Prevention
Auto-Renewal with Cron
# Add to root's crontab: sudo crontab -e
0 3 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx" >> /var/log/certbot-renew.log 2>&1
Auto-Renewal with Systemd Timer
Most Certbot packages ship with a systemd timer. Verify it is active:
sudo systemctl status certbot.timer
sudo systemctl enable --now certbot.timer
# Check when it will next run
systemctl list-timers certbot.timer
To add a reload hook for systemd-managed renewals:
# Create a deploy hook script
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh > /dev/null <<'EOF'
#!/bin/bash
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh
Monitoring Certificate Expiry
# Simple script to check days until expiry
DOMAIN="example.com"
EXPIRY=$(echo | openssl s_client -connect "$DOMAIN:443" -servername "$DOMAIN" 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -jf "%b %d %T %Y %Z" "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
echo "$DOMAIN: $DAYS_LEFT days until certificate expires"
if [ "$DAYS_LEFT" -lt 14 ]; then
echo "WARNING: Certificate expires in less than 14 days!" >&2
fi
Best Practices Checklist
- Always use
fullchain.pem, nevercert.pemalone - Test renewal with
--dry-runafter any server changes - Run SSL Labs test after every certificate or configuration change
- Set up monitoring alerts for certificates expiring within 14 days
- Use protocol-relative URLs or
https://everywhere in your codebase - Keep only one redirect layer — either the web server or the application, not both
- Document which domains and subdomains each certificate covers
- Store Certbot credentials files (for DNS plugins) with strict permissions (
chmod 600)
Need Expert Help?
Still stuck? I fix ONE SSL issue in 30 min for €39. Money-back guarantee.
Book Now — €39100% money-back guarantee