Skip to main content
RunBook Academy

← All break/fix scenarios in Secrets, PKI & Certificates

intermediatetls-handshake~30 min

The partner endpoint fails verification and the certificate is perfectly valid

Reported symptoms

  • From the 08:00 UTC partner cutover every request to the new EU endpoint fails during the TLS handshake and never reaches the application, and the partner reports a certificate error that names their own hostname
  • The existing endpoint keeps serving at full traffic from the same host, the same listener and the same certificate file, which still has 94 days of validity left
  • An engineer opens a diagnostic TLS connection to the new name and the client reports a verification return code of zero, which the channel reads as proof the certificate is fine
  • The application access log records no requests at all for the new hostname, so the platform team initially reports back that the partner is simply not sending traffic yet
  • A browser on an engineer laptop shows a full-page certificate interstitial naming the site, and clicking through it loads the API normally, which convinces two people the API itself is healthy
  • DNS resolves the new name to the same address as the working name, and the firewall counters show partner connections arriving and being closed within milliseconds
  • Somebody adds the new name to the certificate Common Name on a staging host at 09:40 and no client behaviour changes at all

Evidence

  • · curl against the new name from the partner network fails with error 60 and the message that no alternative certificate subject name matches the target hostname api-eu.example.com
  • · curl against the existing name from that same host, with that same trust store and that same clock, returns HTTP 200, which rules out the chain, the trust store and expiry in one command
  • · openssl s_client with the new name sent as the server name reports a verify return code of 0 and the word ok, because it does not check the hostname unless it is explicitly asked to
  • · openssl x509 with the subjectAltName extension printed shows exactly two DNS entries on the served leaf, api.example.com and www.api.example.com, and nothing else
  • · openssl verify against that leaf with the hostname check set to api.example.com prints OK, so the leaf, the chain and the purpose are all correct
  • · openssl verify against the identical leaf with the hostname check set to api-eu.example.com prints error 62 at 0 depth lookup hostname mismatch, which is the same certificate failing on the name alone
  • · The certificate subject Common Name is api.example.com, and adding a second name to the Common Name on a staging host changed the behaviour of no client at all
  • · The nginx server_name directive for this virtual host lists three names while the certificate carries two, and the one name in the difference is exactly the name that fails
Diagnosis and resolutionclick to reveal

Root cause

A hostname was added to the service and not to the certificate. The EU partner cutover changed DNS, the load balancer listener and the nginx server_name directive so that api-eu.example.com reaches the same virtual host as api.example.com. Nothing changed the certificate, so the server still presents a leaf whose subjectAltName carries two names, neither of which is the one the partner asks for. Every partner client therefore completes chain building and expiry checking successfully and then rejects the certificate on identity, which is why the handshake fails and the application never records a request. Two independent defects put it there. The first is that the certificate was not part of the change. The names the service answers on are declared in the virtual host configuration, and the names the certificate vouches for are declared in a separate issuance configuration held in a different repository, so a single merge request could change one and not the other and nobody reviewing it would see a gap. The second is that nothing tests the relationship between those two lists. Staging could not have caught this because staging is served by a wildcard certificate covering every name in its zone, so a new name in staging works automatically and proves nothing about production. The first defect is the fault. The second is the reason it reached a partner cutover instead of a pull request review, and it is the one worth fixing first.

Remediation

Begin by confirming that this is an identity failure rather than a chain or expiry failure, because the three produce similar-looking client errors and only one of them is fixed by reissuing with a different name set. Run the verification twice against the same served leaf, once with the working name and once with the failing name, and confirm that only the name changes the result. That single pair rules out everything else and takes under a minute. Then collect the authoritative list of names the service must answer on, from the virtual host configuration and the load balancer listener rather than from memory, and reissue the certificate with every one of them present in the subjectAltName. Install the new certificate alongside the old one, reload rather than restart so connections drain, and verify each name individually before telling the partner to retry. Keep the previous certificate on disk until the new one is proven, because a reload that fails leaves you needing the old file within seconds. Do not ask the partner to disable certificate verification, even temporarily. That turns a visible outage into an unauthenticated channel carrying payment traffic, and a workaround given to a partner during a cutover is a workaround that survives for years. Do not add the name to the subject Common Name: RFC 9525 removes the Common Name fallback outright, clients are required to ignore it for identity, and the staging attempt at 09:40 demonstrated exactly that. Do not reach for a wildcard certificate merely to stop having this conversation, without first deciding whether one key should be able to speak for every name in the zone.

