你的网络策略规定:允许来自10.0.1.45的流量通过。

昨天,10.0.1.45是你的支付服务所在的地址;而今天,在进行了滚动部署之后,它变成了你的日志采集代理。现在的支付服务地址是10.0.1.89

Kubernetes已经更新了所有的端点和服务记录——但你的网络策略并不知道这一变化。它仍然基于那个不再属于你期望信任的工作负载的IP地址,允许流量通过。

这就是“工作负载身份识别问题”所在。IP地址代表的是位置,而不是身份;而在Kubernetes集群中,这些位置是会不断变化的。如果基于IP地址来制定安全策略,那么每当有一个Pod被调度、重新调度或进行扩展时,你的安全措施就会无形中变得脆弱起来。

解决这个问题的方法是使用加密技术来确定工作负载的身份:每个工作负载都会获得一份由证书支持的身份证明,这份证明能够说明它是谁,而不是它位于哪里。服务之间在交换数据之前,会先使用这些证书进行相互认证;如果证书不匹配,无论数据来自哪个IP地址,连接都会被拒绝。

SPIFFE和SPIRE正是为了解决这个问题而设计的。而Cilium则利用eBPF技术实现了这一功能,而且无需在每个Pod中添加额外的组件。

通过这篇文章,你将了解SPIFFE的身份识别模型是如何工作的,如何部署SPIRE来为工作负载生成加密身份证明,以及如何使用Cilium内置的SPIRE集成功能,在不修改应用程序代码的情况下,实现服务之间的相互TLS认证。

先决条件

  • 需要熟悉Kubernetes的RBAC机制和Pod安全配置——这份手册介绍了相关基础知识

  • 需要了解TLS证书以及Kubernetes的Secrets功能——这份手册讲解了cert-manager和证书的相关概念

  • 需要安装Helm 3和Cilium CLI工具

  • 需要使用kind类型的Kubernetes集群——在这篇文章中,你会使用Cilium作为CNI来创建一个新的kind集群

  • 需要有足够的耐心:这是我在这一系列文章中介绍过的最复杂的演示案例。SPIRE涉及的技术组件比之前讨论的任何方案都要多。

所有的演示文件都保存在这个GitHub仓库中

目录

工作负载身份识别问题

这种场景并非理论上的假设。在Kubernetes中,Pod是临时存在的对象。调度器可以将一个Pod放置在任何节点上,而Pod的IP地址会在调度时从该节点的IP地址池中分配而来。

当一个Pod通过滚动部署、节点资源回收或自动扩展机制被删除并重新创建时,它会获得一个新的IP地址。如果你编写了这样一条NetworkPolicy规则:“允许来自这个IP地址的流量”,那么这条规则现在就会指向无效的目标,甚至可能会指向另一个工作负载。

在这种情况下,Kubernetes的服务名称能够帮助处理东西向流量——无论支持该服务的Pod是什么,服务名称都能被一致地解析出来。但是,基于服务名称制定的NetworkPolicy仍然只是一种标签匹配机制,并不具备加密验证的功能。任何能够伪造正确标签的Pod都可以绕过这种安全机制。

你真正需要的是这样的解决方案:在服务A向服务B发送请求之前,服务B必须通过加密方式证明自己的身份;如果服务B无法证明自己就是它所声称的那个实体,服务A就应该拒绝建立连接。这就是所谓的“相互TLS认证”机制,而关键的问题在于:这些身份验证信息是从哪里获得的呢?

SPIFFE为这个问题提供了答案。

SPIFFE的工作原理

SPIFFE,即“适用于所有人的安全生产身份识别框架”,是一项由CNCF制定的标准。它定义了一种工作负载身份识别的模型,但本身并不实现任何具体的功能。它仅规定了身份信息的格式、用于请求这些信息的API接口,以及使这些身份信息能够在不同的服务、集群和云环境中被验证的信任机制。SPIRE则是这一标准的参考实现方案。

SPIFFE身份标识与信任域

一个SPIFFE身份标识是一个具有特定格式的URI:

spiffe:///

“信任域”是一个用于标识行政边界的信息,通常指的是你的组织、集群或所处的环境。同一信任域内的所有实体都可以互相验证彼此的身份;而来自不同信任域的实体则需要通过额外的配置才能实现身份验证。

