5.2 TLS Secure Client and Fault Diagnosis
The beacon tower in the mirror sends no reply, only this message:
ssl.SSLCertVerificationError: certificate verify failedRetrying 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
name resolution → TCP connect → TLS handshake → HTTP exchange| Symptom | First Check | Don't Guess First |
|---|---|---|
Name or service not known | DNS name and resolver | certificate |
Connection refused | address, port, listener | cipher suite |
| TCP timeout | route, firewall, service load | HTTP status |
CERTIFICATE_VERIFY_FAILED | trust path, name, time, usage | REST route |
| TLS succeeds but receives 421/404 | SNI, Host, virtual host, and HTTP routing | CA 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:
openssl s_client \
-connect example.com:443 \
-servername example.com \
-showcerts \
-verify_return_error </dev/nullLook 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:
openssl s_client \
-connect example.com:443 \
-servername example.com \
-verify_hostname example.com \
-verify_return_error </dev/null3. 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.
#!/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:
python3 tls_client.py example.comserver_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"
# Do not put in production client
context.check_hostname = False
context.verify_mode = ssl.CERT_NONEThis 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:
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:
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
- Use
openssl s_clientto observe a host you're authorized to test, and record the TLS version, cipher, ALPN, and verification result. - 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. - Explain in one sentence what SNI, hostname verification, and HTTP
Hostdo. - 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.
- 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.