Verification

Verify from the partner network path rather than from the origin host, because the origin can reach itself in ways a partner cannot and the failure was only ever visible to a client that checks names. Request the health endpoint on the new name with verification enabled and confirm an HTTP 200 rather than a handshake error, then repeat for every other name the virtual host answers on so that the fix has not quietly dropped one. Read the certificate the listener now presents and confirm its subjectAltName contains the complete set, and confirm the serial differs from the certificate that was being served during the incident, which proves the reload reached the running process rather than only the filesystem. Run the hostname verification once per name against that served leaf and require every one of them to return OK, since a certificate that satisfies two names out of three fails in exactly the way this incident already demonstrated. Confirm the application access log now records partner requests under the new hostname, which is the independent observation that traffic is arriving rather than merely being accepted at the handshake. Finally, confirm the partner has removed any verification workaround they introduced during the outage.

Prevention

Make the certificate part of the change that adds the name. The names a service answers on and the names its certificate vouches for are one decision, and holding them in two repositories guarantees they will drift. Where the two must stay separate, generate the issuance name list from the virtual host configuration so that adding a server_name entry cannot silently fail to update the certificate. Add a test that asserts set equality between the names configured on the listener and the names in the served certificate, and run it in continuous integration and again as a post-deploy check, failing on any name present in one list and absent from the other. Probe every published name, not one name per service, at a five minute interval with hostname verification enabled, and alert on the first failure rather than after a threshold, because a name that fails verification fails one hundred per cent of the time and never recovers on its own. Make staging match production in certificate topology: a wildcard in staging and named certificates in production means staging cannot reproduce the most common naming failure there is. Finally, write the cutover checklist so that certificate coverage for the new name is verified from the partner network before DNS is pointed at it, at least 24 hours ahead, since verifying afterwards is what turned a configuration gap into an incident with an external audience.

Reported symptoms

A payments partner in the EU is being cut over to a dedicated hostname. The work is small: api-eu.example.com is added to DNS as a CNAME onto the existing edge address, the load balancer listener learns the name, and the nginx server_name line for the API virtual host gains a third entry. It is reviewed, merged and deployed on the Friday. The partner switches at 08:00 UTC on Monday.

Nothing arrives. The channel fills up faster than the facts do:

  • The partner reports that every call fails immediately with a certificate error naming api-eu.example.com. They have not received a single HTTP response.
  • The original endpoint is completely unaffected. Same host, same listener, same certificate file, 94 days of validity remaining, full production traffic.
  • 08:20, an engineer opens a diagnostic TLS connection to the new name. The client reports Verify return code: 0 (ok). This is taken as proof that the certificate is fine and the fault must be at the partner end.
  • 08:35, the platform team reports that the application access log contains no requests for the new hostname at all, and suggests the partner has not actually cut over yet.
  • 09:05, someone loads the new name in a browser and gets a full-page certificate interstitial. They click through it, the API responds normally, and two people conclude the API is healthy.
  • The firewall counters show partner connections arriving and being closed within milliseconds, which is consistent with a handshake that never completes.

At 09:40 somebody proposes the obvious shortcut: add the new name to the certificate Common Name, since the Common Name is a hostname and this is a hostname problem. It is tried against a staging host. Not one client behaves any differently, and the team now has a broken cutover, a diagnostic that says the certificate is fine, and a fix that did nothing.

Evidence provided