下面是一些具体的例子:

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可验证身份文档”,是一种将SPIFFE身份标识转化为服务可以直接使用的形式的方式。

目前存在两种SVID格式。

X.509 SVID是一种标准的TLS证书,其中SPIFFE身份标识被嵌入到“Subject Alternative Name”字段中。由于它属于标准的X.509证书,因此任何TLS库都可以直接使用它而无需进行任何修改。

在工作负载进行TLS握手时,会出示这份证书,对方会验证该证书确实是由可信的SPIRE服务器签署的。这种格式适用于gRPC流等长期连接的场景。

JWT SVID是一种经过签名的JSON Web Token,其中包含了SPIFFE ID这一身份信息。它非常适合通过HTTP进行基于请求的身份验证——只需将其放在Authorization头部中,接收方服务就会验证其签名是否有效。

与X.509 SVID相比,JWT SVID的有效期较短,并且其使用范围也是有限的,这样就可以防止不同服务之间重复使用同一令牌。

对于Cilium的相互认证机制来说,使用的是X.509 SVID。本文的其余部分将重点介绍X.509证书的相关内容。

信任包

如果服务A需要验证服务B的证书,那么服务A就必须知道这份证书是由哪个证书颁发机构签署的。在SPIFFE体系中,这个被称为“信任包”——也就是在某个信任域内被认可的所有证书颁发机构的证书集合。

SPIRE通过Workload API提供了这种信任包。当一个工作负载请求获取自己的身份信息时,它也会同时收到当前的信任包。一旦SPIRE服务器更新了其证书颁发机构列表,就会将新的信任包分发给所有代理节点,而这些代理节点又会将其推送给所有的工作负载。因此,你的应用程序完全不需要手动管理这些信任包。

SPIRE的工作原理

SPIRE是负责生成SVID并管理身份生命周期的组件。理解它的架构结构,才能真正理解Cilium如何与它进行集成。

SPIRE服务器与SPIRE代理节点

SPIRE主要由两个部分组成。SPIRE服务器是整个系统的核心证书颁发机构。它负责维护一份工作负载记录清单,确定哪些SPIFFE ID应该发放给哪些工作负载,并代表这些工作负载生成SVID,因此它是整个信任域的权威认证源。

SPIRE代理节点则以DaemonSet的形式运行在每个节点上。它的职责主要有两项:首先,它需要向SPIRE服务器证明自己确实运行在合法的节点上;其次,它在本地提供SPIFFE Workload API接口,供工作负载通过这些接口请求获取SVID。

代理节点会将SVID缓存在本地上,这样即使与SPIRE服务器的连接暂时中断,工作负载的身份认证过程也不会受到影响。

这种“中央服务器+分布式代理节点”的架构设计是经过深思熟虑的。工作负载从来不会直接与SPIRE服务器进行通信,它们只与自己所在节点上的代理节点交互。代理节点负责处理所有的身份验证请求,这样一来,即使某个节点遭到攻击,其影响范围也会被限制在较小的范围内。

节点认证

当一个SPIRE代理节点在新节点上启动时,它首先需要向SPIRE服务器证明自己的身份,才能开始为其他工作负载提供身份验证服务。这个过程就是节点认证。

在Kubernetes环境中,SPIRE使用PSAT(即专为SPIRE服务器设计的Service Account Tokens)来进行节点认证。代理节点会提供一个专门为SPIRE服务器准备的Kubernetes服务账户令牌,SPIRE服务器会通过Kubernetes API来验证这个令牌的有效性,确认该代理节点确实运行在预期的命名空间中,并使用预期的服务账户进行操作,之后才会向该代理节点颁发相应的SVID。

这就是为什么SPIRE需要特定的Kubernetes API标志。必须配置kube-apiserver以支持具有正确访问权限的投影服务账户令牌,因此下面演示中的cluster config配置了--api-audiences--service-account-issuer这些参数。

工作负载认证

一旦某个节点完成了认证过程,其代理就可以对该节点上的工作负载进行认证。当某个工作负载连接到Workload API接口并请求获取SVID时,代理会通过查询Kubernetes API来收集该工作负载的相关信息(如它的Kubernetes命名空间、服务账户、Pod名称以及标签等),然后将这些信息与SPIRE服务器中注册的工作负载记录进行比对。如果找到匹配的记录,代理就会生成相应的SVID并颁发给该工作负载。

