mTLS 在 TLS 连接中认证双方。常见证书模式下,客户端验证服务端,服务端也请求并验证客户端证书及私钥持有证明。它把连接关联到经过核验的主体,但不自动决定主体能读取哪个租户、调用哪个 API。
假设订单服务调用支付服务:mTLS 可以确认调用方是受信任的订单工作负载;是否允许它执行扣款、退款,仍由支付服务的授权策略决定。来自同一 CA 的测试服务,也不应因此获得生产支付权限。
| 常见疑问 | 核心区别 |
| 请求客户端证书是否等于必须有证书 | 请求机制与拒绝匿名的策略分开 |
| 必须自己搭建 CA 吗 | 可以用受控托管签发或预注册信任方式 |
| 证书验证通过是否就放行 | 还需身份映射与具体权限判断 |
| 网关启用 mTLS 是否贯穿到后端 | 看实际 TLS 终止点 |
| SPIFFE ID 是否写在 CN | X.509-SVID 使用 URI SAN |
| 轮换证书是否立即替换所有连接身份 | 已有连接与会话恢复有独立状态 |
握手:证书、签名与密钥确认相互补充
在 TLS 1.3 的证书认证完整握手中,服务端发送 CertificateRequest,客户端有合适证书时发送 Certificate、CertificateVerify、Finished。证书表达签发者认可的身份,CertificateVerify 将私钥持有证明绑定本次握手,Finished 确认握手密钥与历史。
客户端找不到合适证书时,返回空 certificate_list,不发送 CertificateVerify;服务端可以依策略继续匿名,或以 certificate_required 中止。要求 mTLS 的入口必须确保匿名路径被拒绝,不能只看有没有发 CertificateRequest。
certificate_authorities 是证书选择提示,省略它不意味着信任任意 CA。TLS 1.3 该扩展出现时列表本身有非空长度要求;扩展缺席与编码一个空列表不是一回事。signature_algorithms、oid_filters 等也各有定义,并不存在通用的“certificate_extensions 中填写 SPIFFE OID”流程。RFC 9846:客户端认证
TLS 1.3 另有协商支持后的 post-handshake authentication;其实现和上层协议支持需要确认,不能假设访问某个 HTTP 路径时都能临时补一次认证。实践中应先明确入口是否整条连接要求客户端身份,再设计路由和授权。
验证通过后,怎样得到可授权的身份
链验证负责确认签发关系、时间和用途等条件;身份映射负责按约定字段得到 principal。服务器身份常用 DNS SAN,客户端可能采用 URI SAN、专门注册的证书映射或其他应用约定。不要随意取第一个 CN 字符串当全系统唯一身份。
以 spiffe://prod.example/ns/orders/sa/api 为例,可以将其作为明确允许的主体,再决定它能否执行 charge。同样的路径出现在 spiffe://staging.example/... 下是不同身份;字符串里含 orders 或以 prod.example 为前缀都不够。
认证和授权的失败应分别记录:未知签发者、证书用途不合适、身份无法映射属于不同问题;已认证主体没有退款权限则是授权拒绝。这样才能解释“TLS 成功但 HTTP 403”,而不是一律重新签证书。
教学策略:默认拒绝,逐个动作授权
以下函数的输入必须由真实验证层产生;它不读取客户端自报的 HTTP 头,也不实现 X.509 或完整 SPIFFE 解析。它只展示认证结果如何参与后续策略。
from dataclasses import dataclass
@dataclass(frozen=True)
class Peer:
authenticated: bool
identity: str | None
RULES = {
'spiffe://prod.example/ns/orders/sa/api': {'charge'},
'spiffe://prod.example/ns/finance/sa/worker': {'refund'},
}
def allow(peer, action, tenant, permitted_tenants):
return (peer.authenticated
and peer.identity in RULES
and action in RULES[peer.identity]
and tenant in permitted_tenants.get(peer.identity, set()))
orders = 'spiffe://prod.example/ns/orders/sa/api'
tenants = {orders: {'tenant-a'}}
assert allow(Peer(True, orders), 'charge', 'tenant-a', tenants)
assert not allow(Peer(True, orders), 'refund', 'tenant-a', tenants)
assert not allow(Peer(True, orders), 'charge', 'tenant-b', tenants)
assert not allow(Peer(False, orders), 'charge', 'tenant-a', tenants)
assert not allow(Peer(True, orders.replace('prod.', 'staging.')), 'charge', 'tenant-a', tenants)
assert not allow(Peer(True, orders + '/extra'), 'charge', 'tenant-a', tenants)
assert not allow(Peer(True, None), 'charge', 'tenant-a', tenants)
print('主体、动作、租户与未认证拒绝分支通过')
工作负载身份与用户身份也应分开。订单服务替用户发起操作时,mTLS 证明的是订单服务;用户、租户和委托范围需要可靠的请求上下文或令牌,并检查服务是否有权代表该用户执行该动作。
信任从哪里来:私有 CA 常见,但并非唯一方式
自建或托管私有 CA 便于控制服务身份命名、签发条件和信任范围。客户端认证信任库应限定用途,不能为图方便直接接受整个公共浏览器根库下的任意合格客户端证书。
mTLS 并不要求组织亲自运维离线根 CA。比如 OAuth 的 mTLS 客户端认证既定义 PKI 方法,也定义基于注册材料的 self_signed_tls_client_auth。后者信任预先绑定的客户端公钥材料,不是“看到任何自签证书都接受”。RFC 8705:客户端认证
签发系统同样需要初始身份依据。新实例不能仅提交“我是 payments”就获得支付身份;它需要由平台证据、工作负载登记及策略建立授权关系。证书签发自动化解决凭据交付,不会凭空解决初始可信身份问题。
SPIFFE:身份规范、凭据与交付接口
| 名称 | 职责 |
| SPIFFE ID | 标识 trust domain 下的工作负载 |
| X.509-SVID | 用 X.509 表达可验证的 SPIFFE 身份 |
| JWT-SVID | 用 JWT 表达另一种 SVID,不等同于 mTLS 证书 |
| Trust bundle | 验证相应域凭据的公开信任材料 |
| Workload API | 向已识别工作负载交付凭据、密钥和信任更新 |
| SPIRE | SPIFFE 的实现之一,负责证明、登记及凭据管理等 |
X.509-SVID 必须恰好有一个 URI SAN 来承载 SPIFFE ID;它可以有其他类型 SAN,但不能用 CN 回退,也不能在多个 URI 中任选一个。叶证书还受 CA 标志、KU 和身份路径等约束;验证 SPIFFE 身份需使用对应 trust domain 的可信材料。X.509-SVID 规范
SVID 不是“证书内容里装着私钥”:证书是公开对象,Workload API 可以把相应私钥作为另一个材料交付。短期轮换是整体设计的一部分,但不能把“默认一小时”写成 SPIFFE 永久统一规定。多个实例也可以共享服务级身份,这并不要求它们共享同一私钥。SPIFFE Concepts
代理边界:证书身份不会自动穿过 TLS 终止
若网关终止 A 的 mTLS,再向 B 建立第二条 mTLS,B 直接认证到的 TLS 对端是网关 G。若 B 需要原调用方 A 的身份,应定义网关到后端的可信传递约定,并限制后端只能从合格入口接受该约定。
以 X-Forwarded-Client-Cert 为例,它是可配置的代理头:入口要清理或覆盖外部伪造值,再根据已验证连接生成内容;后端需验证来源、传递规则和所需字段。把一段 PEM 或 Subject 文本放进头里,不会让 A 参与第二段 TLS 的 CertificateVerify。Envoy:XFCC
如果使用 TLS passthrough,后端直接完成原始握手,但网关通常不能同时读取加密后的 HTTP 路径做常规七层路由。选择终止还是透传,取决于哪一层需要看见身份与业务信息。
后端认证到的是原调用方,还是网关?
在终止模式中,即使原主体可采信,B的直接TLS对端仍是gateway。破坏任一传递条件,就不能继续把orders当作可信原主体。透传模式则由A与B直接握手;改变动作到refund仍会授权拒绝。
正在呈现知识画面
后端认证到的是原调用方,还是网关?两段mTLS建立两份连接身份。可信代理传递可以携带原主体,但不会使原调用方参与后半段握手;授权还需要主体和动作匹配。
{"id":"mtls-proxy-identity","title":"后端认证到的是原调用方,还是网关?","summary":"两段mTLS建立两份连接身份。可信代理传递可以携带原主体,但不会使原调用方参与后半段握手;授权还需要主体和动作匹配。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"terminate\" data-i18n=\"terminate\"></button><button data-mode=\"pass\" data-i18n=\"passthrough\"></button></div><label class=\"check\"><input type=\"checkbox\" id=\"clean\" checked><span data-i18n=\"clean\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"trusted\" checked><span data-i18n=\"trusted\"></span></label><label class=\"form-field\"><span data-i18n=\"action\"></span><select id=\"action\"><option value=\"charge\">charge</option><option value=\"refund\">refund</option></select></label><svg id=\"path\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\" data-i18n-label=\"heading\"></svg><div class=\"metrics\"><div><span data-i18n=\"peer\"></span><strong id=\"peer\"></strong></div><div><span data-i18n=\"principal\"></span><strong id=\"principal\"></strong></div><div><span data-i18n=\"allow\"></span><strong id=\"allow\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n .metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let mode='terminate';function draw(){const pass=mode==='pass',ok=pass||($('#clean').checked&&$('#trusted').checked);graph('path',[{name:'A',x:45,y:55,sub:'orders'},{name:'G',x:180,y:130,sub:'gateway'},{name:'B',x:315,y:55,sub:'payments'}],pass?[[0,2,true]]:[[0,1,true],[1,2,true]]);set('peer',pass?'orders':'gateway');set('principal',ok?'orders':'∅');set('allow',ok&&$('#action').value==='charge'?'ALLOW':'DENY');$('#principal').dataset.valid=String(ok);pressed(mode);}document.querySelectorAll('input,select').forEach(e=>e.onchange=draw);document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});draw();","audio":false,"strings":{"action":"请求动作","allow":"业务授权","clean":"入口覆盖外来身份头","heading":"后端认证到的是原调用方,还是网关?","intro":"切换TLS终止方式与身份传递条件,观察B的直接TLS身份和可采信业务主体。","mode":"连接方式","passthrough":"TLS 透传","peer":"B直接TLS对端","principal":"可采信原主体","reset":"重置","scope":"A=orders,G=gateway,B=payments,证书检查均通过。只允许orders执行charge。头部清理与网关来源检查由开关代表,不实现HTTP代理或真实证书验证。","scopeLabel":"模型范围与约定","terminate":"网关终止 TLS","trusted":"后端验证网关来源及传递约定"}}
Service Mesh:加密覆盖范围与权限策略分开看
Mesh 可以由代理承担证书交付和互相认证,但实际覆盖范围取决于流量是否进入数据平面、工作负载是否纳管及端口策略。Sidecar 只是部署方式之一,不能把所有 mesh 都画成每个 Pod 必有 sidecar。
例如 Istio 中 PeerAuthentication 控制接收侧 mTLS 要求,AuthorizationPolicy 控制访问授权。PERMISSIVE 可以兼容明文与 mTLS,不能用“开启自动 mTLS”推断所有入口已拒绝明文;ambient 等模式还有自己的能力约束。Istio 安全概念、PeerAuthentication
代理间有加密,也不等于应用到本地代理之间必然使用 TLS。评估时应画出真实终止点、进程隔离和可绕过入口,而不是仅凭控制台显示绿色锁判断端到端身份边界。
轮换:旧证书、旧连接和恢复票据是三种状态
假设教学部署的叶证书有效两小时,每小时轮换。10:00 签发的新证书在 11:00 被替换,不意味着 10:30 建立的长连接立刻重新认证。证书到期通常也不会自行在每次应用写入时触发链验证;是否限连接寿命、主动断连或重新授权,需要明确策略。
根或中间 CA 轮换可采用重叠信任:先分发包含新旧锚点的 bundle,确认接收方已加载,再切换新签发链,最后在旧凭据退出且其他相关状态处理完成后移除旧信任。只先切换签发者,会让尚未更新的客户端立即失去互通。
会话恢复可能继承前次认证上下文而不重新发送证书。禁用某个身份时,除了新握手验证,还要考虑现有连接、恢复票据和授权缓存;短有效期只缩短部分暴露窗口。不能因系统使用 mTLS,就把 0-RTT 当作已经完成本次客户端证书认证的普通请求。
与 API Key、访问令牌如何协作
| 机制 | 常见证明对象 | 应单独设计的部分 |
| mTLS | TLS 对端持有绑定身份的私钥 | 终止点、信任库、连接与凭据生命周期 |
| API Key | 请求方持有某个应用凭据 | 传输保护、权限、撤销和泄露处置 |
| Bearer access token | 请求方持有签发的访问令牌 | issuer/audience/期限/权限与重放边界 |
| 证书绑定 access token | 令牌与当前客户端证书关联 | 资源服务器必须检查绑定关系 |
OAuth 证书绑定令牌将证书摘要关联到令牌,资源服务器检查当前 mTLS 证书与绑定是否一致,降低令牌单独泄露后的可用性。它是传输身份与请求授权协作的例子,不能以“用了 JWT 就不需要看 mTLS”替代验证。RFC 8705:绑定访问令牌
不存在脱离连接复用、签名算法、令牌验证缓存和请求规模的固定开销排名。也不能把 API Key 标为天然不兼容零信任:关键是明确身份、最小权限和持续执行策略。
继续查阅