Self-Hosted SSL Certificate Errors: Fixes

The Problem

Your self-hosted service shows SSL/TLS errors in the browser:

NET::ERR_CERT_AUTHORITY_INVALID
SSL_ERROR_BAD_CERT_DOMAIN
ERR_CERT_DATE_INVALID
ERR_SSL_PROTOCOL_ERROR
SSL: CERTIFICATE_VERIFY_FAILED
Mixed Content: The page was loaded over HTTPS but requested HTTP resource

These errors prevent HTTPS access or cause browsers to warn users about security.

The Cause

SSL certificate errors in self-hosted environments have a few distinct root causes:

ErrorCauseUrgency
ERR_CERT_AUTHORITY_INVALIDSelf-signed certificate or untrusted CACommon — cert not from a trusted authority
ERR_CERT_COMMON_NAME_INVALIDCertificate doesn’t match the domainMisconfigured cert or wrong domain
ERR_CERT_DATE_INVALIDCertificate expiredRenewal failed — fix immediately
ERR_SSL_PROTOCOL_ERRORTLS version mismatch or broken handshakeServer configuration issue
CERTIFICATE_VERIFY_FAILEDApp/container can’t verify upstream SSLMissing CA certificates in container
Mixed ContentPage loads HTTP resources over HTTPSApp or proxy misconfiguration

The Fix

Fix 1: Let’s Encrypt Certificate Not Issuing

Symptom: Reverse proxy shows self-signed cert. Let’s Encrypt challenge fails.

For HTTP-01 challenge (most common):

Let’s Encrypt needs to reach your server on port 80 from the internet.

# Verify port 80 is reachable from outside
curl -I http://yourdomain.com/.well-known/acme-challenge/test

Checklist:

  1. Port 80 forwarded from your router to your server
  2. DNS A record points to your public IP (not a local IP)
  3. Firewall allows inbound port 80
  4. No other service (Apache, another Nginx) is binding port 80
  5. Cloudflare proxy (orange cloud) is disabled for the domain during initial cert issuance

For DNS-01 challenge (wildcard certs, Cloudflare Tunnel):

DNS challenge doesn’t require port 80 but needs API access to your DNS provider:

# Verify DNS API credentials work
# Caddy with Cloudflare:
CLOUDFLARE_API_TOKEN=your-token caddy run

# Traefik with Cloudflare:
# Check CF_API_EMAIL and CF_API_KEY are set correctly

Fix 2: Certificate Domain Mismatch

Symptom: ERR_CERT_COMMON_NAME_INVALID — browser says cert is for wrong.domain.com

The certificate’s Subject Alternative Names (SANs) must include the exact domain you’re accessing.

Verify what domain the cert covers:

echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

Common causes:

  • Certificate issued for example.com but accessing app.example.com (need wildcard or per-subdomain cert)
  • Nginx Proxy Manager assigned the wrong certificate to the proxy host
  • Traefik router matches the wrong certificate resolver

Fix for Nginx Proxy Manager: Edit the proxy host → SSL tab → ensure the correct domain certificate is selected. If missing, request a new Let’s Encrypt cert for the exact domain.

Fix for Caddy: Caddy auto-provisions certificates for every domain in its config. Verify the domain in Caddyfile matches what you access:

app.yourdomain.com {
    reverse_proxy localhost:8080
}

Fix for Traefik: Verify the router’s Host rule matches the domain:

labels:
  - "traefik.http.routers.myapp.rule=Host(`app.yourdomain.com`)"
  - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"

Fix 3: Expired Certificate

Symptom: ERR_CERT_DATE_INVALID — certificate expired

Let’s Encrypt certificates expire every 90 days. Auto-renewal should handle this.

Nginx Proxy Manager: Renewal is automatic. If it fails:

  1. Check the SSL certificate list — is renewal failing?
  2. Verify port 80 is still reachable (required for renewal)
  3. Check Nginx Proxy Manager logs: docker compose logs npm
  4. Delete the certificate and re-issue it

Caddy: Renewal is fully automatic. If failing:

docker compose logs caddy | grep -i "cert\|tls\|renew"

Traefik: Check the ACME storage file:

docker exec traefik cat /acme.json | python3 -m json.tool | grep -A5 "notAfter"

Manual renewal (any provider):

# Using certbot
certbot renew --dry-run  # Test first
certbot renew            # Actually renew

Fix 4: SSL Protocol Error

Symptom: ERR_SSL_PROTOCOL_ERROR — connection reset during handshake

Cause 1: Service listening on HTTP, not HTTPS

Your reverse proxy is sending HTTPS to a backend that only speaks HTTP:

Browser → (HTTPS) → Reverse Proxy → (HTTPS) → Container expecting HTTP

Fix: Tell the proxy to use HTTP to the backend:

Nginx Proxy Manager: In the proxy host, set scheme to http (not https)

Caddy:

app.yourdomain.com {
    reverse_proxy http://localhost:8080  # Explicit HTTP
}

Cause 2: Port conflict

The service is on a different port than expected. Verify:

docker exec myapp ss -tlnp
# Check which port the service actually listens on

Fix 5: Container Can’t Verify Upstream SSL

Symptom: App inside container throws CERTIFICATE_VERIFY_FAILED when connecting to external HTTPS services

Cause: Minimal container images (Alpine, distroless) may not have CA certificates installed.

Fix for Alpine:

RUN apk add --no-cache ca-certificates

Fix for Debian/Ubuntu:

RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

Fix in Docker Compose (mount host CA certs):

volumes:
  - /etc/ssl/certs:/etc/ssl/certs:ro

Fix 6: Mixed Content Warnings

Symptom: Browser shows mixed content warning; some resources load over HTTP

Cause: The application generates HTTP URLs for assets, links, or API calls instead of HTTPS.

Fix — tell the app it’s behind HTTPS:

Most apps need to know they’re behind an HTTPS reverse proxy:

# Common env vars to set
FORCE_HTTPS: "true"
BASE_URL: "https://app.yourdomain.com"
TRUSTED_PROXIES: "172.16.0.0/12"

For Nginx Proxy Manager, enable “Force SSL” on the proxy host.

For apps behind Caddy or Traefik, set the X-Forwarded-Proto: https header (usually automatic).

Prevention

Use a Reverse Proxy That Auto-Manages Certs

ProxyCert ManagementEffort
CaddyFully automatic, zero configLowest
TraefikAutomatic with cert resolver configLow
Nginx Proxy ManagerUI-driven, one-click Let’s EncryptLow

Caddy is the least error-prone — it auto-provisions and renews certificates with zero configuration beyond listing the domain name.

Monitor Certificate Expiry

Add certificate monitoring to your Uptime Kuma instance:

  • Monitor type: Keyword or Certificate Expiry
  • URL: https://app.yourdomain.com
  • Alert when: Certificate expires within 14 days

Use DNS Challenge for Internal Services

If your services aren’t publicly accessible (VPN-only or LAN-only), use DNS challenge validation for Let’s Encrypt. This doesn’t require port 80 to be open.

Comments