一个工作负载记录的具体格式如下:

SPIFFE ID: spiffe://example.org/ns/production/sa/checkout
Parent ID: spiffe://example.org/spire/agent/k8s_psat/default/<node-uid>
Selectors:
  k8s:ns:production
  k8s:sa:checkout

这些选择器用于指定必须匹配的Kubernetes相关信息。那些运行在production命名空间中且使用checkout服务账户的Pod,将会获得SPIFFE ID spiffe://example.org/ns/production/sa/checkout;而其他Pod则不会获得这个ID。

SVID的颁发与更新

SVID被设计为具有较短的有效期。在SPIRE中,X.509格式的SVID默认的有效时间为1小时。SPIRE代理会自动在后台更新这些SVID:生成新的密钥对,向服务器请求新的SVID,并在旧SVID过期之前将其提供给Workload API接口。

直接使用Workload API或像SPIFFE CSI驱动程序这样的工具来处理工作负载的系统,能够透明地获取到新的SVID。

采用这种短期有效的凭证机制,实际上就是实现了“零信任”安全策略。如果某个工作负载的SVID被泄露,它的有效期也只有1小时而已;而传统的Kubernetes服务账户令牌,过去是可以永久使用的。

Cilium如何利用SPIFFE实现相互TLS认证

传统的服务网格TLS解决方案(比如Istio或Linkerd)会在每个Pod中插入一个侧车代理来处理所有的网络流量和TLS握手过程。应用程序本身并不知道这些TLS操作正在发生。此外,侧车代理还会增加内存占用(对于Envoy来说,每个Pod大约需要50–100MB的内存),并且每次请求都会增加一次网络传输环节,同时还需要复杂的证书注入机制。

Cilium采取了不同的实现方式。它并没有在每个Pod中插入代理程序,而是利用eBPF技术在网络层直接处理认证流程。运行在每个节点上的Cilium代理会拦截连接请求,使用SPIFFE SVID来完成相互TLS握手,并确保认证结果得到正确执行——所有这些操作都是在内核层面完成的,不需要任何用户空间代理程序。

具体来说,当Pod A尝试与Pod B建立连接时,位于Pod A所在节点上的Cilium代理会拦截这个连接请求。它会从SPIRE Workload API中获取Pod A的SVID,然后检查是否存在要求进行相互认证的CiliumNetworkPolicy规则。如果存在这样的规则,它就会与位于Pod B所在节点上的Cilium代理进行TLS握手,同时出示Pod A的SVID,并请求获得Pod B的SVID作为响应。

两种代理都会将SVID与SPIRE信任包进行验证。如果两个SVID都是有效的,并且政策允许建立连接,那么连接就会成功建立;如果其中一个SVID无效或缺失,连接就会被终止。

位于Pod A上的应用程序会从位于Pod B上的应用程序接收数据。这两款应用程序都没有编写任何TLS相关代码,也没有使用任何辅助工具;整个认证过程完全是在它们各自所在节点上的Cilium代理中完成的。

在Cilium的模型中,Cilium代理本身会从SPIRE系统中获取一个SPIFFE身份凭证。这个身份凭证可以代表相关工作负载来请求SVID。

这与独立的SPIRE模型略有不同——在独立模型中,每个工作负载都需要自行直接请求SVID。而在Cilium模型中,Cilium管理器会根据它所管理的Kubernetes身份信息自动为工作负载在SPIRE系统中注册相应的条目,因此你无需为每个Pod手动创建SPIRE条目。

演示1——使用SPIRE集成来安装Cilium

你只需通过一条Helm命令,就能创建一个以Cilium作为CNI的kind集群,并启用其内置的SPIRE集成功能。

步骤1:安装Cilium CLI

# 在macOS系统上
brew install cilium-cli

# 在Linux系统上
CILIUMCLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --remote-name-all \
  https://github.com/cilium/cilium-cli/releases/download/${CILIUMCLI_VERSION}/cilium-linux-amd64.tar.gz
sudo tar -xzf cilium-linux-amd64.tar.gz -C /usr/local/bin

步骤2:创建一个不使用默认CNI的kind集群

必须禁用kind的默认CNI(kindnet),这样才能让Cilium取代它的位置。将以下配置保存为kind-cilium.yaml文件:

