5.2 TLS 安全客户端与故障诊断
观测镜里的信标塔没有回信,只留下一行:
ssl.SSLCertVerificationError: certificate verify failed重试不会自动修复它,关掉 verification 更不是修复。你需要先判断故障停在哪一层,再查该层的证据。
1. 先给故障画边界
name resolution → TCP connect → TLS handshake → HTTP exchange| 现象 | 先查 | 不要先猜 |
|---|---|---|
Name or service not known | DNS name 与 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 成功后收到 421/404 | SNI、Host、virtual host 与 HTTP routing | CA store |
这张表的目的不是一眼猜中 root cause,而是别让一个 application error 把你拖回去改 TLS configuration。
2. 用 OpenSSL 看见 handshake
检查 virtual host 时,-servername 不是装饰,它会发 SNI:
openssl s_client \
-connect example.com:443 \
-servername example.com \
-showcerts \
-verify_return_error </dev/null从 output 里分别找这些证据:
- negotiated TLS version 和 cipher suite;
- leaf 的 Subject / Subject Alternative Name;
- server 实际发送了哪些 intermediate certificate;
Verify return code;- ALPN 是否选中预期 protocol。
-showcerts 显示 server 发来的 certificate list,它不等于 OpenSSL 已经为你证明每一张 certificate 都可信。结论要看 verification result,并且 hostname verification 要按你使用的 OpenSSL command/version 明确开启,不要只看“handshake succeeded”。
如果需要明确验证 hostname,可用:
openssl s_client \
-connect example.com:443 \
-servername example.com \
-verify_hostname example.com \
-verify_return_error </dev/null3. 一个默认验证身份的 Python client
Python 标准库的 ssl.create_default_context() 会为 server authentication 加载 secure defaults 和 default CA certificates。下面这个 client 不硬编码 Linux CA path,也不会在失败时悄悄关闭 hostname checking。
#!/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()保存为 tls_client.py 后运行:
python3 tls_client.py example.comserver_hostname=host 同时支持 SNI 和 client-side service identity check。两者目的不同:SNI 帮 server 选 certificate,hostname verification 检查选出的 certificate 是否匹配 client 的期望身份。
这个程序只使用 HTTP/1.1,并且用 Connection: close 把 response body 的结束交给 connection close。它是 TLS 观察实验,不是通用 HTTP client:没处理 redirect、proxy、compression、connection reuse 或 streaming body size limit。Production code 应使用维护良好的 HTTP library。
4. 不要用这些“修复”
# 不要放进 production client
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE这会同时拿掉 trust path 与 service identity 保护。Traffic 仍然可能显示为 encrypted,但 client 不知道 key 是和真 server 还是 attacker 协商的。
更常见的正确修复是:
- 补齐 server 的 intermediate chain;
- 换用覆盖正确 DNS name 的 certificate;
- 修正 system clock;
- 把 private CA 作为明确 trust anchor 配置给指定 client;
- 修复 proxy 或 TLS interception 的 enterprise trust configuration;
- 记录并完成 certificate renewal,而不是临时忽略 expiry。
5. Private CA 和 mTLS
如果驿道只在一座私有城邦中使用,client 可以明确信任该城邦的 private CA:
context = ssl.create_default_context(cafile="company-root-ca.pem")这与 CERT_NONE 完全不同:前者改变了 trust anchor 集合,仍然执行 certificate 和 hostname verification;后者取消验证。
mTLS 还要求 client 出示 certificate:
context = ssl.create_default_context(cafile="company-root-ca.pem")
context.load_cert_chain("client-cert.pem", "client-key.pem")mTLS 能 authentication client certificate identity,却不自动完成 application authorization。Server 仍要把 certificate identity 映射到明确 principal,再判断这个 principal 能否执行当前 operation。Private key 也要有 access control 和 rotation policy。
6. 从错误走到 root cause
Certificate expired
确认当前时间、leaf validity period 和 renewal job。不要只换 leaf,还要检查 server 引用的 full chain 文件是否真正 reload。
Unable to get local issuer certificate
把 server 发送的 list 与预期 intermediate 对比。不要假设每个 client 都会从网上自动补链。
Hostname mismatch
检查实际 URL name、SNI 和 leaf subjectAltName。直接把 URL 换成 IP 常会让 virtual hosting 和 identity verification 同时失效。
Protocol version / no shared cipher
先确认 client 与 server 的 supported versions 和 policy,再检查 cipher/group/signature 交集。不要为了一台旧 client 盲目全局恢复 TLS 1.0/1.1 或 weak algorithms;优先 upgrade / isolate 该 client,并以当前 TLS guidance 评估兼容边界。
7. 练习与验收
- 用
openssl s_client观察一个你有权测试的 host,记录 TLS version、cipher、ALPN 和 verification result。 - 在本地将 Python client 的 host 换成与 certificate 不匹配的 name,观察
verify_message;不要关闭 verification。 - 用一句话分别解释 SNI、hostname verification 和 HTTP
Host。 - 为 internal service 设计 private CA 配置,写出 trust anchor 放在哪里、谁能更新、如何 rotation。
- 解释为什么 mTLS authentication 成功后仍要 authorization。
验收标准不是背出一串 cipher 名,而是能用证据说清故障停在 DNS、TCP、TLS 还是 HTTP,且不会用“关掉 verification”来清除报错。
观测镜里,信标塔终于出示了能连到 trust anchor 的通行文书,名字也与你此行的目的地一致。信函可以进入 protected channel 了。再往前,你会发现有些对话不想每次都拆信封,而想保留一条双向通道——下一站是WebSocket 与 gRPC。