库存服务凭什么相信 orders

累计工程里 loadgen → frontend → orders → inventory 是只读调用;orders 在库存成功后才调用 external-stub。如果另一个服务也能直接请求库存,单看 inventory-v1 返回的报价,分辨不出调用来源。Linkerd 能在两端工作负载都正确入网、策略覆盖入站连接时,把“谁连进来”变成由身份凭据验证的条件;它不能凭 orders 这个 HTTP 字符串证明对端身份。

来源经代理认证后由目标代理执行授权,拒绝在库存应用之前停止

图的左边是出站代理,右边的入站代理验证来源并执行策略。Kubernetes API 和 Linkerd 控制面负责提供身份/目的地/策略信息,不是每个报价请求必经的同步授权网关。以 Linkerd 2.18 文档快照为基线(2026-10-03 核对);这里没有安装该版本,不能把文档中的默认值当作当前环境的配置。

两个代理,不是两个应用里的 TLS 库

Linkerd 数据面使用 Rust 编写的轻量 linkerd2-proxy,并非 Envoy 换一个名称。控制面的 destination 提供目的地及端点变化信息,identity 为合格的工作负载供应短期凭据,policy 将入站服务的授权条件交给代理;代理对入站连接验证并决策。Envoy 则是通用的可扩展代理,有丰富的 listener/filter/xDS 资源模型;第 03 篇的独立 Envoy 模板没有自动变成 Linkerd。这是实现职责差异,不是延迟或内存排名,尤其不能用两份未经功能对齐的请求曲线比较性能。

自动 mTLS 有边界:两端是否注入、协议探测是否适用、控制面是否信任各自身份、工作负载是否绕过代理,都影响加密与身份验证。目标端入站拒绝应检查认证出的服务账户身份与目标端 Server/AuthorizationPolicy 的作用范围;若某条路径从网格外直达未受保护端口,则“已配置策略”不等于“所有路径受保护”。证书失效、新代理无法取得身份以及已有连接仍在使用旧凭据也应分开观察,不能由短暂控制面故障推出所有流量立即失败(参见 29 篇)。

用同一个报价区分允许与拒绝

两次输入只改变经代理认证出的工作负载身份,不靠请求头伪造来源:

1
2
orders 的身份 → GET /quote?sku=paper → 预期 inventory-v1 返回报价
另一账户身份 → GET /quote?sku=paper → 预期目标代理拒绝,应用无该 ID
观察点 允许路径 拒绝路径
身份 orders 的服务账户 不在授权集合内的账户
执行位置 目标代理放行后 inventory 处理 目标代理拦截,inventory 不处理
判据 请求 ID 同时出现在代理和库存 客户端失败且库存无对应 ID

实验在明确的隔离 Kubernetes context 中为 orders、inventory-v1 注入代理,分别固定 namespace、service account 和服务端口。先以源为 orders 的 GET /quote?sku=paper 取得版本与请求 ID;再从另一个独立服务账户发出相同请求,预期被目标代理拒绝,库存应用无对应请求 ID。撤掉策略或停止注入会改变安全前提,不能把“也返回 200”当作 Linkerd 放行。核验证据至少包括部署版本和镜像 digest、Server/AuthorizationPolicy、代理接收的策略状态、两边身份公开元数据、两条客户端原始响应/退出码、目标代理日志与库存处理日志;若拒绝请求在库存日志出现,应判实验失败。

本轮脚本在检查 kubectl 时退出 2,未创建 Kubernetes 资源。因此 mTLS、授权和绕过对照仍为 NOT_RUN;完整配置与复跑命令见下节,静态检查不替代代理实跑。

把服务账户约束写成资源

完整材料放在 examples/service-mesh/electives/E01/。这里故意用两个独立的 curl 客户端工作负载表示 orders 与未知调用方,库存使用 Python 标准库 HTTPServer。它们都是合成只读服务,不调用支付或外部系统,也不复用累计工程中的订单编排。应用对所有 GET 路径都返回 200,因而后续 403 可以定位到代理层。

workloads.yaml 创建 sm-e01 命名空间、两个 ServiceAccount、三个 Deployment、库存 Service 和内嵌 Python 的 ConfigMap。命名空间的 linkerd.io/inject: enabled 让注入器为新 Pod 加代理;已有 Pod 不会因为随后添加注解而自动重建。库存端口在 Pod 中命名为 http,Service 把 8080 映射到该端口。

身份和端口的连接由以下完整策略建立,内容对应仓库的 policy.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: policy.linkerd.io/v1beta1
kind: Server
metadata:
name: inventory
namespace: sm-e01
spec:
podSelector:
matchLabels: {app: inventory}
port: http
proxyProtocol: HTTP/1
---
apiVersion: policy.linkerd.io/v1alpha1
kind: MeshTLSAuthentication
metadata:
name: orders
namespace: sm-e01
spec:
identityRefs:
- kind: ServiceAccount
name: orders
---
apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
name: orders-to-inventory
namespace: sm-e01
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: inventory
requiredAuthenticationRefs:
- group: policy.linkerd.io
kind: MeshTLSAuthentication
name: orders