# kind-cilium.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
networking:
  disableDefaultCNI: true   # 必须设置此项,以便让Cilium担任CNI的角色
  kubeProxyMode: none       # Cilium也会取代kube-proxy的功能
kind create cluster --name k8s-mtls --config kind-cilium.yaml

在Cilium安装完成之前,这些节点会处于NotReady状态——这是因为此时还没有CNI可用。

步骤3:启用SPIRE集成后安装Cilium

由于在步骤2中设置了kubeProxyMode: none,因此Cilium必须自行承担kube-proxy的角色。这意味着它的引导节点无法通过kubernetes Service的ClusterIP来访问API服务器,因为此时还没有任何路由机制能够实现这一连接。

你需要提前提供API服务器的实际地址。可以通过Docker获取kind控制平面的IP地址:

API_SERVER_IP=$(docker inspect k8s-mtls-control-plane \
  --format='{{ .NetworkSettings.Networks.kind.IPAddress }}')
echo "API_SERVER_IP=$API_SERVER_IP"

然后使用SPIRE来安装Cilium:

helm repo add cilium https://helm.cilium.io/
helm repo update

helm upgrade cilium cilium/cilium \
  --install \
  --namespace kube-system \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost=${API_SERVER_IP} \
  --set k8sServicePort=6443 \
  --set authentication.enabled=true \
  --set authentication.mutual.spire.enabled=true \
  --set authentication.mutual.spire.install.enabled=true \
  --set authentication.mutual.spire.install.server.dataStorage.enabled=false

其中一些参数很容易被忽略,但每一个都至关重要:

  • kubeProxyReplacement=true:Cilium会安装自己基于eBPF的替代组件来替换kube-proxy。当配置文件中设置kubeProxyMode: none时,这个参数是必须指定的。

  • k8sServiceHost / k8sServicePort:在Cilium能够开始路由服务请求之前,这些参数用于指定API服务器的地址。在EKS/GKE/AKS环境中,由于安装过程中仍然存在kube-proxy,因此这些参数不是必需的。

  • authentication.enabled=true:这个参数与authentication.mutual.spire.enabled=true一起必须被设置。如果只设置了authentication.mutual.spire.enabled=true,Charmain的validate.yaml文件会提示安装失败,因为SPIRE集成要求这两个参数都必须为true。

  • dataStorage.enabled=false:这个参数用于将SPIRE服务器的数据存储方式从基于PVC的方式切换为内存存储方式。对于实验室环境来说这没问题,但在生产环境中,建议保持这一设置,并确保你的集群具备PersistentVolume支持。

需要注意的是,这里并没有--wait这个参数。在全新的集群上使用这个参数会导致安装失败,因为安装过程本身就是一个竞争性进程。SPIRE服务器需要先在状态为NotReady的节点上启动,然后Cilium代理才会通过SPIRE开始运行,最后这些节点的状态才会变为Ready。因此,安装完成后不需要等待太久,只需观察接下来约2分钟内是否有新的Pod启动即可:

kubectl get pods -A -w

步骤4:验证安装结果

cilium status --wait
    /¯¯\
 /¯¯\__/¯¯\    Cilium:             OK
 \__/¯¯\__/    Operator:           OK
 /¯¯\__/¯¯\    Envoy DaemonSet:    OK
 \__/¯¯\__/    Hubble Relay:       disabled
    \__/       ClusterMesh:        disabled

DaemonSet              cilium             Desired: 3, Ready: 3/3, Available: 3/3
DaemonSet              cilium-envoy       Desired: 3, Ready: 3/3, Available: 3/3
Deployment             cilium-operator    Desired: 2, Ready: 2/2, Available: 2/2

每个节点上都会有三个Cilium代理进程,包括控制平面节点(在配置文件中未设置任何限制)。你还可以检查cilium-spire命名空间中的SPIRE相关组件。

kubectl get all -n cilium-spire
名称                    是否准备就绪   状态    重启次数   运行时间
pod/spire-agent-2cpsr   1/1     正在运行   0          3分钟
pod/spire-agent-klhjx   1/1     正在运行   0          3分钟
pod/spire-agent-vhsnc   1/1     正在运行   0          3分钟
pod/spire-server-0      2/2     正在运行   0          3分钟

