nslookup 与 dig 的区别,以及一次代理引起的证书校验失败
Posted on 六 15 8月 2026 in Tech
| Abstract | nslookup 与 dig 的区别,以及一次代理引起的证书校验失败 |
|---|---|
| Authors | Walter Fan |
| Category | Network / Security |
| Status | v1.1 |
| Updated | 2026-08-15 |
| License | CC-BY-NC-ND 4.0 |
nslookup 与 dig 的区别,以及一次代理引起的证书校验失败
有一类问题,我一年要碰上好几回:某个服务在我机器上跑得好好的,换到另一台机器就报证书错误;或者反过来,在公司网里跑不通,回家连自己的 WiFi 就通了。同事发来的截图通常只有一行红字:certificate verify failed。
大部分人接着做的第一件事,是去改代码——加 verify=False,或者把 NODE_TLS_REJECT_UNAUTHORIZED 设成 0。这一步确实能让报错消失,但也顺手把 HTTPS 里"防止别人冒充服务器"的那部分保护给关掉了。更要紧的是,你到最后也不知道刚才那个错到底是谁造成的。
我的经验是,这类问题有个很省事的顺序:先用 openssl 看服务器给了你什么证书,再用 dig 看你连的到底是不是你以为的那台机器。 前者告诉你"是谁签的",后者告诉你"你在跟谁说话"。这篇文章把这两步讲透——顺带把 nslookup 和 dig 的分工掰扯清楚,因为它俩看着像,用起来差别不小。文末有复现脚本、排查清单,以及把这套顺序收成的 AI skill lazy-tls-doctor。
- 讲什么:两个 DNS 工具的定位差异、代理劫持 HTTPS 的原理、
openssl s_client的读法、按顺序定位的排查法、以及对应的 AI skill - 不讲什么:DNSSEC 验签细节、证书链修复的各种语言写法(我在《一次 HTTPS 证书报错排查》里写过客户端侧的修法)
下文的命令输出都是我在 macOS 上真实跑出来的(DiG 9.10.6、OpenSSL 3.6.2),只把内网 DNS 的 IP 和内部主机名做了脱敏,行为和结论没动。
一、先说 nslookup 和 dig 到底差在哪
两个工具都能把域名换成 IP。区别在于它们默认愿意告诉你多少事。
先看 nslookup:
$ nslookup www.fanyamin.com
Server: 10.0.0.53
Address: 10.0.0.53#53
Non-authoritative answer:
Name: www.fanyamin.com
Address: 47.96.84.118
干净、好读,一眼就知道 IP 是 47.96.84.118。它把结论端到你面前,中间过程一概不提。
再看 dig 查同一个域名:
$ dig www.fanyamin.com
; <<>> DiG 9.10.6 <<>> www.fanyamin.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32297
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;www.fanyamin.com. IN A
;; ANSWER SECTION:
www.fanyamin.com. 4 IN A 47.96.84.118
;; Query time: 33 msec
;; SERVER: 10.0.0.53#53(10.0.0.53)
;; WHEN: Sat Aug 15 11:04:15 CST 2026
;; MSG SIZE rcvd: 61
同样一个 IP,dig 多说了一堆。这堆东西不是啰嗦,每一行在排查时都能派上用场:
status: NOERROR—— DNS 层面的返回码。查不到会是NXDOMAIN,服务端出错会是SERVFAIL。这三者的处置方式完全不同。flags: qr rd ra——ra表示"这台服务器帮我做了递归查询";如果没有aa,说明这答案不是权威服务器亲口说的,是从缓存里拿的。www.fanyamin.com. 4 IN A—— 那个4是 TTL,单位是秒。这一个数字在排查缓存问题时值一百句话:TTL 只剩 4 秒,说明这条记录马上要过期重查;如果你刚改了 DNS 却不生效,先看 TTL 还剩多少。SERVER: 10.0.0.53#53—— 这个答案是谁给你的。公司网里这一行尤其重要,它经常是内网 DNS,跟公网给的答案不一样。
一句话总结这个差别:
nslookup回答"解析成什么",dig回答"这个答案是谁给的、凭什么、还能活多久"。 前者适合确认,后者适合追责。
一张对照表
| 维度 | nslookup | dig |
|---|---|---|
| 设计年代 | 早年随 BIND 发布,曾被官方标记为废弃,后又恢复维护 | BIND 9 主推的查询工具 |
| 默认输出 | 结论式,干净 | 完整 DNS 报文,含 header、flags、TTL |
| 交互模式 | 有(直接敲 nslookup 进入) |
无,一次一条命令 |
| 看得到 TTL | 看不到 | 看得到 |
| 看得到返回码 | 只能从文字推测 | status: 直接给 |
| 追踪完整解析链 | 不支持 | +trace |
| 适合写脚本 | 输出格式不稳定,不推荐 | +short、+noall +answer 很好切 |
| 学习成本 | 低 | 略高,但值得 |
有一点要澄清:网上常说 nslookup "已经废弃了"。这个说法有出处——BIND 9 早期,ISC 确实计划废弃 nslookup,改推 host 和 dig。但这个决定在 2004 年随 BIND 9.3 发布时被撤销了,此后 nslookup 一直是被完整支持的。所以不必刻意躲着它,尤其在 Windows 上——那儿默认只有 nslookup,dig 得自己装。
不过 ISC 当年想废弃它的理由至今仍然成立:nslookup 的交互模式行为古怪,输出格式不稳定,而且它藏起了协议细节。所以我的习惯是:确认一个 IP 用 nslookup 随手一敲,一旦要排查故障就换 dig。
dig 真正拉开差距的三个场景
第一个:指定问哪台 DNS 服务器。 这是排查"为什么我这儿不通他那儿通"的核心动作。
$ dig +short www.fanyamin.com
47.96.84.118
$ dig @8.8.8.8 +short www.fanyamin.com
47.96.84.118
两个答案一致,说明解析没被改。如果这两个不一致,问题基本就锁定在 DNS 上了——要么内网做了域名覆盖,要么有人劫持。nslookup 也能指定服务器(nslookup 域名 服务器),但参数顺序反人类,我总记错。
第二个:+trace 从根域名服务器一级级往下问。
$ dig +trace www.fanyamin.com
. 78437 IN NS h.root-servers.net.
. 78437 IN NS a.root-servers.net.
...
;; Received 683 bytes from 10.0.0.53#53(10.0.0.53) in 34 ms
com. 172800 IN NS c.gtld-servers.net.
com. 172800 IN NS m.gtld-servers.net.
...
这个模式绕过本地缓存,从根服务器开始一跳跳往下走。域名刚改完 NS、或者怀疑某一级委派配错了,用它最直接。
第三个:输出好切,适合塞进脚本。
$ dig +short www.fanyamin.com
47.96.84.118
$ dig +noall +answer www.fanyamin.com A
www.fanyamin.com. 4 IN A 47.96.84.118
+short 只给值,+noall +answer 给结构化的一行。
两个我踩过的坑
坑一:dig 查不到东西时,退出码仍然是 0。
这一条害过我。我曾经写过一个健康检查脚本,用 dig 判断域名是否可解析,靠退出码判断成败——结果域名早就没了,脚本还一直报成功:
$ dig no-such-domain-xyz987654.com +short >/dev/null 2>&1; echo "dig exit=$?"
dig exit=0
$ nslookup no-such-domain-xyz987654.com >/dev/null 2>&1; echo "nslookup exit=$?"
nslookup exit=1
dig 的逻辑是"我成功地完成了一次查询,只不过答案是没有",所以返回 0。它只在自己出问题时才给非零码(比如连不上服务器时给 9)。写脚本要判断"到底解析出来了没有",得看内容或返回码,别看退出码:
# 判断有没有解析结果
if [ -z "$(dig +short "$name" A)" ]; then echo "解析不到"; fi
# 或者直接看 DNS 返回码
dig +noall +comments "$name" | grep -o 'status: [A-Z]*'
# => status: NXDOMAIN
坑二:两个工具都不看 /etc/hosts。
这个坑最阴,因为它让工具和程序的看法出现分歧。我这台机器的 /etc/hosts 里有一行内部服务的映射(下面把主机名和 IP 做了脱敏,行为是真实的):
$ grep internal-svc /etc/hosts
10.0.0.9 internal-svc.corp.example
用两个 DNS 工具去查,都说查不到:
$ dig +short internal-svc.corp.example
(空,NXDOMAIN)
$ nslookup internal-svc.corp.example
** server can't find internal-svc.corp.example: NXDOMAIN
但程序连得上:
$ ping -c1 internal-svc.corp.example
PING internal-svc.corp.example (10.0.0.9): 56 data bytes
64 bytes from 10.0.0.9: icmp_seq=0 ttl=60 time=41.117 ms
原因是 dig 和 nslookup 都是直接对 DNS 服务器发包,走的是纯 DNS 协议;而绝大多数程序走的是操作系统的名字解析(getaddrinfo / gethostbyname),那一套会按本地策略依次查——Linux 上由 /etc/nsswitch.conf 里的 hosts: files dns 决定"先查文件再查 DNS",macOS 上则由系统的 DirectoryService 一套规则决定。所以:
dig说查不到,不代表程序连不上;dig说是这个 IP,也不代表程序连的是这个 IP。
要看"程序眼里"的解析结果,Linux 上用 getent hosts 域名,macOS 上用 dscacheutil -q host -a name 域名。这个区别待会儿排查证书问题时还要再用一次。
二、代理是怎么把证书搞坏的
现在换到证书这条线。先把原理讲明白,不然后面的实验只是敲命令。
HTTPS 的信任建立在一条链上:服务器给你一张证书,这张证书由某个中间 CA 签发,中间 CA 又由某个根 CA 签发,而那个根 CA 的证书预先装在你的操作系统或浏览器里。你的客户端顺着这条链往上找,只要能找到一个自己信任的根,就认。
公司的上网代理(各种 SSL 检查、上网行为管理、零信任网关都属于这类)要看 HTTPS 里的内容,就必须拆开加密。而 TLS 的设计不允许旁观者解密,所以代理只能扮演"中间人":
flowchart LR
subgraph 正常
A[客户端] -->|"TLS,验证到公共 CA"| B[真实服务器]
end
subgraph 被代理拆开
C[客户端] -->|"TLS 第一段<br/>证书由代理 CA 签"| D[代理]
D -->|"TLS 第二段<br/>代理自己去验真实证书"| E[真实服务器]
end
代理站在中间,对服务器扮演客户端,对客户端扮演服务器。为了扮演成 api.example.com,它必须当场伪造一张 api.example.com 的证书,用自己的私有 CA 去签。
这张伪造证书的域名是对的、有效期是新的、格式完全合法——唯一的问题是签它的那个 CA,不在你的信任列表里。于是客户端顺着链往上找根,找不到,报错:
unable to get local issuer certificate
这句话直译是"找不到本地的签发者证书",说人话就是:"这张证书的爹我不认识。"
公司发的电脑通常由 IT 提前把代理的 CA 装进了系统信任库,所以员工日常上网毫无察觉。出问题的往往是这几种情况:
- 你自己装的 Python / Node / Java / Go,用的是自带的证书库,而不是系统证书库——IT 装的那个 CA 它压根看不见
- 容器里的程序,镜像里只有一套精简的公共 CA 列表
- CI 跑在一台没做过信任配置的构建机上
- 你换了网络(比如从公司网切到访客网),走的代理变了
这也解释了那个经典现象:浏览器打得开,代码连不上。 不是代码写错了,是两者查的信任库不是同一个。
三、动手复现:在本机造一个"代理劫持"
光讲原理容易忘。下面这段脚本在你自己机器上完整复现一次,只用 openssl,不装任何东西,不改系统信任库,跑完自动清理。
它做四件事:造一个假装是公司代理的 CA、用它伪造一张 api.example.com 的证书、用这张证书起一个本地服务、然后用客户端去连并看报错。
#!/usr/bin/env bash
# 复现"代理导致证书校验失败"的最小实验
set -euo pipefail
WORK=$(mktemp -d); cd "$WORK"
TARGET="api.example.com"
PORT=4444
echo "工作目录: $WORK"
# 1. 造一个"公司代理"的根 CA —— 现实中它由代理设备自动生成
openssl req -x509 -newkey rsa:2048 -sha256 -days 30 -nodes \
-keyout proxyCA.key -out proxyCA.crt \
-subj "/C=CN/O=Acme Corp IT/CN=Acme Corp Proxy Root CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" 2>/dev/null
# 2. 代理用自己的 CA,现签一张 api.example.com 的证书
cat > leaf.cnf <<EOF
[req]
distinguished_name = dn
prompt = no
[dn]
CN = $TARGET
[ext]
subjectAltName = DNS:$TARGET
basicConstraints = CA:FALSE
extendedKeyUsage = serverAuth
EOF
openssl req -new -newkey rsa:2048 -nodes \
-keyout leaf.key -out leaf.csr -config leaf.cnf 2>/dev/null
openssl x509 -req -in leaf.csr -CA proxyCA.crt -CAkey proxyCA.key \
-CAcreateserial -out leaf.crt -days 30 -sha256 \
-extfile leaf.cnf -extensions ext 2>/dev/null
# 3. 用这张伪造证书起一个假的 api.example.com
openssl s_server -accept $PORT -cert leaf.crt -key leaf.key -www -quiet >/dev/null 2>&1 &
SRV=$!; trap 'kill $SRV 2>/dev/null' EXIT
sleep 1
echo
echo "===== 第一步:客户端视角,报错长什么样 ====="
echo | openssl s_client -connect 127.0.0.1:$PORT -servername "$TARGET" 2>&1 \
| grep -E "verify error|subject=|issuer=|Verify return code" || true
echo
echo "===== 第二步:只看 issuer,就知道是谁在中间 ====="
echo | openssl s_client -connect 127.0.0.1:$PORT -servername "$TARGET" 2>&1 \
| openssl x509 -noout -issuer -subject -dates 2>/dev/null
echo
echo "===== 第三步:拿代理 CA 去验,一次就验过 —— 罪证 ====="
echo | openssl s_client -connect 127.0.0.1:$PORT -servername "$TARGET" -CAfile proxyCA.crt 2>&1 \
| grep -E "Verify return code"
我在 macOS 上用 OpenSSL 3.6.2 跑出来的实际输出:
工作目录: /var/folders/xk/.../tmp.KyipedvLHD
===== 第一步:客户端视角,报错长什么样 =====
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
subject=CN=api.example.com
issuer=C=CN, O=Acme Corp IT, CN=Acme Corp Proxy Root CA
Verify return code: 21 (unable to verify the first certificate)
===== 第二步:只看 issuer,就知道是谁在中间 =====
issuer=C=CN, O=Acme Corp IT, CN=Acme Corp Proxy Root CA
subject=CN=api.example.com
notBefore=Aug 15 03:06:26 2026 GMT
notAfter=Sep 14 03:06:26 2026 GMT
===== 第三步:拿代理 CA 去验,一次就验过 —— 罪证 =====
Verify return code: 0 (ok)
这三段输出,每一段都在回答一个具体问题。
第一段是你在应用日志里会看到的那个错。注意 subject=CN=api.example.com——域名是对的。很多人一看到证书报错就以为是域名不匹配,其实这里域名完全正确,坏的是签发者。
第二段是整个排查里性价比最高的一行。issuer 写着 Acme Corp IT / Acme Corp Proxy Root CA——真实的 api.example.com 绝不可能用一个叫"Acme Corp IT"的 CA。issuer 里出现公司名、设备品牌名(Zscaler、Netskope、Palo Alto、Fortinet、Blue Coat 之类),基本就可以确认是代理在拆包了。 顺便看一眼 notBefore:代理签的证书往往是"今天刚签的",因为它是连接时现场生成的。
第三段是坐实结论。把代理的 CA 显式传给 -CAfile,验证立刻从 21 变成 0 (ok)。这说明证书本身没坏,链条也是完整的——只是这条链的根不在系统的信任列表里。问题不在证书,在信任配置。
同一个连接,curl 的说法是这样:
$ curl -sS --resolve api.example.com:4444:127.0.0.1 https://api.example.com:4444/
curl: (60) SSL certificate problem: unable to get local issuer certificate
那几个数字是什么意思
openssl 的 verify 错误码看着像天书,其实常见的就几个,记住能省不少时间:
| 错误码 | 文字 | 说的是什么 | 典型原因 |
|---|---|---|---|
| 18 | self signed certificate | 自签名证书 | 测试环境、设备自带证书 |
| 19 | self signed certificate in certificate chain | 链里有自签根 | 私有 CA,且根不被信任 |
| 20 | unable to get local issuer certificate | 找不到签发者 | 代理劫持、缺中间证书、信任库不对 |
| 21 | unable to verify the first certificate | 第一张就验不下去 | 服务器没发中间证书,或链断了 |
| 62 | hostname mismatch | 域名不匹配 | 连错了机器、SNI 没发对、证书域名写错 |
(这几个数字对应 OpenSSL 头文件里的 X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT、X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN、X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY、X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE、X509_V_ERR_HOSTNAME_MISMATCH。)
20 和 21 经常一起出现,像上面的例子那样。要区分"是代理插进来了"还是"服务器忘了发中间证书",看 -showcerts 给出的链有几张。伪造的那张是孤零零一张:
$ echo | openssl s_client -connect 127.0.0.1:4444 -servername api.example.com -showcerts 2>&1 \
| grep -E "^ [0-9] s:|^ *i:"
0 s:CN=api.example.com
i:C=CN, O=Acme Corp IT, CN=Acme Corp Proxy Root CA
正常站点会给你一串:
$ echo | openssl s_client -connect www.fanyamin.com:443 -servername www.fanyamin.com -showcerts 2>&1 \
| grep -E "^ [0-9] s:|^ *i:"
0 s:CN=www.fanyamin.com
i:C=US, O=Let's Encrypt, CN=YE2
1 s:C=US, O=Let's Encrypt, CN=YE2
i:C=US, O=ISRG, CN=Root YE
2 s:C=US, O=ISRG, CN=Root YE
i:C=US, O=Internet Security Research Group, CN=ISRG Root X2
所以:只有一张、且 issuer 是个陌生的公司名,是代理;只有一张、issuer 是正常的公共 CA(Let's Encrypt、DigiCert 之类),那多半是服务器少配了中间证书。
一个容易误判的地方:s_client 默认不校验域名
这一条得单独说,因为它会让你得出错误结论。-servername 只负责发送 SNI,它并不让 openssl s_client 去核对"证书上的域名是否与我要访问的一致"。我用刚才那张 api.example.com 的证书试了一下,故意把 SNI 写成一个完全无关的域名:
$ echo | openssl s_client -connect 127.0.0.1:4444 \
-servername totally-wrong.example.org -CAfile proxyCA.crt 2>&1 \
| grep -E "subject=|Verify return code"
subject=CN=api.example.com
Verify return code: 0 (ok)
域名压根对不上,openssl 却说 0 (ok)。要让它真的查这一项,得显式加 -verify_hostname:
$ echo | openssl s_client -connect 127.0.0.1:4444 \
-CAfile proxyCA.crt -verify_hostname totally-wrong.example.org 2>&1 \
| grep -E "verify error|Verify return code"
verify error:num=62:hostname mismatch
Verify return code: 62 (hostname mismatch)
这就是为什么 openssl 说 ok、浏览器和 curl 仍然报错——它们默认查域名,s_client 默认不查。 排查时如果要把域名这一项也纳入,记得加 -verify_hostname;否则请以 curl 的结论为准。
作为对照,同一台机器上连一个没被拆包的站点是这样:
$ echo | openssl s_client -connect www.fanyamin.com:443 -servername www.fanyamin.com 2>&1 \
| grep -E "subject=|issuer=|Verify return code"
subject=CN=www.fanyamin.com
issuer=C=US, O=Let's Encrypt, CN=YE2
Verify return code: 0 (ok)
issuer 是 Let's Encrypt,返回码 0。一眼就能和上面的假证书区分开。
真实环境里怎么连
上面的实验用本地端口模拟。真实排查时,直接对目标发起连接就行:
# 直连(若环境本身走透明代理,这条就会暴露问题)
echo | openssl s_client -connect api.example.com:443 -servername api.example.com
# 显式经过一个 HTTP 代理(openssl 自己会发 CONNECT)
echo | openssl s_client -proxy 10.0.0.1:8080 -connect api.example.com:443 -servername api.example.com
两个参数别省:-servername 发的是 SNI,现在同一个 IP 上跑几十个站点是常态,不发 SNI 很可能拿到一张完全无关的证书,白折腾半天;echo | 是给它一个立即的 EOF,否则命令会挂在那儿等你输入。
四、把两条线接起来:完整的排查顺序
现在回到最开始那个场景:程序报证书错误。按下面的顺序走,通常三四步就能定位。
第一步,看服务器给了什么证书。
echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>&1 \
| grep -E "subject=|issuer=|Verify return code"
看 issuer。是熟悉的公共 CA,跳到第三步;是公司名或安全设备品牌名,代理拆包已经确认,跳到第四步。
第二步,如果域名和你预期的不一样,转去查 DNS。
如果报的是 62(hostname mismatch),或者证书里的域名压根不是你要连的那个,说明你连的机器可能不是你以为的那台。这时候 dig 上场:
# 本地解析结果
dig +short api.example.com
# 换一个公共 DNS 对照
dig @8.8.8.8 +short api.example.com
# 看权威服务器怎么说
dig +short NS example.com
dig @$(dig +short NS example.com | head -1) api.example.com +noall +answer
三个答案一致,DNS 没问题,回到证书这条线。不一致,问题就在 DNS——内网做了覆盖,或者被劫持了。
还有一个容易忽略的情况:CNAME 链。dig 会把整条链摊开给你看:
$ dig +noall +answer www.baidu.com
www.baidu.com. 197 IN CNAME www.a.shifen.com.
www.a.shifen.com. 33 IN A 180.101.49.44
www.a.shifen.com. 33 IN A 180.101.51.73
域名指向了另一个域名。如果最终那台服务器的证书只签了 CNAME 目标的名字、没签你访问的名字,就会报域名不匹配——CDN 和各种托管服务配错时,这是高频原因。
第三步,别忘了 /etc/hosts。 这就是前面那个坑的用处。dig 看不到 hosts 文件,所以两个工具的答案可能对不上。程序连的是 hosts 里那个 IP,你却在拿 dig 的结果推理:
# dig 说的(纯 DNS)
dig +short api.example.com
# 程序眼里的(含 /etc/hosts)
getent hosts api.example.com # Linux
dscacheutil -q host -a name api.example.com # macOS
# 别漏了这个
grep api.example.com /etc/hosts
我见过好几次"证书域名对不上",最后发现是某位同事几个月前为了联调,往 hosts 里加了一行,然后忘了删。
第四步,确认是代理,然后决定怎么处理。
拿到代理的 CA 证书(IT 那儿有,或者从上面 -showcerts 的输出里抠出来),验一下:
echo | openssl s_client -connect api.example.com:443 -servername api.example.com \
-CAfile /path/to/proxyCA.crt 2>&1 | grep "Verify return code"
变成 0 (ok),结论就定了:证书没坏,是你的程序不信任这个 CA。剩下的是把 CA 告诉程序——各个运行时认的路子不一样:
| 运行时 | 怎么告诉它 |
|---|---|
| curl / git | CURL_CA_BUNDLE 或 git config http.sslCAInfo |
| Python requests | REQUESTS_CA_BUNDLE 环境变量 |
| Python 标准库 ssl | SSL_CERT_FILE,或代码里 ssl.SSLContext.load_verify_locations() |
| Node.js | NODE_EXTRA_CA_CERTS 指向 PEM 文件 |
| Java | keytool -importcert 导进 truststore |
| Go | 读系统信任库,把 CA 装进系统即可 |
| Docker 镜像 | CA 拷进 /usr/local/share/ca-certificates/,再跑 update-ca-certificates |
注意最后两行的差别:Go 读系统库,所以系统装好就行;Node 和 Python 各有自己的一套,得单独喂。这正是"浏览器行、代码不行"的根源。
至于 verify=False: 调试时临时用一下可以,但别留在代码里、更别提交。关掉校验之后,你不再能确认对面是不是真的服务器,加密还在、身份认证没了——而身份认证恰恰是 HTTPS 防中间人的那一半。真要临时跑通,宁可用 -CAfile 显式指定 CA,那样至少你知道自己在信任谁。
五、一份可以照着走的清单
遇到证书报错,按这个顺序:
openssl s_client -connect 域名:443 -servername 域名,先看issuer和Verify return codeissuer是公司名 / 安全设备名 → 代理拆包,跳到第 6 条issuer正常但报 21 → 服务器可能少发中间证书,用-showcerts数一下链有几张- 报 62(域名不匹配)→ 用
dig +short对比本地和公共 DNS 的解析结果 - 两个 DNS 答案不一致,或
dig与getent不一致 → 查内网覆盖、CNAME 链、/etc/hosts - 拿代理 CA 加
-CAfile验一次,变成0 (ok)就坐实了 - 按运行时的方式把 CA 配进去,别用
verify=False收尾 - 嫌手工敲命令?用
lazy-tls-doctor按同一顺序跑一遍
两个 DNS 工具怎么选:
- 只想快速确认一个 IP,随手
nslookup或者dig +short - 要判断"答案是谁给的"、"缓存还剩多久"、"哪一级委派错了" →
dig,尤其是@服务器和+trace - 写脚本 → 用
dig +short,但别信它的退出码,要看输出内容或status: - 排查"程序和工具说法不一致" → 记住两个工具都跳过
/etc/hosts,用getent hosts或dscacheutil看程序视角
三个记得住的判断:
证书报错先看
issuer,不看subject。域名对不对是次要的,谁签的才是关键。
dig查不到不等于程序连不上。工具走纯 DNS,程序走系统解析,两条路。关掉证书校验不是修好了,是把报警器拆了。
六、把这套方法收成一个 AI skill:lazy-tls-doctor
文章写到这儿,顺序已经齐了:看 issuer、数证书链、对照 dig、区分信任库。但每次从文章里抠命令再敲一遍,还是费事。所以我把上面这套排查法收成了一个 skill:lazy-tls-doctor。
它的定位很明确——把一句红字 certificate verify failed 变成一个可执行的结论:谁有问题,该改哪一侧。 不会建议你关掉校验。四种常见原因各自对应不同修法:
| 原因 | 怎么认 | 该谁改 |
|---|---|---|
| 代理拆包 | issuer 是公司名或安全设备品牌 | 信任库——装上代理 CA |
| 缺中间证书 | 链只有一张,issuer 却是公共 CA | 服务器——发 fullchain |
| 私有根不被信任 | 链终点是陌生的自签根 | 信任库——装上那个根 |
| 连错了主机 | 域名不匹配,证书本身合法 | DNS 或路由 |
在支持 skill 的 AI coding 工具里,说一句就行:
please use lazy-tls-doctor to check www.fanyamin.com
对我自己的站点跑出来是这样的(截取关键部分):
Checked `www.fanyamin.com:443`.
- TLS: 1.3, `TLS_AES_256_GCM_SHA384`
- Verification: `0 (ok)`
- DNS: `47.96.84.118`
- Verdict: `ok` — high confidence
| Depth | Certificate | Issuer | Validity | SAN |
|---|---|---|---|---|
| 0 | www.fanyamin.com |
Let's Encrypt YE2 |
Jul 29–Oct 27, 2026 | DNS:www.fanyamin.com |
| 1 | Let's Encrypt YE2 |
Root YE |
Sep 2025–Sep 2028 | — |
| 2 | Root YE |
ISRG Root X2 |
May 2026–Sep 2032 | — |
| 3 | ISRG Root X2 |
ISRG Root X1 |
May 2026–Sep 2032 | — |
证书链会画成:
www.fanyamin.com
-> Let's Encrypt YE2
-> Root YE
-> ISRG Root X2
-> ISRG Root X1
[issuer not sent by server]
它还会给出可复制的手工命令,方便你自己复现(注意用了 `