Read-only / Safefrom the partner network, the failing name
$ curl -sS https://api-eu.example.com/v1/health
curl: (60) SSL: no alternative certificate subject name matches target hostname 'api-eu.example.com'

Illustrative output

Read-only / Safethe same host, the same trust store, the same second, the working name
$ curl -sS -o /dev/null -w 'http_code %{http_code}' https://api.example.com/v1/health; echo
http_code 200

Illustrative output

Read-only / Safethe diagnostic that sent the team in the wrong direction for forty minutes
$ openssl s_client -connect api-eu.example.com:443 -servername api-eu.example.com </dev/null 2>/dev/null | grep 'Verify return code'
Verify return code: 0 (ok)

Illustrative output

Read-only / Safethe leaf that was served in answer to a request for a third name
$ openssl s_client -connect api-eu.example.com:443 -servername api-eu.example.com </dev/null 2>/dev/null | openssl x509 -out app.crt; openssl x509 -in app.crt -noout -subject -ext subjectAltName
subject=CN=api.example.com
X509v3 Subject Alternative Name:
  DNS:api.example.com, DNS:www.api.example.com

Illustrative output

Read-only / Safethe same leaf, checked against the name it does carry
$ openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt -untrusted chain.pem -verify_hostname api.example.com app.crt
app.crt: OK

Illustrative output

Read-only / Safethe same leaf, the same chain, the same command, only the requested name changed
$ openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt -untrusted chain.pem -verify_hostname api-eu.example.com app.crt
error 62 at 0 depth lookup: hostname mismatch

Illustrative output

Work the evidence before reading on

Two commands, one certificate, opposite results, and the only difference between them is a string. Everything else in this incident is noise generated by tools that were asked easier questions.

  1. The diagnostic client reported a verification return code of zero and curl refused to connect, against the same endpoint, seconds apart. Both are correct. What did each of them check, and what did the one reporting success decline to check?
  2. Two openssl verify runs differ in one flag value and produce OK and error 62. What does that pair prove about the chain, the trust store, the clock and the certificate purpose?
  3. The browser showed an interstitial and then, once clicked through, served the API perfectly. Why is that consistent with the certificate being the fault rather than evidence against it?
  4. The Common Name of the served certificate is a hostname, and the staging experiment that added a second name to it changed nothing. Say what a modern client is required to do with the Common Name.

Before continuing: name the two lists that must be equal for this service to work, say where each one is declared, and say which component is responsible for comparing them.

Root cause

The name was added to the service and not to the certificate

The virtual host answers on three names. The certificate vouches for two. A TLS client asks for api-eu.example.com, receives a certificate whose subjectAltName holds api.example.com and www.api.example.com, finds that neither entry matches what it asked for, and aborts. The chain is intact, the certificate is 94 days from expiry, the trust store is correct and the purpose is right. Identity is the single check that fails, and it fails absolutely.

The two openssl verify runs are the proof, because they differ by one flag value against one file. app.crt: OK with the working name and error 62 at 0 depth lookup: hostname mismatch with the failing name is the whole diagnosis in two lines.

The diagnostic client does not check the name unless asked

Verify return code: 0 (ok) was honest and completely irrelevant. A raw TLS diagnostic builds and validates the chain and reports whether that succeeded. Matching the presented identity against the name you intended to reach is a separate step, performed by the application on top of TLS, and the diagnostic omits it by default.

This is why a general-purpose client and a diagnostic client disagreed so sharply. curl is an application: it knows which hostname it meant to reach and it enforces that. The diagnostic knows only which socket it opened. Whenever those two disagree, the application is describing your users and the diagnostic is describing your chain.

The Common Name is not a fallback and has not been one for years

The staging experiment at 09:40 was the right instinct and the wrong decade. RFC 9525 replaced RFC 6125 and removed the Common Name fallback outright: the Common Name relative distinguished name must not be used to identify a service, and neither may any other component of the subject. Clients are required to ignore it.