名称                              类型        集群IP地址    端口        运行时间
service/spire-server              集群IP       10.96.x.x     8081/TCP     3分钟

名称                          期望状态   实际状态   是否准备就绪   运行时间
daemonset.apps/spire-agent    3         3         3           3分钟

名称                             是否准备就绪   运行时间
statefulset.apps/spire-server    1/1     3分钟

每个节点上都有一个SPIRE代理程序。SPIRE服务器是一个StatefulSet,它由两个容器组成:一个是服务器本身,另一个是SPIRE控制器管理器,该管理器会自动为Cilium身份创建工作负载注册条目。

运行命令来检查SPIRE服务器的运行状态:

kubectl exec -n cilium-spire spire-server-0 -c spire-server -- \
  /opt/spire/bin/spire-server healthcheck
kubectl exec -n cilium-spire spire-server-0 -c spire-server -- \
  /opt/spire/bin/spire-server agent list
演示2——使用CiliumNetworkPolicy实施相互TLS加密

继续在演示1中使用的同一个集群上进行操作,您将部署两个服务,并通过CiliumNetworkPolicy强制它们之间进行相互认证,然后验证经过认证的流量是否能够正常传输,同时确认未经认证的连接会被阻断。

在这里,所有的请求都会使用SPIRE服务器生成的SVID来进行身份验证。这两个演示是一个连续的整体流程,并非独立的练习环节。

步骤1:部署客户端和服务器

这个文件中同时包含了服务器端和客户端代码——其中客户端实际上是一个处于“睡眠”状态的curl容器,我们会使用它来执行后续操作。

# echo-workloads.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-server
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo-server
  template:
    metadata:
      labels:
        app: echo-server
    spec:
      containers:
        - name: echo-server
          image: ealen/echo-server:latest
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: echo-server
  namespace: default
spec:
  selector:
    app: echo-server
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-client
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo-client
  template:
    metadata:
      labels:
        app: echo-client
    spec:
      containers:
        - name: client
          image: curlimages/curl:latest
          command: ["sleep", "infinity"]
kubectl apply -f echo-workloads.yaml
# kubectl rollout status一次只能检测一个资源的状态
kubectl rollout status deployment/echo-server -n default
kubectl rollout status deployment/echo-client -n default

步骤2:确认无需身份验证即可正常通信

在启用相互TLS认证之前,需要先确认客户端能够成功连接到服务器:

CLIENT=$(kubectl get pod -l app=echo-client -o jsonpath '{.items[0].metadata.name}')
kubectl exec $CLIENT -- curl -s http://echo-server/

你应该会收到来自echo服务器的JSON响应。此时,通信过程是无需任何身份验证即可正常进行的。

步骤3:应用要求相互认证的CiliumNetworkPolicy规则

CiliumNetworkPolicy规则中添加authentication.mode: required选项,就可以强制要求所有匹配的通信流量都必须进行相互TLS认证。连接双方都必须提供有效的SPIFFE SVID证书:

# mtls-policy.yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: echo-server-mtls
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: echo-server
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: echo-client
      authentication:
        mode: required     # 要求所有此类通信都必须进行相互TLS认证
kubectl apply -f mtls-policy.yaml

步骤4:验证经过认证的通信流量是否依然可以正常传输

kubectl exec $CLIENT -- curl -s http://echo-server/

连接成功。Cilium拦截了该连接,在两个Pod节点上的Cilium代理之间完成了SPIFFE mTLS握手流程,验证了双方的SVID信息,随后允许数据流通过。客户端应用程序发送了一个普通的HTTP请求,并收到了响应——这种相互认证的过程是在网络层完成的,且整个过程是完全透明的。

步骤5:使用Hubble观察认证过程(可选)

Hubble是Cilium提供的可视化工具。使用它需要单独安装相应的CLI:


# 在macOS系统中
brew install hubble

# 在Linux系统中
HUBBLE_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/hubble/master/stable.txt)
curl -L --remote-name-all \
  https://github.com/cilium/hubble/releases/download/${HUBBLE_VERSION}/hubble-linux-amd64.tar.gz
sudo tar -xzf hubble-linux-amd64.tar.gz -C /usr/local/bin

