Fix SSL Certificate Name Mismatch for www and Apex Domains
Fix NET::ERR_CERT_COMMON_NAME_INVALID and certificate name mismatch errors on a Django site: issue one certificate for both www and the apex domain, redirect to a canonical host, and check DNS, SNI, and default server blocks.
Problem statement
https://example.com loads, but https://www.example.com shows:
Your connection is not private
NET::ERR_CERT_COMMON_NAME_INVALID
This server could not prove that it is www.example.com; its security certificate is from example.com.
Or curl reports no alternative certificate subject name matches target host name. A certificate lists the exact hostnames it is valid for. example.com and www.example.com are different names, so a certificate for one does not cover the other. The same error appears when a request for one site on a shared server receives another site's certificate.
- Check what the certificate covers:
openssl s_client -connect www.example.com:443 -servername www.example.com | openssl x509 -noout -ext subjectAltName. - Reissue with every name:
sudo certbot --nginx -d example.com -d www.example.com --cert-name example.com. - Put both names in the
server_nameof the 443 block that uses that certificate. - Redirect the non-canonical name to the canonical one after TLS, in Nginx.
- Add both names to
ALLOWED_HOSTSandCSRF_TRUSTED_ORIGINS, or only the canonical one if the other always redirects at Nginx.
Step-by-step solution
1. See which certificate is served for each name
for h in example.com www.example.com; do
echo "== $h"
echo | openssl s_client -connect $h:443 -servername $h 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName
done
The -servername flag sends SNI, as browsers do. If the subjectAltName for www lacks DNS:www.example.com, the certificate is missing the name. If it shows a completely different domain, Nginx chose the wrong server block.
2. Issue one certificate for both names
sudo certbot --nginx --cert-name example.com -d example.com -d www.example.com
--cert-name expands the existing certificate instead of creating example.com-0001. Both names must resolve to this server and pass the HTTP-01 challenge, so create the www DNS record first; see Point a Domain to a Django Server.
Check the result:
sudo certbot certificates
3. Serve the certificate for both names
Both hostnames must reach a 443 block that uses the certificate. A clean pattern with example.com as canonical:
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / { proxy_pass http://unix:/run/gunicorn.sock; include proxy_params; }
}
The www block still needs the certificate: the TLS handshake, and therefore the certificate check, happens before Nginx can send any redirect.
4. Check DNS for stray records
dig +short example.com A
dig +short www.example.com A
dig +short www.example.com CNAME
If www points at an old host, a parking page, or a CDN with its own certificate, the mismatch comes from that server. Point both names at the same place. Also check AAAA records: a stale IPv6 address can send some visitors to a different machine.
5. Watch the default server on shared hosts
When a request's SNI name matches no server_name on port 443, Nginx uses the default server for that port and presents its certificate, which belongs to another site. Make every name explicit, and consider a catch-all default that rejects unknown names:
server {
listen 443 ssl default_server;
server_name _;
ssl_reject_handshake on;
}
ssl_reject_handshake (Nginx 1.19.4+) closes unknown-name handshakes instead of leaking another site's certificate.
Verification check: rerun the openssl s_client loop; both names should list both DNS: entries, curl -sI https://www.example.com should return the 301, and curl -sI https://example.com should return 200 with no -k flag.
Explanation
Browsers validate the hostname in the address bar against the certificate's Subject Alternative Name list (the old Common Name field is ignored by modern browsers). Let's Encrypt issues certificates with up to 100 names, so covering both forms of a domain costs nothing. Because this validation is part of the TLS handshake, it happens before HTTP exists on the connection, which is why redirects, Django settings, and ALLOWED_HOSTS cannot help until the certificate itself is right.
Edge cases / notes
- Wildcards (
*.example.com) coverwww.example.combut notexample.comitself, and need a DNS-01 challenge. - Subdomains for an API or admin each need to be on a certificate too; list them or use a wildcard.
- Cloudflare proxy presents Cloudflare's edge certificate to visitors; a mismatch there is a Cloudflare setting, while the origin certificate matters for Full (strict) mode.
- Old bookmarks to
http://wwwkeep working through the port 80 redirect, but HSTS-preloaded domains go straight to HTTPS, so the certificate must coverwww.
Next steps
- Review the full certificate setup in Configure HTTPS With Let's Encrypt for Django.
- If HTTPS does not load at all, start with Fix HTTPS Not Working for Django Behind Nginx.
FAQ
Does a certificate for example.com cover www.example.com?
No. Each hostname must be listed, unless a wildcard covers it, and wildcards do not cover the apex.
Can I just redirect www to the apex without a certificate?
No. The browser checks the certificate before it receives the redirect.
Do I need two certificates?
No. One certificate can list both names, and both server blocks can use it.
Why do I see another site's certificate?
The requested name did not match any server_name on port 443, so Nginx used the default server.