Certificate authorities may still emit a Common Name, and public issuance rules require that if it is present it is a character for character copy of one of the subjectAltName entries. That is the entire remaining role of the field. Putting a name there that is not in the subjectAltName produces a certificate that looks fixed to a human reading it and behaves identically to the broken one.

Resolution

  1. Prove the failure is identity and not chain or expiry before changing anything. Verify the same served leaf twice, once with -verify_hostname api.example.com and once with -verify_hostname api-eu.example.com, and confirm that only the requested name changes the outcome.
  2. Collect the authoritative name list from the running configuration rather than from the change ticket. Read every server_name entry on the virtual host and every hostname the load balancer listener is configured to accept, and treat the union of those as the set the certificate must cover.
  3. Reissue the certificate with the complete set in the subjectAltName. Every name the service answers on belongs there, including the ones that already worked, because reissuing with only the new name would break the two that were fine.
  4. Install the new certificate and key beside the existing pair without removing the old files. A reload that is rejected leaves you needing the previous certificate within seconds, and deleting it first converts a failed change into a second outage.
  5. Reload nginx rather than restarting it so that established connections drain, then confirm the reload was accepted before you tell anybody the work is done.
  6. Read the certificate back off the listener and confirm the serial has changed. A configuration reload that did not reach the running process leaves the old leaf on the wire and every symptom intact.
  7. Verify each name individually from outside the host before notifying the partner, so that a missing entry is found by you rather than by them for the second time in one morning.
  8. Do not tell the partner to disable verification, even for an hour. That replaces a visible failure with an unauthenticated channel carrying payment traffic, and a workaround handed to an external party during an incident is one you will not be able to withdraw.
  9. Do not add the name to the subject Common Name. Clients are required to ignore it for identity, the staging attempt already demonstrated that, and a certificate that reads as fixed while behaving as broken costs more time than the original fault.

Verification

  1. From the partner network path rather than from the origin host, request the health endpoint on api-eu.example.com with verification enabled and confirm an HTTP 200 rather than a handshake failure.
  2. Repeat that request for every other name the virtual host answers on, so that the reissue has not quietly dropped a name that was previously working.
  3. Read the subjectAltName of the certificate the listener is now presenting and confirm it contains the complete set, checked against the list collected from the configuration rather than from the ticket.
  4. Confirm the serial of the served certificate differs from 2D7A4F1980C6B3E5417F0A9C2E86B41D5307FA62, which proves the running process picked up the new material rather than the filesystem alone.
  5. Run the hostname verification once per name against the served leaf and require OK from every one of them. A certificate satisfying two names out of three fails exactly the way this incident already showed.
  6. Confirm the application access log now records partner requests under the new hostname. A completed handshake is not the same observation as traffic arriving, and only one of them is what the partner cares about.
  7. Confirm with the partner that any verification workaround introduced during the outage has been removed, and record that confirmation rather than assuming it.

Prevention

  • Make the certificate part of the change that adds the name. The names a listener answers on and the names its certificate vouches for are a single decision. Holding them in two repositories guarantees drift, and no reviewer can see a gap between a file they are reading and a file they are not.
  • Derive the issuance name list from the listener configuration. Where the two genuinely must stay separate, generate one from the other so that adding a server_name entry cannot silently fail to reach the certificate.
  • Test set equality, in continuous integration and after deploy. A check that compares configured names against served names and fails on any name present in one and missing from the other would have caught this in the pull request, before a partner ever saw it.
  • Probe every published name at a five minute interval. Per-name probes with hostname verification enabled, alerting on the first failure rather than on a threshold, because a name that fails verification fails every time and never recovers on its own.
  • Match staging to production in certificate topology. A wildcard in staging and named certificates in production means staging can never reproduce the single most common naming failure, and gives false confidence to every change that adds a name.
  • Verify coverage from the client network before DNS is pointed at it. Twenty-four hours ahead of a cutover, request the new name against the target listener from the network the real client will use. That converts this class of incident into a task nobody outside the team ever hears about.