在集群中启用Hubble工具,然后观察数据流。执行`cilium hubble enable`命令会部署Hubble Relay,并重新启动Cilium代理,从而激活其中的Hubble服务器;因此在进行端口转发之前,请等待这些服务完全启动。如果跳过这个等待步骤,端口转发操作可能会在Relay尚未开始监听时就被触发,从而导致连接失败,出现“connection reset by peer”或“rpc error … EOF”等错误信息。


cilium hubble enable
cilium status --wait          # 等待直到显示“Hubble Relay: OK”后再继续操作

cilium hubble port-forward &
# 观察echo-server的数据流(按Ctrl-C可停止观察)
hubble observe --namespace default --pod echo-server --follow

在另一个终端中发送另一个请求:


kubectl exec $CLIENT -- curl -s http://echo-server/

在Hubble的输出结果中,你会看到如下信息:


ℹ️ Hubble Relay已启动,地址为127.0.0.1:4245
Jul  7 12:44:42.380: default/echo-client-86d446b8f-9bn5v:47500 (ID:2822) -> default/echo-server-7467b4b54d-5tvkz:80 (ID:30854) policy-verdict:none TRAFFIC_direction_UNKNOWN ALLOWED (TCP Flags: SYN)
Jul  7 12:44:42.380: default/echo-client-86d446b8f-9bn5v:47500 (ID:2822) -> default/echo-server-7467b4b54d-5tvkz:80 (ID:30854) to-endpoint FORWARDED (TCP Flags: SYN)
Jul  7 12:44:42.381: default/echo-client-86d446b8f-9bn5v:47500 (ID:2822) <- default/echo-server-7467b4b54d-5tvkz:80 (ID:30854) to-endpoint FORWARDED (TCP Flags: SYN, ACK)

“ALLOWED”这一判定结果以及“policy-verdict”的具体原因,都证明了CiliumNetworkPolicy规则得到了正确应用,认证过程也已经完成。整个过程中并没有使用任何额外的侧车组件——所有的操作都是由Cilium代理本身完成的。

更喜欢图形化界面吗?可以启用Hubble的UI工具。上述所有步骤其实都是通过API或终端命令来执行的;而Hubble还提供了带有实时服务地图的Web管理界面,但这个功能是需要单独启用的,执行`cilium hubble enable`命令后,默认情况下并不会自动启动该界面。

# 添加用户界面功能:允许重新运行任务,保留Relay组件,并添加hubble-ui部署配置  
cilium hubble enable --ui  

# 等待该配置准备好后再打开用户界面——这个过程与配置Relay组件的过程是同时进行的。如果跳过这一步,执行`cilium hubble ui`命令时会遇到“连接被拒绝”的错误,因为此时hubble-ui的用户界面前端容器尚未开始运行。  
kubectl -n kube-system rollout status deployment/hubble-ui --timeout=90s  

# 将hubble-ui的端口映射到本地主机,并在浏览器中访问http://localhost:12000  
cilium hubble ui  

从下拉列表中选择default命名空间。演示用Pod和策略就位于这个命名空间中。该服务映射是实时更新的:它会随着数据流的变化立即显示相应的连接路径,因此空闲的命名空间在界面上会显示为空白状态。要让它“活跃”起来,请执行以下命令:

kubectl exec $CLIENT -- curl -s http://echo-server/

你会看到一条被标记为echo-client → echo-server的连接路径。点击这条路径(或查看底部的流表),你会看到policy-verdict: ALLOWED这一提示,说明该连接是被允许通过的。请保持这个界面打开状态,直到完成第6步操作。当你在那里运行unauthorized-client测试时,其连接会以一条红色的dropped路径显示出来,这种视觉表现实际上反映了curl请求的超时情况。

Hubble UI——实时显示的命名空间服务映射,包括被转发和被拒绝的数据流

该用户界面由三个部分组成。

顶部的服务映射会将每个工作负载的标识显示为一个个框,而每条被检测到的连接则会以不同颜色来表示其状态:echo-client → echo-server:80这条路径用绿色实线表示,表示请求被成功转发;而标记为default的框(实际上对应的是unauthorized Pod)与echo-server之间的连接则用红色虚线表示,意味着请求被拒绝了。echo-server上的→ 80 TCP端口处显示的🔒锁形符号,说明该端口已经通过了策略验证。

