Skip to content

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 -d arguments (or use a wildcard for many subdomains).
  • After issuance, run the openssl s_client … | openssl x509 -ext subjectAltName check above.
  • Re-run sysadmin-sslcheck to confirm every variant passes.

Page last modified: 2026-08-31 15:31:45