Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for DevOps Security & Hardening By James Joyner IV · · 9 min read

SSL_ERROR_BAD_CERT_DOMAIN: Certificate Name Mismatch — Cause and Fix

Quick answer

Fix SSL_ERROR_BAD_CERT_DOMAIN and ERR_CERT_COMMON_NAME_INVALID: diagnose missing SAN entries, wildcard scope, SNI default-vhost fallback, and Ingress host mismatches with openssl.

  • #security
  • #hardening
  • #tls
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

What this error means

The certificate the server presented is structurally valid and signed by a CA the browser trusts — it is simply not issued for the hostname you asked for. Firefox reports this as SSL_ERROR_BAD_CERT_DOMAIN; Chrome and Edge call the same condition NET::ERR_CERT_COMMON_NAME_INVALID.

Firefox:  Warning: Potential Security Risk Ahead
          websites prove their identity via certificates. Firefox does not trust this
          site because it uses a certificate that is not valid for shop.example.com.
          Error code: SSL_ERROR_BAD_CERT_DOMAIN

Chrome:   Your connection is not private
          NET::ERR_CERT_COMMON_NAME_INVALID

The check is narrow and worth knowing precisely: the client compares the hostname in the URL against the certificate’s Subject Alternative Name (SAN) list. Modern browsers have ignored the legacy Common Name (CN) field entirely since Chrome 58 (2017), so a certificate whose CN says shop.example.com but whose SAN list omits it will still fail. This surprises people who inspect the cert, see the right name in the subject line, and conclude the certificate is fine.

This is distinct from expiry (SEC_ERROR_EXPIRED_CERTIFICATE) and from an untrusted issuer (SEC_ERROR_UNKNOWN_ISSUER / ERR_CERT_AUTHORITY_INVALID). Those are problems with the certificate itself. This one is a routing or issuance mismatch — usually the server handed back the wrong certificate, not a broken one.

Symptoms at the client

  • The browser interstitial names the host it expected and the name the certificate covers.
  • curl fails but curl -k succeeds — the transport works, only validation fails.
  • The apex works and www does not, or vice versa.
  • One subdomain fails while its siblings are fine.
  • It fails from outside but works from inside the cluster or over a port-forward.
  • It started immediately after a certificate renewal, an Ingress edit, or a new vhost.
  • Older clients that still honour CN succeed while current browsers fail.

Name-mismatch causes

  • SAN list omits the hostname. The most common cause. The CSR was built with a CN only, or a name was dropped at renewal.
  • Apex and www treated as one name. example.com and www.example.com are distinct SAN entries. A certificate for one does not cover the other.
  • Wildcard scope misunderstood. *.example.com matches shop.example.com but not example.com itself and not api.shop.example.com — a wildcard covers exactly one label.
  • SNI default-vhost fallback. The client sent a hostname the server has no vhost for, so the server answered with its default certificate. Very common on nginx when server_name does not match, or when no default_server is defined and the first block wins.
  • TLS terminated somewhere else. A load balancer, CDN, or service mesh sidecar is terminating TLS with its own certificate before the request ever reaches your origin.
  • Ingress host does not match the TLS block. In Kubernetes, spec.rules[].host and spec.tls[].hosts[] are separate lists; a name present in one and absent from the other yields the cluster’s fallback certificate.
  • cert-manager issued a different set of names. dnsNames on the Certificate drifted from the Ingress host, or the Certificate is not actually Ready and a stale Secret is being served.
  • Connecting by IP or internal DNS name. https://10.0.1.15/ cannot match a certificate issued for a DNS name unless the IP is in the SAN list as an IP Address entry.

Inspecting the TLS handshake

Ask the server what it actually presents for that exact hostname. -servername sets SNI and is the flag that matters here:

openssl s_client -connect shop.example.com:443 -servername shop.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -ext subjectAltName -dates

Read the X509v3 Subject Alternative Name line, not the subject. If your hostname is absent from that list, you have found the problem.

Confirm an SNI default-vhost fallback by asking the same socket twice — once with SNI, once without. Two different certificates means the server has no vhost for that name and is falling back to a default:

for sni in shop.example.com ""; do
  echo "--- SNI: ${sni:-<none>}"
  openssl s_client -connect shop.example.com:443 ${sni:+-servername "$sni"} </dev/null 2>/dev/null \
    | openssl x509 -noout -subject
done

Check the whole SAN list at once when you are auditing several names:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -text \
  | awk '/Subject Alternative Name/{getline; gsub(/ *DNS:/,"\n  "); print}'

Confirm what curl thinks, which reports the mismatch in plain language:

curl -vI https://shop.example.com 2>&1 | grep -Ei 'subjectAltName|SSL certificate problem|issuer'

On nginx, find which block is answering — the resolved config is authoritative, not the file you think is loaded:

nginx -T 2>/dev/null | grep -nE 'server_name|ssl_certificate|listen .*443'

In Kubernetes, compare the two host lists that must agree:

kubectl get ingress shop -o jsonpath='rules: {.spec.rules[*].host}{"\n"}tls:   {.spec.tls[*].hosts}{"\n"}'
kubectl get certificate shop-tls -o jsonpath='{.spec.dnsNames}{"\n"}{.status.conditions[?(@.type=="Ready")].status}{"\n"}'

