Skip to content

5.2 TLS Secure Client and Fault Diagnosis

The beacon tower in the mirror sends no reply, only this message:

text
ssl.SSLCertVerificationError: certificate verify failed

Retrying won’t fix it automatically. Disabling verification isn’t a solution. You must first determine where the failure occurs in the stack, then investigate the evidence at that layer.

1. Draw the Boundary of the Failure

text
name resolution → TCP connect → TLS handshake → HTTP exchange
SymptomFirst CheckDon't Guess First
Name or service not knownDNS name and resolvercertificate
Connection refusedaddress, port, listenercipher suite
TCP timeoutroute, firewall, service loadHTTP status
CERTIFICATE_VERIFY_FAILEDtrust path, name, time, usageREST route
TLS succeeds but receives 421/404SNI, Host, virtual host, and HTTP routingCA store

This table isn't about instantly identifying the root cause; it's about preventing you from being pulled back into fixing TLS configurations just because an application error occurred.

2. See the handshake with OpenSSL

When checking a virtual host, -servername is not just decoration, it actually sends the SNI extension:

bash
openssl s_client \
  -connect example.com:443 \
  -servername example.com \
  -showcerts \
  -verify_return_error </dev/null

Look for these pieces of evidence in the output:

  • The negotiated TLS version and cipher suite;
  • The leaf certificate’s Subject and Subject Alternative Name;
  • Which intermediate certificates the server actually sends;
  • Verify return code;
  • Whether ALPN selected the expected protocol.

-showcerts shows the server’s certificate list, but this does not mean OpenSSL has verified the trustworthiness of every certificate in that list. The actual verification result must be examined. Hostname verification must be explicitly enabled depending on the OpenSSL command and version you're using, don’t assume it’s active just because the handshake succeeded.

To explicitly verify the hostname, use:

bash
openssl s_client \
  -connect example.com:443 \
  -servername example.com \
  -verify_hostname example.com \
  -verify_return_error </dev/null

3. A Python Client with Default Identity Verification

The Python standard library's ssl.create_default_context() loads secure defaults and default CA certificates for server authentication. The following client does not hardcode a Linux CA path and will not silently disable hostname checking on failure.

python
#!/usr/bin/env python3
import socket
import ssl
import sys


def https_get(host: str, path: str = "/", port: int = 443) -> bytes:
    if not path.startswith("/"):
        raise ValueError("path must use origin-form and start with /")

    context = ssl.create_default_context()
    context.set_alpn_protocols(["http/1.1"])

    with socket.create_connection((host, port), timeout=5) as raw:
        raw.settimeout(5)
        with context.wrap_socket(raw, server_hostname=host) as tls:
            print("TLS:", tls.version())
            print("cipher:", tls.cipher())
            print("ALPN:", tls.selected_alpn_protocol())

            request = (
                f"GET {path} HTTP/1.1\r\n"
                f"Host: {host}\r\n"
                "User-Agent: atlas-tls-client/1\r\n"
                "Accept: */*\r\n"
                "Connection: close\r\n"
                "\r\n"
            ).encode("ascii")
            tls.sendall(request)

            chunks: list[bytes] = []
            while True:
                chunk = tls.recv(65536)
                if not chunk:
                    return b"".join(chunks)
                chunks.append(chunk)


def main() -> None:
    host = sys.argv[1] if len(sys.argv) > 1 else "example.com"
    try:
        response = https_get(host)
    except ssl.SSLCertVerificationError as exc:
        print(f"certificate verification failed: {exc.verify_message}", file=sys.stderr)
        raise SystemExit(2) from exc
    except (ssl.SSLError, OSError) as exc:
        print(f"connection failed: {exc}", file=sys.stderr)
        raise SystemExit(1) from exc

    head, _, _body = response.partition(b"\r\n\r\n")
    print(head.decode("iso-8859-1"))


if __name__ == "__main__":
    main()

Save as tls_client.py and run:

bash
python3 tls_client.py example.com

server_hostname=host supports both SNI and client-side service identity verification. These serve different purposes: SNI helps the server select the appropriate certificate, while hostname verification ensures that the selected certificate matches the client's expected identity.

This program uses only HTTP/1.1 and uses Connection: close to signal the end of the response body to connection closure. It is a TLS observation experiment, not a general-purpose HTTP client: it does not handle redirects, proxies, compression, connection reuse, or streaming body size limits. Production code should use a well-maintained HTTP library.

4. Don't Use These "Patches"

python
# Do not put in production client
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE

This simultaneously removes both the trust path and service identity protection. Traffic may still appear encrypted, but the client has no way of knowing whether the key was negotiated with the real server or an attacker.

A more common correct fix is:

  • Complete the server's intermediate certificate chain;
  • Use a certificate that correctly covers the intended DNS name;
  • Correct the system clock;
  • Configure a private CA as a clear trust anchor for specific clients;
  • Fix the enterprise trust configuration in proxies or TLS interception systems;
  • Record and complete certificate renewal, rather than temporarily ignoring expiry.

5. Private CA and mTLS

If the road is used only within a private city-state, clients can explicitly trust that city-state's private CA:

python
context = ssl.create_default_context(cafile="company-root-ca.pem")

This is entirely different from CERT_NONE: the former modifies the trust anchor set while still performing certificate and hostname verification; the latter disables all verification.

mTLS also requires the client to present a certificate:

python
context = ssl.create_default_context(cafile="company-root-ca.pem")
context.load_cert_chain("client-cert.pem", "client-key.pem")

mTLS can authenticate a client's certificate identity, but it does not automatically grant application authorization. The server must still map the certificate identity to a specific principal and then determine whether that principal is authorized to perform the current operation. The private key must also have access control policies and rotation policies in place.

6. From Error to Root Cause

Certificate expired

Verify the current time, the leaf certificate's validity period, and the renewal job schedule. Don't just replace the leaf certificate, also confirm that the server is actually reloading the full chain file it references.

Unable to get local issuer certificate

Compare the list of intermediate certificates sent by the server with the expected intermediates. Don't assume every client will automatically fetch and complete the chain from the internet.

Hostname mismatch

Check the actual URL hostname, the SNI value, and the leaf certificate's subjectAltName. Simply replacing a URL with an IP address often simultaneously breaks virtual hosting and identity verification.

Protocol version / no shared cipher

First, verify the supported protocol versions and security policies on both client and server. Then, examine the intersection of cipher suites, key exchange groups, and signature algorithms. Don't blindly re-enable TLS 1.0/1.1 or weak algorithms across the entire system to support a single old client. Instead, prioritize upgrading or isolating that client, and evaluate the compatibility boundary based on current TLS best practices.

7. Exercises and Verification

  1. Use openssl s_client to observe a host you're authorized to test, and record the TLS version, cipher, ALPN, and verification result.
  2. On your local machine, change the host name in the Python client to one that doesn't match the certificate, and observe verify_message; do not disable verification.
  3. Explain in one sentence what SNI, hostname verification, and HTTP Host do.
  4. Design a private CA configuration for an internal service, specifying where the trust anchor is stored, who is authorized to update it, and how certificate rotation works.
  5. Explain why mTLS authentication succeeds doesn’t mean authorization is automatically granted.

The verification criteria aren’t memorizing a list of cipher names, it’s about being able to point to evidence that clearly identifies where a failure occurs: DNS, TCP, TLS, or HTTP, and never dismissing errors by disabling verification.

In the mirror, the beacon tower finally presents a credential proving access to the trust anchor, and the name matches the destination you came to reach. The letter can now enter the protected channel. Beyond that, you’ll notice some conversations don’t want to open and re-inspect envelopes every time, they’d rather maintain a persistent, bidirectional channel. The next stop is WebSocket and gRPC.

References

Built with VitePress | Software Systems Atlas