四级台阶:L1 受控语言 → L2 结构图 → L3 交互网页 → L4 定制视频。先用五十余句确定性短句建立词汇(签名、证书链、EKU、SAN),再用五张结构图搭起骨架(信任链、证书解剖、单向握手、双向握手、八步校验),然后亲手推一次 mTLS 握手、逐字段拆解一张真实证书、做题自测,最后看一支 2 分 56 秒的解说片,把两张证书的相遇串成一个故事。
CA 证书与 mTLS 做一件事:让两台机器在一条谁都能偷看的线上,互相确认「对面确实持有那把私钥」,然后才开始传数据。顺序是固定的——先有密钥对,再有 CA 把它们和身份绑成证书,再用证书完成握手,最后用证书里的身份去做授权。四级台阶对这个链条逐层下钻。
五十四句确定性短句。每句一概念、一词一义,无形容词、无脑补空间。读完即拥有完整词汇表。
五张确定性结构图:PKI 信任链(可点击)、X.509 证书解剖、单向握手时序、双向握手时序、八步校验流水线。
亲手推一次握手:明文 / 单向 / 双向三种模式逐报文推进;再逐字段拆一张真实证书;十四题自测;末尾附 openssl 签发与排障参考。
2 分 56 秒解说片:七镜头、画面 + 中文配音 + 内嵌字幕。从两台机器第一次见面讲到授权,一条线串完。
证书=公钥+身份+有效期+用途,被 CA 的私钥签了名。
验证=顺着签名一路追到本地预置的根证书。
mTLS=把「客户端验证服务端」这件事,反向再做一遍。
认证只回答「你是谁」;授权才回答「你能干什么」。
最容易死的地方:证书链没发全、SAN 忘了写、到期没人管。
密钥对 → CA 签发证书(公钥+身份+用途,CA 私钥签名)→ 服务端出示叶子+中间证书 → 客户端验签至根 CA → mTLS 时服务端发 CertificateRequest → 客户端出示自己的证书 + CertificateVerify 签名 → 双方验签通过 → 会话密钥加密传输 → 用证书里的 SAN/CN 去做授权
每句话都可以被验证,也都能在 L2 找到对应的图、在 L4 找到对应的命令。读不懂的句子,去 L2 找图;想动手的句子,去 L4 找命令。
先看信任怎么建立(图 1),再看一张证书里到底装了什么(图 2),然后把单向握手逐报文排开(图 3),再把 mTLS 的差异叠上去(图 4),最后把校验规则做成流水线(图 5)。
先切到「双向 mTLS」,从第 0 步一路点到第 11 步,注意第 4 步那条红色报文。再切回「单向 TLS」重播一遍,两条时间线的差别会自己浮出来。然后往下拆证书字段、做题。
提示:切模式会重置到第 0 步;每一步的右侧日志是累积的,能倒回去看前面发生了什么。
双向 mTLS · 握手剧本
11 步推进,重点看第 4 步与第 8–10 步。
等待开始……
· CA 证书的 subject 会出现在它签发证书的 issuer 里,两串必须逐字相等。
· 域名写进 SAN,不写 CN;CN 只留作人看的备注。
· 客户端证书写 clientAuth,服务端证书写 serverAuth;写反了握手一定失败,而且报错往往只说「bad certificate」。
交互玩过之后,这里是能直接粘进终端的东西。命令在 bash 下可直接跑;配置片段只保留了与证书相关的行,其余按你的框架补。
# P-256 比 RSA-2048 短得多,握手更快,现代客户端全都支持 openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out root.key openssl req -x509 -new -key root.key -sha256 -days 3650 \ -subj "/CN=Corp Root CA/O=Corp/C=CN" \ -addext "basicConstraints=critical,CA:TRUE,pathlen:1" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -out root.crt
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out inter.key openssl req -new -key inter.key -subj "/CN=Corp Issuing CA/O=Corp" -out inter.csr printf 'basicConstraints=critical,CA:TRUE,pathlen:0\n' > inter.ext printf 'keyUsage=critical,keyCertSign,cRLSign\n' >> inter.ext openssl x509 -req -in inter.csr -CA root.crt -CAkey root.key -CAcreateserial \ -days 1825 -sha256 -extfile inter.ext -out inter.crt
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out server.key openssl req -new -key server.key -subj "/CN=api.corp.local" -out server.csr printf 'basicConstraints=critical,CA:FALSE\n' > server.ext printf 'keyUsage=critical,digitalSignature\n' >> server.ext printf 'extendedKeyUsage=serverAuth\n' >> server.ext printf 'subjectAltName=DNS:api.corp.local,IP:10.20.30.40\n' >> server.ext openssl x509 -req -in server.csr -CA inter.crt -CAkey inter.key -CAcreateserial \ -days 90 -sha256 -extfile server.ext -out server.crt # 交付给服务进程的是「叶子 + 中间」拼接,不是只有叶子 cat server.crt inter.crt > server-fullchain.crt
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out client.key openssl req -new -key client.key -subj "/CN=svc-order" -out client.csr printf 'basicConstraints=critical,CA:FALSE\n' > client.ext printf 'keyUsage=critical,digitalSignature\n' >> client.ext printf 'extendedKeyUsage=clientAuth\n' >> client.ext printf 'subjectAltName=URI:spiffe://corp.local/ns/prod/sa/order\n' >> client.ext openssl x509 -req -in client.csr -CA inter.crt -CAkey inter.key -CAcreateserial \ -days 7 -sha256 -extfile client.ext -out client.crt
# 只给叶子 + 中间,看能不能追到根 openssl verify -CAfile root.crt -untrusted inter.crt server.crt # server.crt: OK # 带上客户端证书打一次,服务端会要求对端证书 openssl s_client -connect api.corp.local:443 \ -servername api.corp.local \ -cert client.crt -key client.key \ -CAfile root.crt -state # 打印整张证书,检查四个扩展 openssl x509 -in server.crt -noout -text | grep -A1 -E "Subject Alternative|Key Usage|Basic Constraints"
server {
listen 443 ssl;
server_name api.corp.local;
# 必须是「叶子 + 中间」拼接;只配叶子会让部分客户端报链不完整
ssl_certificate /etc/ssl/corp/server-fullchain.crt;
ssl_certificate_key /etc/ssl/corp/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# mTLS 三件套:用哪套 CA 验客户端、验不验、链最深深到几层
ssl_client_certificate /etc/ssl/corp/root.crt;
ssl_verify_client on;
ssl_verify_depth 2;
# 可选:吊销列表(需要定期更新文件并 nginx -s reload)
ssl_crl /etc/ssl/corp/root.crl;
location / {
# 把验证结果和客户端身份透给后端,授权在后端做
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Fingerprint $ssl_client_fingerprint;
proxy_pass http://127.0.0.1:8080;
}
}cert, err := tls.LoadX509KeyPair("server-fullchain.crt", "server.key") if err != nil { log.Fatal(err) } pool := x509.NewCertPool() pem, _ := os.ReadFile("root.crt") pool.AppendCertsFromPEM(pem) srv := &http.Server{ Addr: ":8443", TLSConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, ClientCAs: pool, // 无证书直接拒绝;TLS 1.3 下这就是 mTLS 的开关 ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS13, }, } // 认证过了,还要授权:从 SAN 里取工作负载身份 http.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) { if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { http.Error(w, "no client cert", 401); return } c := r.TLS.PeerCertificates[0] var id string for _, u := range c.URIs { id = u.String() } if !acl.Allowed(id, r.URL.Path) { http.Error(w, "forbidden", 403); return } io.WriteString(w, "ok") }) srv.ListenAndServeTLS("", "")
# downstream:面向客户端(Agent / 上游服务) transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/extensions.transport_sockets.tls.v3.DownstreamTlsContext require_client_certificate: true common_tls_context: tls_certificates: - certificate_chain: { filename: /certs/server-fullchain.crt } private_key: { filename: /certs/server.key } validation_context: trusted_ca: { filename: /certs/root.crt } # 生产建议打开吊销检查;文件不存在时 fail-open 需显式声明 crl: { filename: /certs/root.crl } # 用 SAN 做授权:把 spiffe id 映射成 header 交给上游 # 或者直接上 RBAC: # principal: spiffe://corp.local/ns/prod/sa/order
| 报错原文 | 真实原因 | 处理 |
|---|---|---|
certificate signed by unknown authority | 客户端信任库没有签发链的根;或服务端只发了叶子、没发中间证书 | 导入根证书;服务端改配 fullchain |
unable to get local issuer certificate | openssl 手工验证时没给中间证书 | 加 -untrusted inter.crt |
certificate relies on legacy Common Name field | 证书只有 CN,没有 SAN | 重签,把域名写进 subjectAltName |
certificate has expired or is not yet valid | 真过期,或容器/主机时钟漂移 | 先校时 chronyc tracking,再换证 |
remote error: tls: certificate required | 服务端要求客户端证书,客户端没配 | 客户端加载 cert/key;或确认是否真的需要 mTLS |
remote error: tls: bad certificate | 客户端证书 EKU 不是 clientAuth、链不对、或被吊销 | 查 EKU 与签发 CA,不要只看 CN |
incompatible key usage | 拿 CA 证书或 serverAuth 证书当客户端证书用 | 用正确的叶子证书 |
握手成功但接口 403 | 认证过了,授权没过——证书身份不在 ACL 里 | 不是证书问题,查 SPIFFE/ACL 映射 |
公共 CA(Let's Encrypt / DigiCert …)
用在:面向公网的域名,任意浏览器或未知客户端都要能直接连。
代价:受 CA/B 规则约束,有效期 2026 年起 200 天、2029 年起 47 天;必须做 ACME 自动化。
不适合:内部服务身份——你无法给一万个 Pod 各申请一张公网证书。
私有 CA(自建 openssl / Vault / step-ca)
用在:内网 API、数据库、消息队列、设备接入;客户端是自己能控的。
代价:根证书要分发到每台机器的信任库;吊销、轮换、清单全得自己管。
关键:私有 CA 不受 47 天规则限制,但「有效期长」不等于「可以不轮换」。
服务网格自动签发(Istio / Linkerd / SPIRE)
用在:K8s 里东西向流量,服务数量大、生命周期短。
做法:Sidecar 或 ambient 模式自动签发 24 小时 SVID,自动轮换,身份是 spiffe://…。
代价:引入控制面复杂度;跨集群要规划信任域联邦。
签发时没登记到期日,也没接监控。这张证书只存在于某个 Nginx 目录里。
监控只探了 TCP 端口和 HTTP 200,没探 notAfter。端口一直通,所以一片绿。
服务端日志刷 certificate has expired,客户端刷 x509: certificate has expired。连接不是变慢,是全部失败。
用 openssl s_client -connect host:443 | openssl x509 -noout -dates 确认是服务端证书问题,不是客户端信任库问题。两条命令分清责任方。
不要靠改系统时间绕过。签发新证书、拼 fullchain、reload。若客户端做了证书固定(pinning),新证书指纹会让固定校验失败——这是短有效期时代最容易被忽略的坑。
① 证书清单:域名、签发者、到期日、负责人、部署位置;② 到期告警:T-30 / T-7 / T-1 三档;③ 自动化:ACME 或 Vault 签发 + reload 钩子,人只处理失败。
| 术语 | 一句话 |
|---|---|
| CA | 签发并担保证书的机构或程序。 |
| CSR | 证书签名请求:把公钥和主体信息打包,请 CA 签名。 |
| CN | Common Name,历史字段;域名校验已改看 SAN。 |
| SAN | subjectAltName,装 DNS / IP / URI 身份的地方。 |
| EKU | extendedKeyUsage,决定这张证书能当服务端还是客户端。 |
| CRL / OCSP | 吊销名单 / 在线查询;短有效期正在取代它们。 |
| SPIFFE / SVID | 工作负载身份标准 / 它签出的短期证书,身份写在 URI SAN。 |
| ACME | 自动签发与续期的协议,Let's Encrypt 用的就是它。 |
| 1-RTT | TLS 1.3 一个往返完成握手,比 TLS 1.2 少一个来回。 |
一支片子讲完 CA 证书与 mTLS。七个镜头从「两台机器第一次见面」讲到「认证之后还要授权」,画面里的每一条报文(ClientHello / CertificateRequest / CertificateVerify)、每一个扩展名(SAN / EKU / pathLen)都与 L1–L3 同源。用 Remotion 逐帧渲染——画面即代码:同一时间轴,永远渲染出同一帧,改一个字重渲即可。
规格:1920×1080 · 30fps · 2:56 · H.264 + AAC · 中文配音(zh-CN-Yunxi,36 句逐句合成;每句实测时长都落在自己的字幕窗口内,最紧的一句 4.73s / 5.0s)+ 画面内嵌字幕。底部常驻六阶进度轨(密钥 → 证书 → 信任链 → 握手 → 双向 → 授权),随镜头推进点亮。
镜头 1 · 开场:两张证书碰面
画面:客户端与服务端隔着一条虚线登场,线中间浮出红框「中间人:可读 / 可改 / 可冒充」。
旁白:两台机器,第一次见面。中间隔着一条谁都能看的线。凭什么相信对面?
镜头 2 · 密钥对:谁持有私钥
画面:私钥卡(上锁)与公钥卡(开锁)左右并置,中间双向箭头分别标「用私钥签名」「用公钥验证」,底部一条公式 sign → verify = ✓。
旁白:每台机器都有一对钥匙。私钥不出门,公钥随便给。私钥签的字,只有对应公钥能验过。签名证明的,是私钥在我手里。可这把公钥,到底是谁的?
镜头 3 · 证书:给公钥开一张证明
画面:server.crt 逐行展开——version、issuer、validity、subject、SAN、EKU、keyUsage,最后 signature 一行高亮为绿色。
旁白:证书,就是给公钥开的一张身份证明。里面写着公钥、身份与用途。最后一行,是签发 CA 的签名。改任何一个字节,签名立刻作废。所以证书不能自己证明自己,它靠谁签的。
镜头 4 · 信任链:一路追到根
画面:根 → 中间 → 终端三层竖排,左侧「本地信任库」虚线接入根,右侧一支向上箭头标「验证方向」,收尾弹出红字 unknown authority。
旁白:签发者,就叫 CA。根 CA 自签,靠预装进信任库获得信任。中间 CA 由根签,替它干活。验证时,顺着签名一路往上追。追到信任库里的根,就停。追不到,就断。
镜头 5 · 单向 TLS:只验服务端
画面:两条生命线,ClientHello 到 Finished 六条报文依次划过,末尾提示「客户端身份:未知」。
旁白:握手开始,先谈出一个会话密钥。然后服务端出示证书,证明自己是谁。客户端验链、验期、验用途。八条全过,才开始加密传数据。可服务端,始终不知道客户端是谁。
镜头 6 · mTLS:互相验明正身
画面:同一张时序图,第三条插入红色 CertificateRequest;从第六条起箭头方向反转,客户端出示证书与签名。
旁白:双向认证,只多了一句问话。服务端多发一条请求,要客户端出示证书。客户端回自己的证书,再加一段签名。服务端用证书里的公钥,验这段签名。到这一步,双方都确认对方持有私钥。中间人拿不到私钥,冒充不了任何一方。
镜头 7 · 授权:认证之后
画面:已认证身份 → 策略引擎 → 目标资源,两段箭头分别标「认证 ✓」「授权 ✓」;底部三件长期工程:短有效期、自动轮换、证书清单。
旁白:但认证,不等于授权。证书只说你是谁,不说你能干什么。能不能做,要拿身份去查策略。证书会过期,所以轮换必须自动。信任不是一次握手,是一条持续运转的链。
① 私有 CA 的根证书怎么分发到上万台终端,又能在一台失窃后精准撤销?
② 47 天有效期之下,你手上最不敢自动化的那张证书是哪张?为什么?
③ 服务网格替你轮换证书之后,谁还能说清某一次请求到底是谁发出的?
React 组件写画面 → 逐帧确定性渲染 → 1920×1080 H.264;旁白逐句 TTS(36 句,每句实测时长必须短于自己的字幕窗口,脚本逐句校验)→ 按时间轴 adelay 铺轨、统一响度(loudnorm −16 LUFS)→ 与画面合流。字体本地化落盘(Noto Serif SC / Noto Sans SC 分片 woff2,不联网取字)。时间轴与字幕表分离管理:改一句文案,只改 src/ca/timeline.ts 再重渲,画面、字幕、配音自动对齐。源码在 video/ 目录,出片与配音各一条命令。