SAN entries and why www.example.com can fail when example.com works¶
What a SAN is¶
SAN = Subject Alternative Name. It’s an X.509 extension inside a TLS certificate listing every hostname the cert is valid for. Modern browsers (and Python’s ssl module, curl, openssl, etc.) ignore the legacy CN= field entirely and check only the SAN list. A cert whose only SAN is DNS:example.com is valid for example.com and nothing else — not www.example.com, not api.example.com.
How this breaks in practice¶
If a server returns the same cert for both example.com and www.example.com (the typical nginx/Caddy default when there’s a single server block), the www. variant will fail TLS validation even though browsing the bare domain looks fine. The HTTP-level redirect (301 → https://www.example.com/) happens after the TLS handshake, so a redirect from the broken hostname to the working one doesn’t save you — the handshake has already failed.
You’ll often miss this in everyday browsing because you click links or autocomplete to the bare domain. The sysadmin-sslcheck script surfaces it because it tests each variant independently.
How to fix it¶
Reissue the cert and include every hostname you want to serve as a separate -d (or equivalent) flag. Each one becomes a SAN entry.
Let’s Encrypt + certbot:
certbot certonly -d example.com -d www.example.com
The first -d also becomes the legacy CN, but functionally all entries are SANs.
Caddy: list every name in the site block; Caddy asks for matching SANs automatically.
example.com, www.example.com {
...
}
acme.sh + nginx / haproxy / whatever:
acme.sh --issue -d example.com -d www.example.com --webroot /var/www/...
Wildcard alternative¶
*.example.com as a SAN covers every direct subdomain (www., api., mail., …) but not the bare domain itself — you’d still list example.com separately. Let’s Encrypt only issues wildcards via the DNS-01 challenge (you prove control by setting a TXT record), so it needs DNS automation or manual intervention. Worth it if you have many subdomains; overkill for just www..
Verifying the fix¶
After reissuing, check that all the names you expect are present:
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null \
| openssl x509 -noout -ext subjectAltName
You want to see something like:
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com
Both names, comma-separated, in the same extension. If you only see one, the cert is still incomplete.
Quick checklist for any new HTTPS site¶
- Decide every hostname that should resolve via HTTPS (apex,
www., any subdomains you proxy). - Request the cert with all of them as separate
-darguments (or use a wildcard for many subdomains). - After issuance, run the
openssl s_client … | openssl x509 -ext subjectAltNamecheck above. - Re-run
sysadmin-sslcheckto confirm every variant passes.
Page last modified: 2026-08-31 15:31:45