← All Articles
Last updated: 2026-03-30

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

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

SymptomLikely CauseFix
NET::ERR_CERT_DATE_INVALIDExpired certificateRun certbot renew and reload the web server
NET::ERR_CERT_COMMON_NAME_INVALIDCert issued for wrong domainRe-issue the cert with the correct -d flags
NET::ERR_CERT_AUTHORITY_INVALIDMissing intermediate or self-signed certUse fullchain.pem; check with openssl verify
ERR_TOO_MANY_REDIRECTSHTTPS redirect loopCheck that only one layer (server config OR app) does the redirect
Mixed content warnings in consoleHTTP resources on HTTPS pageReplace http:// references; add upgrade-insecure-requests
Certbot renewal fails with 403.well-known/acme-challenge not accessibleCheck webroot path, ensure no rewrite rules block it
SSL works in browser but curl failsIncomplete certificate chainServe the full chain including intermediates
Cert renewed but old cert still servedWeb server not reloadedRun 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

Need Expert Help?

Still stuck? I fix ONE SSL issue in 30 min for €39. Money-back guarantee.

Book Now — €39

100% money-back guarantee

HR

Harald Roessler

Infrastructure Engineer with 20+ years experience. Founder of DSNCON GmbH.