2.18 授权资源文档规定,Server 选择同命名空间的 Pod 及声明过的端口;匹配它的流量默认需要授权。这里使用身份引用,避免手工拼接信任域字符串。orders 不是客户端随意填写的 HTTP 请求头,而是经过 mesh TLS 验证的服务账户身份。授权范围是整个库存 HTTP 端口,本例不声称实现逐路径授权;需要逐路径限制时才引入 HTTPRoute 并把 AuthorizationPolicy 指向它。

[PATTERN] 将授权条件拆成“受保护对象、可接受身份、二者关系”,能分别检查 selector、证书身份与授权引用。一个 YAML 被 API 接收,仅证明声明合法,不证明流量经过这个对象。

执行允许、拒绝和恢复

先在独立测试集群安装与 2.18 API 对应的 Linkerd 发行包及 CLI,按发行包的 Kubernetes 支持范围核对集群。商业稳定发行与 edge 包的版本命名不相同,不能把文档版本直接当作一个可下载的二进制标签。本例不自动修改已有集群的控制面。

1
2
linkerd --context "$LAB_CONTEXT" check
LAB_CONTEXT=kind-linkerd bash examples/service-mesh/scripts/run-E01.sh

脚本要求显式 context,先创建工作负载和策略,再等待三个 Deployment。它把集群版本、Pod 的实际 imageID、资源 YAML 和双方日志写入输出目录;镜像标签固定为 Python 3.13.2 Alpine、curl 8.12.1,实际 digest 仍需看该次 pods.json,标签不等于不可变镜像摘要。

允许和拒绝的核心调用如下,ID 必须按每轮实验更换:

1
2
3
kubectl --context "$LAB_CONTEXT" -n sm-e01 exec deploy/orders -c client -- curl -sv --max-time 5 -H 'X-Request-ID: e01-allow' 'http://inventory:8080/quote?sku=paper'
kubectl --context "$LAB_CONTEXT" -n sm-e01 exec deploy/unknown -c client -- curl -sv --max-time 5 -H 'X-Request-ID: e01-deny' 'http://inventory:8080/quote?sku=paper'
kubectl --context "$LAB_CONTEXT" -n sm-e01 logs deploy/inventory -c inventory

脚本要求 orders 返回 200、unknown 返回 403,并且库存日志只出现允许 ID。DNS 失败、连接超时或两边均失败都不能通过。运行时还应执行 linkerd --context "$LAB_CONTEXT" -n sm-e01 check --proxy,确认两端代理有可用身份;这与 HTTP 判据共同限定 mTLS 路径,不能仅凭一个 403 推断证书验证过程。

故障注入可以删除唯一的 AuthorizationPolicy,保留 Server。此时 orders 也应被拒绝,因为目标端口仍被 Server 选中。恢复时重新 apply 策略,并等待新的请求恢复 200:

1
2
kubectl --context "$LAB_CONTEXT" -n sm-e01 delete authorizationpolicy orders-to-inventory
kubectl --context "$LAB_CONTEXT" apply -f examples/service-mesh/electives/E01/policy.yaml

这两个命令之间应重放允许请求并保存失败,恢复后再重放一次保存成功。不要删除 Server 来模拟同一故障:删除它会重新落入默认入站策略,可能放宽访问。实验结束只清理专用命名空间:kubectl --context "$LAB_CONTEXT" delete namespace sm-e01。

常见失败的定位顺序

如果所有来源都能访问,先检查 inventory 的容器列表有无 linkerd-proxy,再检查 Server selector 和端口名;没有命中的策略谈不上执行授权。若所有来源都被拒绝,检查 orders Pod 的 serviceAccountName、代理身份状态以及 MeshTLSAuthentication 所在命名空间。若允许请求成功但库存日志缺失,先检查请求是否命中另一个副本,收集该 Deployment 全部 Pod 日志,不能据单个 Pod 的空日志宣布安全。

[PATTERN] 授权失败必须和连接失败分开。使用一个确定允许的对照请求,能排除后端停机、DNS 故障等共同原因;再用被拒 ID 未进入应用日志证明拒绝发生在业务处理之前。

模式 使用位置 误用后果
对象、身份、关系分离 Server、MeshTLSAuthentication、AuthorizationPolicy 标签命中被误当成身份可信
正负请求共享业务输入 只改变服务账户 无法区分授权与应用逻辑
保留保护对象再撤授权 故障和恢复实验 删除 Server 导致保护边界一并消失

练习与参考解答

  1. 画出来源不是 orders 时,请求最远能到哪里。参考解答:若两端入网且入站策略精确匹配目标端口,来源代理发起连接,目标代理验证身份并拒绝;库存 handler 不执行,external-stub 更不应被库存调用。以入站代理日志与库存请求 ID 验证,而非仅看客户端错误码。
  2. 把来源服务账户换成同名但不同 namespace 的账户,还应允许吗?参考解答:不应仅按账户短名判断;检查授权身份是否包含 namespace/信任边界,实际证书身份、策略 selector 与认证结果必须一致。若放行,审查是否匹配了更宽的授权或流量绕过目标代理。

资料与系列导航

Linkerd 2.18 架构、自动 mTLS、Server policy;源码入口 linkerd2-proxy 固定查询提交(不是 2.18 发布标签,不作该版本的内部行为断言)。核验边界见 writing-plans/service-mesh/research/E01.md。

系列导航:19 工作负载身份 → E01 Linkerd(本文) → E02 Cilium。