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 看你连的到底是不是你以为的那台机器。 前者告诉你"是谁签的",后者告诉你"你在跟谁说话"。这篇文章把这两步讲透——顺带把 nslookupdig 的分工掰扯清楚,因为它俩看着像,用起来差别不小。文末有复现脚本、排查清单,以及把这套顺序收成的 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,改推 hostdig。但这个决定在 2004 年随 BIND 9.3 发布时被撤销了,此后 nslookup 一直是被完整支持的。所以不必刻意躲着它,尤其在 Windows 上——那儿默认只有 nslookupdig 得自己装。

不过 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

原因是 dignslookup 都是直接对 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_CERTX509_V_ERR_SELF_SIGNED_CERT_IN_CHAINX509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLYX509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATUREX509_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_BUNDLEgit 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,那样至少你知道自己在信任谁。


五、一份可以照着走的清单

遇到证书报错,按这个顺序:

  1. openssl s_client -connect 域名:443 -servername 域名,先看 issuerVerify return code
  2. issuer 是公司名 / 安全设备名 → 代理拆包,跳到第 6 条
  3. issuer 正常但报 21 → 服务器可能少发中间证书,用 -showcerts 数一下链有几张
  4. 报 62(域名不匹配)→ 用 dig +short 对比本地和公共 DNS 的解析结果
  5. 两个 DNS 答案不一致,或 diggetent 不一致 → 查内网覆盖、CNAME 链、/etc/hosts
  6. 拿代理 CA 加 -CAfile 验一次,变成 0 (ok) 就坐实了
  7. 按运行时的方式把 CA 配进去,别用 verify=False 收尾
  8. 嫌手工敲命令?用 lazy-tls-doctor 按同一顺序跑一遍

两个 DNS 工具怎么选:

  • 只想快速确认一个 IP,随手 nslookup 或者 dig +short
  • 要判断"答案是谁给的"、"缓存还剩多久"、"哪一级委派错了" → dig,尤其是 @服务器+trace
  • 写脚本 → 用 dig +short,但别信它的退出码,要看输出内容或 status:
  • 排查"程序和工具说法不一致" → 记住两个工具都跳过 /etc/hosts,用 getent hostsdscacheutil 看程序视角

三个记得住的判断:

证书报错先看 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]

它还会给出可复制的手工命令,方便你自己复现(注意用了 `