下方的流表会为每条数据流记录一行信息,包括源地址、目标地址、目标端口、L7层协议信息、处理结果以及时间戳。这样你就可以将echo-client → echo-server这类被允许通过的请求与default → echo-server这类被拒绝的请求并排查看。flow table中的记录方式与CLI命令行工具是一样的,即每条数据流都会占用一行。

顶部的导航栏包含了命名空间选择器、数据流过滤选项、任何处理结果/可视化显示切换按钮,以及实时显示的flows/s吞吐量统计信息和报告节点的数量(例如3/3)。

第6步:验证没有相应标签的Pod是否会被阻止

部署第三个没有echo-client标签的Pod,然后尝试连接服务器:

# unauthorized-client.yaml
apiVersion: v1
kind: Pod
metadata:
  name: unauthorized
  namespace: default
spec:
  containers:
    - name: client
      image: curlimages/curl:latest
      command: ["sleep", "infinity"]
kubectl apply -f unauthorized-client.yaml
kubectl wait --for=condition=Ready pod/unauthorized --timeout=60s
kubectl exec unauthorized -- curl -sS --max-time 5 http://echo-server/
curl: (28) 连接在 5000 毫秒后超时

连接发生了超时。CiliumNetworkPolicy仅允许标签为 app: echo-client 的Pod进行入站通信。没有该标签的Pod既无法匹配到SVID,也无法符合任何策略规则,因此Cilium会默默地丢弃这些数据包。

这里有两个需要注意的地方:在执行相关操作之前,请先运行 kubectl wait 命令。如果在执行 apply 命令后太快就运行 exec,就会收到 “container not found (“client”)” 的错误提示,因为此时Pod的容器尚未启动。

另外,请使用 curl -sS 而不是单纯的 -s。如果只使用 -s,curl会忽略错误信息,你只会看到 “command terminated with exit code 28” 这样的提示。虽然结果是一样的——28确实表示连接超时——但使用 -S 可以让你看到详细的错误信息。出现超时现象而不是 “连接被拒绝”,正是这种策略导致数据包被默默地丢弃的体现;如果真的是被主动拒绝,系统会立即返回不同的错误信息。

步骤 7:检查SPIRE中的工作负载条目

Cilium的SPIRE控制器管理器会自动为该集群中的Cilium安全身份创建相应的SPIFFE身份。你可以这样查看这些信息:

kubectl exec -n cilium-spire spire-server-0 -c spire-server -- \
  /opt/spire/bin/spire-server entry show \
  -selector cilium:mutual-auth

每条记录都会将一个Cilium安全身份与一个SPIFFE ID对应起来。Cilium的操作系统会自动管理这个注册表,因此在使用Cilium的内置集成功能时,你根本不需要手动注册工作负载。

结论

IP地址代表的是位置信息,而非身份信息。在Kubernetes环境中,每次部署都会导致IP地址发生变化,因此任何基于IP地址匹配的策略都会随着时间的推移而失效。

而使用加密技术来标识工作负载,则可以从根本上解决这个问题。SPIFFE定义了这种识别模型:一个SPIFFE ID可以用来标识某个信任域内的工作负载,而X.509 SVID则可以将这个身份信息转化为任何TLS库都能验证的证书;SPIRE则负责实现这一机制:服务器同时充当CA和注册机构的角色,而每个节点上的代理则会通过Kubernetes PSAT机制来生成短期有效、会自动更新的SVID。

Cilium将这种身份识别机制融入到了网络架构中。只需在CiliumNetworkPolicy中添加 authentication.mode: required 这一配置,其eBPF代理就会获取相关工作负载的SVID信息,执行相互TLS握手流程,并强制执行相应的访问控制规则。这种方式不需要额外的组件,也不需要修改应用程序代码,而且与服务网格相比,其开销几乎为零。整个解决方案只需通过一条Helm命令即可部署完成——所有的复杂性都体现在基础设施层面,而不会影响到你的代码。

清理操作

# 删除演示用工作负载
kubectl delete deployment echo-server echo-client -n default
kubectl delete service echo-server -n default
kubectl delete pod unauthorized -n default
kubectl delete ciliumnetworkpolicy echo-server-mtls -n default

# 卸载Cilium组件(helm不会删除它创建的cilium-spire命名空间)
helm uninstall cilium -n kube-system
kubectl delete namespace cilium-spire

# 删除整个集群(这是最简单的清理方式)
kind delete cluster --name k8s-mtls
Comments are closed.