笔记
0工具
0此页用于记录用户反馈问题后的每一次改进
关于
“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
不想公开、只想临时给人看:点“分享”生成一条带密码和有效期的链接,到期自动失效,你也能随时撤销。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
趋势
如何在 Kubernetes 中使用 SPIFFE、SPIRE 和 Cilium 实现零信任工作负载身份
你的网络策略说:“允许来自 10.0.1.45 的流量”。昨天,10.0.1.45 是你的支付服务。今天,经过一次滚动部署后,它变成了你的日志代理。你的支付服务现在在 10.0.1.89。Kubernetes 已经更新了所有 endpoints 和服务记录——但你的网络策略对此一无所知。它还在默默地放行流量,基于一个不再属于你原本想信任的工作负载的 IP 地址。
这就是工作负载身份问题。IP 地址不是身份,它们只是位置。而在 Kubernetes 集群中,位置一直在变。基于 IP 地址构建安全策略意味着,每次 Pod 被调度、重新调度或扩缩容时,你的安全姿态都在无声地退化。
解决方案是密码学工作负载身份:每个工作负载都获得一个由证书支持的身份,证明“我是谁”,而不是“我在哪”。服务在交换任何数据之前,先使用这些证书相互认证。如果证书不匹配,连接就会被拒绝,不管流量来自哪个 IP 地址。
这就是 SPIFFE 和 SPIRE 提供的功能。而 Cilium 通过 eBPF 实现了这一点,无需在每个 Pod 中注入 sidecar。
在这篇文章里,你会理解 SPIFFE 身份模型的工作原理,部署 SPIRE 来为工作负载签发密码学身份,并使用 Cilium 内置的 SPIRE 集成来实现服务间的双向 TLS(mTLS),而且完全不需要改动你的应用代码。
前置条件
- 熟悉 Kubernetes RBAC 和 Pod 安全——这本手册涵盖了基础
- 熟悉 TLS 证书和 Kubernetes Secrets——这本手册涵盖了 cert-manager 和证书概念
- 已安装 Helm 3 和 Cilium CLI
- 一个 kind 集群——你会在本文中创建一个全新的集群,并用 Cilium 作为 CNI
- 耐心:这是本系列文章中我做过的最复杂的演示。SPIRE 的组件比之前所有内容都多。
所有演示文件都在配套的 GitHub 仓库中。
目录
- 工作负载身份问题
- SPIFFE 的工作原理
- SPIFFE ID 和信任域
- SVID:密码学身份文档
- 信任包
- SPIRE 的工作原理
- SPIRE Server 和 SPIRE Agent
- 节点证明
- 工作负载证明
- SVID 签发与轮换
- Cilium 如何用 SPIFFE 实现双向 TLS
- 演示 1:安装带有 SPIRE 集成的 Cilium
- 步骤 1:安装 Cilium CLI
- 步骤 2:创建一个没有默认 CNI 的 kind 集群
- 步骤 3:安装启用了 SPIRE 的 Cilium
- 步骤 4:验证安装
- 演示 2:用 CiliumNetworkPolicy 强制双向 TLS
- 步骤 1:部署客户端和服务器
- 步骤 2:确认没有认证时流量正常
- 步骤 3:应用要求双向认证的 CiliumNetworkPolicy
- 步骤 4:验证经过认证的流量仍然正常
- 步骤 5:(可选)用 Hubble 观察认证过程
- 步骤 6:验证没有匹配标签的 Pod 被拦截
- 步骤 7:检查 SPIRE 中的工作负载条目
- 结论
- 清理(kind 集群)
工作负载身份问题
开头那个场景不是理论上的。在 Kubernetes 里,Pod 是短暂的。调度器可以把 Pod 放在任意节点上,而 Pod 的 IP 地址是在调度时从节点的 IP 池中分配的。当 Pod 因滚动部署、节点腾空或自动扩缩容事件被删除并重建时,它会获得一个新的 IP 地址。
如果你写了一个 NetworkPolicy,说“允许来自这个 IP 的流量”,那么这个策略现在要么指向了空,要么更糟——指向了一个不同的工作负载。
Kubernetes Service 名称在东西向流量上确实有帮助——无论后端是哪些 Pod,Service 名称都能一致地解析。但基于 Service 名称的 NetworkPolicy 本质上还是标签选择器匹配,而不是密码学断言。任何能伪造正确标签的 Pod 都可以绕过它。
你真正想要的是:在服务 A 向服务 B 发送请求之前,服务 B 必须用密码学方式证明自己的身份。如果服务 B 无法证明它是它所声称的那个服务,服务 A 就拒绝连接。这就是双向 TLS(mTLS)。而关键问题是:这些身份从哪里来?
SPIFFE 回答了这个问题。
SPIFFE 的工作原理
SPIFFE——Secure Production Identity Framework for Everyone——是一个 CNCF 标准,定义了一套工作负载身份模型。它本身不实现任何东西。它规定了身份的格式、请求身份的 API,以及使身份在服务、集群和云之间可验证的信任模型。
SPIRE 是该规范的参考实现。
SPIFFE ID 和信任域
SPIFFE 身份是一个 URI,格式如下:
spiffe://<trust-domain>/<workload-path>
信任域是一个字符串,标识管理边界——通常是你的组织、集群或环境。同一个信任域内的所有实体可以互相验证身份。来自不同信任域的身份需要显式的联邦配置。
一些具体的例子:
spiffe://payments.corp/ns/production/sa/checkout
spiffe://analytics.corp/ns/data/sa/pipeline-worker
spiffe://cluster.local/ns/monitoring/sa/prometheus
信任域后面的路径是任意的——由你的 SPIRE 配置定义,通常编码了 Kubernetes 命名空间和服务账户。
SVID:密码学身份文档
SVID——SPIFFE Verifiable Identity Document——是将 SPIFFE 身份具体化为服务可以实际使用的东西。有两种 SVID 格式:
- X.509 SVID:一个标准的 TLS 证书,其中 SPIFFE ID 嵌入在 Subject Alternative Name (SAN) URI 字段中。因为是标准 X.509 证书,任何 TLS 库都可以直接使用。工作负载在 TLS 握手时出示此证书,对端验证证书是由受信任的 SPIRE Server 签名的。这是用于长连接(如 gRPC 流)的格式。
- JWT SVID:一个签名的 JSON Web Token,其中 SPIFFE ID 作为声明。适合基于请求的 HTTP 认证——放在 Authorization 头中传递,接收服务验证签名。JWT SVID 的寿命比 X.509 SVID 短,并且限定在特定受众,以防止令牌跨服务重用。
对于 Cilium 的互相认证,使用 X.509 SVID。本文其余部分聚焦于 X.509。
信任包
为了让服务 A 验证服务 B 的证书,服务 A 需要知道哪个证书颁发机构(CA)签署了它。在 SPIFFE 中,这被称为信任包——一个信任域内受信任的 CA 证书集合。
SPIRE 通过 Workload API 提供信任包。当工作负载请求其身份时,它也会收到当前的信任包。当 SPIRE Server 轮换其 CA 时,它会将新的信任包分发给所有 Agent,再由 Agent 推送给所有工作负载。你的应用程序永远不需要手动管理信任包。
SPIRE 的工作原理
SPIRE 是签发 SVID 并管理身份生命周期的引擎。理解它的架构是理解 Cilium 集成的关键。
SPIRE Server 和 SPIRE Agent
SPIRE 有两个主要组件:
- SPIRE Server:中央 CA。它维护一个工作负载条目注册表(描述应该向哪些工作负载签发哪些 SPIFFE ID)。它代表工作负载向 Agent 签发 SVID,是整个信任域的信任根。
- SPIRE Agent:作为 DaemonSet 在每个节点上运行。它有两个任务:
- 向 SPIRE Server 证明它运行在一个合法的节点上——这叫节点证明。
- 在节点上通过 Unix 套接字暴露 SPIFFE Workload API,工作负载通过它请求自己的 SVID。
Agent 会在本地缓存 SVID,这样即使暂时与 SPIRE Server 断开连接,也不会立即破坏工作负载身份。
这种设计——中央 Server,每个节点上的 Agent——是有意为之的。工作负载从不直接联系 SPIRE Server,而是通过本地的 SPIRE Agent 获取身份。这减少了 SPIRE Server 的负载,同时提供了更好的容错性和可扩展性。
节点证明
节点证明是 SPIRE Agent 向 SPIRE Server 证明自己运行在合法节点上的过程。具体机制取决于平台:
- 在 AWS 上,Agent 可以展示其 EC2 实例的元数据
- 在 GCP 上,Agent 可以展示其 GCE 实例的元数据
- 在 Kubernetes 上,Agent 使用 Kubernetes 的节点身份和令牌
一旦 Agent 被证明,它就能代表该节点上的工作负载请求 SVID。
工作负载证明
工作负载证明是 SPIRE Agent 识别哪个进程在请求身份的过程。在 Kubernetes 中,这通常基于 Pod 的标签、服务账户、命名空间等元数据。
当 Pod 中的工作负载通过 Unix 套接字向 SPIRE Agent 请求 SVID 时,Agent 会检查调用者的凭证——例如,它可以通过 cgroups 或 Linux 安全模块来确定调用进程属于哪个 Pod——然后根据 SPIRE Server 上配置的工作负载条目来决定是否签发身份。
SVID 签发与轮换
当工作负载第一次请求 SVID 时,SPIRE Agent 向 SPIRE Server 发起 CSR(证书签名请求)。Server 验证请求是来自一个被授权的工作负载,然后签发证书。
SPIRE 会自动轮换 SVID。默认情况下,X.509 SVID 的有效期是 1 小时,Agent 会在过期前自动请求新的证书。这意味着即使私钥泄露,影响范围也限制在很短的时间内。
Cilium 如何用 SPIFFE 实现双向 TLS
Cilium 是一个基于 eBPF 的网络、安全和可观测性项目。它原生支持 SPIFFE/SPIRE,无需 sidecar 即可实现工作负载间的双向 TLS。
Cilium 的 SPIRE 集成工作原理如下:
- 身份获取:Cilium 在每个节点上运行一个 SPIRE Agent(作为 Cilium Agent 的一部分或独立运行)。当 Pod 启动时,Cilium 会代表该 Pod 向 SPIRE Server 请求一个 SVID。
- 身份映射:Cilium 将 SPIFFE ID 映射到其内部的安全身份(Security Identity),这是一个整数 ID,用于 eBPF 策略决策。
- 策略执行:当你创建一个 CiliumNetworkPolicy 并指定
requireAuthentication时,Cilium 会在 eBPF 层面强制要求连接必须经过 TLS 握手并验证对端的 SPIFFE ID。 - 透明代理:Cilium 使用 eBPF 在数据路径上透明地拦截流量,执行 TLS 握手,而不需要修改或重新配置应用程序。应用代码完全不知道 mTLS 的存在。
这意味着你可以为现有工作负载启用 mTLS,而无需更改一行代码或重新部署。
演示 1:安装带有 SPIRE 集成的 Cilium
让我们动起手来。先创建一个 kind 集群并安装 Cilium,启用 SPIRE 集成。
步骤 1:安装 Cilium CLI
如果你还没有安装 Cilium CLI,先从 GitHub 发布页面下载:
# Linux 或 macOS
curl -L --remote-name-all https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-amd64.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gz{,.sha256sum}
或者使用 Homebrew:
brew install cilium-cli
验证安装:
cilium version
你应该看到类似这样的输出:
cilium-cli: v0.16.6 compiled with go1.22.2 on linux/amd64
cilium image (default): v1.16.1
cilium image (stable): v1.16.1
步骤 2:创建一个没有默认 CNI 的 kind 集群
创建一个 kind 配置文件,禁用默认的 CNI,这样 Cilium 可以接管网络:
# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
disableDefaultCNI: true
nodes:
- role: control-plane
- role: worker
创建集群:
kind create cluster --config kind-config.yaml
等待节点就绪:
kubectl get nodes
你应该看到两个节点(一个 control-plane,一个 worker)都处于 Ready 状态,但 Pod 网络还没有就绪(CoreDNS 会处于 Pending 状态)。
步骤 3:安装启用了 SPIRE 的 Cilium
现在安装 Cilium,启用 SPIRE 集成:
cilium install \
--set=spire.enabled=true \
--set=spire.install.enabled=true \
--set=spire.install.server.serviceType=ClusterIP
这个命令会:
- 安装 Cilium 作为 CNI
- 安装 SPIRE Server 和 SPIRE Agent(作为 Cilium 的一部分)
- 配置 SPIRE 与 Cilium 的集成
等待安装完成:
cilium status --wait
你应该看到所有组件都处于健康状态,包括 spire-server 和 spire-agent。
步骤 4:验证安装
检查 SPIRE 相关的 Pod 是否正常运行:
kubectl get pods -n cilium-spire
你应该看到类似这样的输出:
NAME READY STATUS RESTARTS AGE
spire-server-0 1/1 Running 0 2m
spire-agent-6j4k8 1/1 Running 0 2m
spire-agent-7x9qz 1/1 Running 0 2m
验证 Cilium 的 SPIRE 集成:
kubectl exec -n cilium-spire spire-server-0 -- /opt/spire/bin/spire-server entry show
目前应该没有工作负载条目,因为还没有部署任何工作负载。
演示 2:用 CiliumNetworkPolicy 强制双向 TLS
现在让我们部署一个客户端和服务器,然后应用一个要求双向认证的网络策略。
步骤 1:部署客户端和服务器
创建一个命名空间:
kubectl create ns demo
创建服务器部署和 Service。这是一个简单的 HTTP 服务器,监听 8080 端口:
# server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-server
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: http-server
template:
metadata:
labels:
app: http-server
spec:
containers:
- name: http-server
image: hashicorp/http-echo:latest
args:
- "-text=Hello from the server!"
- "-listen=:8080"
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: http-server
namespace: demo
spec:
selector:
app: http-server
ports:
- port: 8080
targetPort: 8080
创建客户端部署。这是一个简单的 curl Pod,用来测试连接:
# client.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-client
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: http-client
template:
metadata:
labels:
app: http-client
spec:
containers:
- name: http-client
image: curlimages/curl:latest
command: ["/bin/sh", "-c", "sleep infinity"]
应用这两个部署:
kubectl apply -f server.yaml -f client.yaml
等待 Pod 就绪:
kubectl wait --for=condition=ready pod -l app=http-server -n demo
kubectl wait --for=condition=ready pod -l app=http-client -n demo
步骤 2:确认没有认证时流量正常
先测试从客户端到服务器的 HTTP 请求,确认网络是通的:
kubectl exec -n demo deploy/http-client -- curl -s http://http-server.demo.svc.cluster.local:8080
你应该看到:
Hello from the server!
流量正常。目前没有认证要求。
步骤 3:应用要求双向认证的 CiliumNetworkPolicy
现在创建 CiliumNetworkPolicy,要求客户端和服务器之间必须进行互相认证:
# mtls-policy.yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "require-mtls"
namespace: demo
spec:
endpointSelector:
matchLabels:
app: http-server
ingress:
- fromEndpoints:
- matchLabels:
app: http-client
authentication:
mode: required
这个策略的意思是:
- 目标(endpointSelector):选择
app=http-server的 Pod - 入站规则:仅允许来自
app=http-client的 Pod 的流量 - 认证要求:必须进行互相认证(
authentication.mode: required)
注意:authentication.mode 可以设置为 required 或 disabled。required 意味着 Cilium 会强制进行 TLS 握手,并验证对端的 SPIFFE ID。
应用策略:
kubectl apply -f mtls-policy.yaml
步骤 4:验证经过认证的流量仍然正常
现在再次测试从客户端到服务器的连接:
kubectl exec -n demo deploy/http-client -- curl -s http://http-server.demo.svc.cluster.local:8080
你应该仍然看到:
Hello from the server!
流量仍然正常,但现在它在幕后经过了 mTLS 认证。注意:应用程序代码没有改变,客户端也没有显式配置证书。Cilium 在 eBPF 层面透明地处理了 TLS 握手。
步骤 5:(可选)用 Hubble 观察认证过程
如果你安装了 Hubble,可以用它来观察认证过程。先启用 Hubble:
cilium hubble enable
然后启动 Hubble UI 或使用 CLI:
# 安装 Hubble CLI
# 然后观察流量
hubble observe --from-pod demo/http-client --to-pod demo/http-server
你应该看到类似这样的输出:
Sep 10 12:00:00.000 demo/http-client:8080 -> demo/http-server:8080 to-endpoint FORWARDED (TCP)
注意流量被标记为 FORWARDED,因为认证成功。
步骤 6:验证没有匹配标签的 Pod 被阻止
现在创建一个没有正确标签的 Pod,尝试访问服务器:
# malicious-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: malicious-pod
namespace: demo
labels:
app: malicious
spec:
containers:
- name: curl
image: curlimages/curl:latest
command: ["/bin/sh", "-c", "sleep infinity"]
创建这个 Pod:
kubectl apply -f malicious-pod.yaml
kubectl wait --for=condition=ready pod malicious-pod -n demo
尝试从恶意 Pod 访问服务器:
kubectl exec -n demo malicious-pod -- curl -s http://http-server.demo.svc.cluster.local:8080
这次请求应该被阻塞。你会看到类似这样的错误(或者连接超时):
curl: (28) Failed to connect to http-server.demo.svc.cluster.local port 8080 after 1000 ms: Connection timed out
这说明 CiliumNetworkPolicy 正确地阻止了未经认证的流量。恶意 Pod 没有正确的标签,因此无法通过策略检查,更无法完成 mTLS 握手。
步骤 7:检查 SPIRE 中的工作负载条目
最后,检查 SPIRE Server 中的工作负载条目,看看 Cilium 为你注册了哪些身份:
kubectl exec -n cilium-spire spire-server-0 -- /opt/spire/bin/spire-server entry show
你应该看到类似这样的输出:
Entry ID : 12345678-1234-1234-1234-123456789abc
SPIFFE ID : spiffe://cluster.local/ns/demo/sa/default
Parent ID : spiffe://cluster.local/ns/cilium-spire/sa/spire-agent
TTL : 3600
Selector : k8s:pod-label:app:http-server
Selector : k8s:ns:demo
注意:
- SPIFFE ID 是
spiffe://cluster.local/ns/demo/sa/default——这表示命名空间demo中的默认服务账户 - TTL 是 3600 秒(1 小时),之后 SVID 会自动轮换
- Selector 指定了哪些 Pod 会获得这个身份——这里是标签
app=http-server且在命名空间demo中的 Pod
Cilium 会自动为每个工作负载创建这样的条目。你不需要手动注册。
结论
我们在这篇文章中做了很多事:
- 理解了为什么 IP 地址不能作为身份——Pod 的 IP 是动态的,基于 IP 的安全策略会随着 Pod 调度和重新调度而失效
- 学习了 SPIFFE 身份模型——SPIFFE ID 是 URI 格式的身份标识,SVID 是具体的证书或令牌,信任包是 CA 证书集合
- 了解了 SPIRE 的架构——SPIRE Server 是中央 CA,SPIRE Agent 在每个节点上运行,工作负载通过本地 Agent 获取身份
- 看到了 Cilium 如何通过 eBPF 透明地实现 mTLS——无需 sidecar,无需修改应用代码
- 通过两个演示,实际部署了 Cilium + SPIRE,并验证了 mTLS 策略的效果
关键收获是:零信任不是关于信任网络位置,而是关于验证身份。SPIFFE 和 SPIRE 提供了标准化的身份框架,Cilium 将其与高性能的 eBPF 数据路径结合起来,让你可以在不牺牲性能或增加复杂性的情况下,为 Kubernetes 工作负载启用 mTLS。
清理
如果你使用的是 kind 集群,删除集群很简单:
kind delete cluster
这会删除整个集群,包括所有 Pod、Service 和存储。
如果你想保留集群但删除演示资源:
kubectl delete ns demo
kubectl delete -n cilium-spire $(kubectl get entries -n cilium-spire -o name)
但最简单的还是直接删掉整个 kind 集群。