Inspect the certificate actually stored in the Secret, which may lag the Certificate resource:

kubectl get secret shop-tls -o jsonpath='{.data.tls\.crt}' | base64 -d \
  | openssl x509 -noout -subject -ext subjectAltName -dates

The fix

Work in this order — it separates “wrong certificate issued” from “right certificate, wrong server answering”, which need different repairs:

  1. Read the served SAN list with openssl s_client -servername <host> and confirm the hostname is genuinely absent, rather than assuming from the browser message.
  2. Repeat the handshake without SNI. If a different certificate comes back, the server has no vhost for that name and is serving a default — fix the routing, not the certificate.
  3. If the name is missing from the SAN list, reissue the certificate with every hostname included, verifying the CSR before submitting it.
  4. If the certificate is correct, align the server configuration so the hostname maps to the vhost holding that certificate — server_name in nginx, or matching rules.host and tls.hosts in a Kubernetes Ingress.
  5. Reload the serving process so it re-reads the certificate from disk, then confirm the Certificate or Secret actually updated rather than trusting the control plane’s status.
  6. Re-validate from outside the network in a private browser window, since TLS state is cached aggressively and a fixed server can still look broken.

Reissue with every name in the SAN list. With an explicit config so nothing is dropped at renewal:

cat > san.cnf <<'EOF'
[req]
distinguished_name = dn
req_extensions     = v3_req
prompt             = no
[dn]
CN = example.com
[v3_req]
subjectAltName = @alt
[alt]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = shop.example.com
EOF

openssl req -new -key tls.key -out tls.csr -config san.cnf
openssl req -in tls.csr -noout -text | grep -A1 'Subject Alternative Name'   # verify before sending

With certbot, list every name in one certificate:

certbot certonly --nginx -d example.com -d www.example.com -d shop.example.com

Fix the nginx vhost so the hostname resolves to the right block, and add an explicit default_server that fails closed rather than serving the wrong certificate:

server {
    listen 443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;          # nginx 1.19.4+: refuse unknown SNI instead of guessing
}

server {
    listen 443 ssl;
    server_name example.com www.example.com shop.example.com;
    ssl_certificate     /etc/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/example.com/privkey.pem;
}
nginx -t && systemctl reload nginx

Align the Kubernetes Ingress host lists — every host under rules must also appear under tls.hosts:

spec:
  tls:
    - hosts:
        - shop.example.com      # must match the rule host exactly
      secretName: shop-tls
  rules:
    - host: shop.example.com

Align cert-manager dnsNames with those hosts, then confirm the Certificate goes Ready before you retest:

kubectl patch certificate shop-tls --type merge \
  -p '{"spec":{"dnsNames":["shop.example.com","www.shop.example.com"]}}'
kubectl wait --for=condition=Ready certificate/shop-tls --timeout=120s

Re-validate from outside, and remember browsers cache TLS state aggressively — test in a private window or restart the browser before concluding the fix failed:

openssl s_client -connect shop.example.com:443 -servername shop.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName

Do not “fix” this by clicking through the interstitial, adding a permanent exception, or setting curl -k / verify=False in application code. The warning is doing exactly its job: telling you the identity presented does not match the identity requested. Suppressing it in a client removes the only protection against an interception you would otherwise detect.

Certificate lifecycle

  • Monitor SAN coverage, not just expiry. Most alerting checks notAfter and never notices that a name silently dropped out of the certificate at renewal.
  • Add new hostnames to the certificate in the same change that adds the DNS record. Name mismatches almost always ship as a two-step deploy where the second step is forgotten.
  • Prefer ssl_reject_handshake on over a catch-all certificate. A clean handshake refusal is far easier to diagnose than a plausible-looking certificate for the wrong name.
  • Treat wildcards as one label deep and issue explicitly for anything nested.
  • Automate renewal end to end, including the reload — a renewed certificate that the process never re-read behaves exactly like a mismatch.
  • Blackbox Exporter can assert the served SAN list continuously; pair it with an alert on certificate age so both failure modes are covered.

Frequently asked questions

Why does the certificate look correct in the browser but still fail? Browsers validate the hostname against the Subject Alternative Name list only. The Common Name field shown in most certificate viewers has been ignored by Chrome since version 58 and by Firefox for as long. A certificate whose CN is right but whose SAN list omits the name will always fail.

Does a wildcard certificate cover every subdomain? No. A wildcard matches exactly one label. *.example.com covers shop.example.com but not example.com itself and not api.shop.example.com. Nested subdomains need their own SAN entry or a second wildcard at that level.

Why does it work internally but fail from the internet? Something in front of the origin is terminating TLS — a load balancer, CDN, or ingress controller — and presenting its own certificate. Inside the cluster you reach the origin directly and see the correct one. Check the certificate at each hop rather than only at the origin.

Can I fix this by adding a browser exception? You can dismiss it, but you should not. The warning means the identity presented does not match the identity requested, which is precisely the signal that distinguishes a misconfiguration from an interception. Suppressing it in a browser — or with curl -k or verify=False in code — removes the only protection you have against the latter.

The certificate resource says Ready but the error persists. What now? Inspect the certificate in the Secret rather than the Certificate resource, and confirm the serving process reloaded. A renewed certificate that nginx or the ingress controller never re-read behaves identically to a name mismatch.

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.