用 SPIRE 为 Pod 提供身份
开始之前
SPIFFE 是一个认证框架,能为多种节点和工作负载类型提供证实能力,解决“我是我”的问题,前面文章演示过用 SPIRE 给类 Unix 进程提供身份的方法,今天这篇就试试给 Pod 提供身份。
这次实验会在前面的基础之上,在 Kubernetes 集群之外运行独立的 SPIRE Server,在集群中用 Pod 的形式运行 SPIRE Agent 作为节点,最后在其它 Pod 中访问 SPIRE Agent,获取 SVID。本文所涉及的对象关系如下图所示:
开始之前,需要做一些准备:
- 有一个 Kubernetes 集群,Kind 或者 Minikube 也都是可以完成测试的。
- SPIRE 1.5.x 的二进制文件,可以从
https://spiffe.io/downloads/
下载 - 构建镜像所需的基础镜像和 Podman/Docker 等工具。
Kubernetes 相关插件
这里要用到 SPIRE 的三个插件:
Kubernetes Node Attestor:用于证实 Node 身份,需要分别在 Server 和 Agent 两侧进行配置。目前可以选择 k8s_sat
或者 k8s_psat
两种插件,两侧的插件选择应保持一致,分别用于 ServiceAccount Token 和新版本 Kubernetes 中新增的 Projected ServiceAccount Token,本文选择的是 k8s_sat
。
Projected Token 具有更好的安全性,延伸阅读:https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
Kubernetes Bundle:Trust Bundle 是数字证书的集合,在 Kubernetes 中往往需要使用 Configmap 来存储和共享,所以一个直接的想法就是通过 spire-server bundle show
命令来获取证书集合,并生成 Configmap。但是这个插件可以方便地通过 Kubernetes API 来自动维护证书集合到 Configmap 的转换过程,并自动完成轮转工作。
Kubernetes Workload Attestor:用于证实 Workload 身份,只需要在 Agent 中配置即可。
配置和启动 SPIRE Server
简单粗暴上配置:
server {
...
}
}
plugins {
DataStore "sql" {
...
}
KeyManager "disk" {
...
}
Notifier "k8sbundle" {
plugin_data {
kube_config_file_path = "/home/dustise/.kube/config"
}
}
NodeAttestor "k8s_sat" {
plugin_data {
clusters = {
"kindcluster" = {
service_account_allow_list = ["spire:spire-agent"]
use_token_review_api_validation = true
上面的 SPIRE Server 配置中,省略了通用部分,具体内容可以参考前面一篇文章,重点看一下两节 Kubernetes 相关配置。
k8sbundle
的作用就是把 Trust Bundle 内容保存到 Configmap 里面,因此是需要和 API Server 打交道的,这里给他直接配置了一个 KubeConfig 文件,访问方式还有其他的配置内容,可以参考官方文档。要注意的是,这里使用的 KubeConfig 文件所包含的账号是 Cluster Admin 权限,如果使用其他的账号,需要具备对 Configmap 进行 create 和 patch 操作的授权。
k8s_sat
一节中,clusters
字段是一个 Map,其中可以对接多个 Kubernetes 集群,这里我们填充了三个字段:
service_account_allow_list
:允许 Agent 注册时使用的 Service Account。use_token_review_api_validation
:使用 TokenReview API 对 Serivce Account Token 进行验证,除此之外,还可以使用证书进行认证。kube_config_file
:和 API Server 进行沟通的凭据。
和 Bundle 类似,这里同样需要具备一定的权限来完成 SPIRE Server 的工作,
- Configmap 的 patch、get、list
- tokenreviews 的 create
创建好配置文件之后,可以先在目标集群中创建 spire
命名空间。使用 spire-server -config=[config file path]
命令启动服务器。稍后会在集群中看到新建的 Configmap。
更多配置信息可以参考官方文档
Server 启动成功后,可以提前为工作负载创建 Node 和 Entry:
spire-server entry create -socketPath=socks/spire-server.sock \
-spiffeID spiffe://spiffe.dom/clusters/kindcluster \
-selector k8s_sat:cluster:kindcluster -node
spire-server entry create -socketPath=socks/spire-server.sock \
-spiffeID spiffe://spiffe.dom/ns/default/sa/default \
-parentID spiffe://spiffe.dom/ns/spire/sa/spire-agent \
-selector k8s:ns:default \
-selector k8s🈂️default
首先用 k8s_sat:cluster:kindcluster
创建了一个在 spiffe.dom
中的 Node 条目,它的 SPIFFE ID 是 spiffe://spiffe.dom/clusters/kindcluster
;
接下来以 Node 条目为上级,使用 k8s:ns:default
+ k8s🈂️default
的 Selector,创建一个 SPIFFE ID spiffe://spiffe.dom/ns/default/sa/default
,代表在 default
命名空间中用 default
Service Account 身份运行的 Pod。
创建 Agent
在运行 Agent 之前,首先要制作一个镜像,这里偷懒的使用现成二进制进行构建:
FROM busybox:1.35.0-glibc
RUN mkdir -p /spire/bin
COPY spire-agent /spire/bin
CMD ["/spire/bin/spire-agent", "-config=/spire/conf/k8s-agent.conf"]
这里要创建一个 Agent 的工作负载,为了让 Agent 能够通过进程号查询工作负载的 Pod 信息,并对工作负载提供 Workload API,需要满足几个条件:
- Agent 需要有授权访问 Kubernetes 的特定资源
- 共享 Socket 文件,让 Workload 可以访问 Agent 提供的 Workload API
- 能够识别调用 Workload API 的进程的 Pod 信息,从而生成 Selector
综合以上考虑,我们需要设计这样的 Workload:
- 用主机卷的方式在每个节点上暴露 Socket
- 能够访问 Trust Bundle 所在的 Configmap
- Agent 和 Workload 共享 IPC 空间,便于通过进程号识别身份
- Agent 所使用的 Service Account 需要具备和 API Server/Kubelet 通信查询信息的能力。
因此产生如下的 YAML 片段:
spec:
hostPID: true
hostNetwork: true
serviceAccountName: spire-agent
...
containers:
- name: spire-agent
image: gcr.io/spiffe-io/spire-agent:1.5.0
args: ["-config", "/run/spire/config/agent.conf"]
volumeMounts:
- name: spire-config
mountPath: /run/spire/config
readOnly: true
- name: spire-bundle
mountPath: /run/spire/bundle
- name: spire-agent-socket
mountPath: /run/spire/sockets
readOnly: false
volumes:
- name: spire-config
configMap:
name: spire-agent
- name: spire-bundle
configMap:
name: spire-bundle
- name: spire-agent-socket
hostPath:
path: /run/spire/sockets
type: DirectoryOrCreate
...
这段 YAML 有几个要点:
- 使用了符合 SPIRE Server 配置中要求的 ServiceAccount
- HostPID 共享主机 PID 空间
- HostNetwork 共享主机网络空间
- 加载 Trust Bundle 所在的 Configmap
- 加载一个主机卷用于输出 Socket 文件
- 用一个 Configmap 保存配置文件并加载
Agent 的配置文件如下:
agent {
data_dir = "/run/spire"
log_level = "DEBUG"
server_address = "10.211.55.5"
server_port = "8081"
socket_path = "/run/spire/sockets/agent.sock"
trust_bundle_path = "/run/spire/bundle/bundle.crt"
trust_domain = "spiffe.dom"
}
plugins {
NodeAttestor "k8s_sat" {
plugin_data {
cluster = "kindcluster"
}
}
KeyManager "memory" {
...
}
WorkloadAttestor "k8s" {
plugin_data {
skip_kubelet_verification = true
}
}
}
Agent 配置相对来说稍显复杂:
server_address
和server_pod
,用于访问前面启动的 SPIRE SERVERtrust_bundle_path
引用 Configmap 的加载路径即可trust_domain
需要保持和 SPIRE Server 定义一致k8s_sat
的cluster
字段中,集群名称需要和 SPIRE Server 的 Map 中的定义匹配skip_kubelet_verification
:跳过对 Kubelet 证书的检查
Agent 使用的 Service Account 也需要进行 RBAC 授权,需要能够对 pod
、node
以及 node/proxy
进行 get
操作。
先后把配置 Configmap、RBAC 以及 Daemonset 等资源提交之后,会看到 Agent Pod 启动。
启动客户端
任意启动一个客户端程序,为模仿接入 Workload API 的实现,其中还是需要使用 SPIRE Agent 的二进制。客户端应该使用 Agent 的 Socket 访问 Wokrload API,同时为了表明身份,同样需要用 HostPID
供 Agent 识别,因此运行如下工作负载:
...
hostPID: true
hostNetwork: true
...
containers:
- name: client
image: gcr.io/spiffe-io/spire-agent:1.2.3
command: ["sleep"]
args: ["1000000000"]
volumeMounts:
- name: spire-agent-socket
mountPath: /run/spire/sockets
readOnly: true
volumes:
- name: spire-agent-socket
hostPath:
path: /run/spire/sockets
type: Directory
Pod 在 default
命名空间启动之后,进入 Shell 使用 spire-agent api fetch
命令,就能成功的获取 SVID 了:
$ bin/spire-agent api fetch -socketPath=/run/spire/sockets/agent.sock
Received 1 svid after 83.772792ms
SPIFFE ID: spiffe://spiffe.dom/shutup
SVID Valid After: 2022-11-24 17:02:03 +0000 UTC
SVID Valid Until: 2022-11-24 17:04:13 +0000 UTC
CA #1 Valid After: 2022-11-23 14:57:51 +0000 UTC
To be continued
现在我们就用一个非常笨拙的方法,把 Kubernetes 的工作负载识别能力接入到了 SPIRE Server 里面了。事实上接入 Kubernetes 还有别的部署和使用方式,例如使用 CRD、在集群内运行 SPIRE Server、使用 Envoy 等接入 Workload API 等。官网文档中对这些案例都有较为详细的指导。
结合前面对于 Ghostunel 等的介绍,不难看出,打通虚拟机和 Kubernetes 工作负载身份是可行的,而根据联邦一文的描述,这个体系还可以和 OIDC 等进行互通,进一步扩大 SPIFFE SVID 的版图。