# 用 SPIRE 为 Pod 提供身份

## 开始之前

SPIFFE 是一个认证框架，能为多种节点和工作负载类型提供证实能力，解决“我是我”的问题，前面文章演示过用 SPIRE 给类 Unix 进程提供身份的方法，今天这篇就试试给 Pod 提供身份。

这次实验会在前面的基础之上，在 Kubernetes 集群之外运行独立的 SPIRE Server，在集群中用 Pod 的形式运行 SPIRE Agent 作为节点，最后在其它 Pod 中访问 SPIRE Agent，获取 SVID。本文所涉及的对象关系如下图所示：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745914467215/e2e4c523-7d03-486b-a998-caac45f78232.png align="center")

开始之前，需要做一些准备：

* 有一个 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/](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

简单粗暴上配置：

```plaintext
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 文件，访问方式还有其他的配置内容，可以参考[官方文档](https://github.com/spiffe/spire/blob/main/doc/plugin_server_notifier_k8sbundle.md)。要注意的是，这里使用的 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。

> 更多配置信息可以参考[官方文档](https://github.com/spiffe/spire/blob/main/doc/plugin_server_nodeattestor_k8s_sat.md)

Server 启动成功后，可以提前为工作负载创建 Node 和 Entry：

```text
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:sa:default
```

首先用 `k8s_sat:cluster:kindcluster` 创建了一个在 `spiffe.dom` 中的 Node 条目，它的 SPIFFE ID 是 `spiffe://spiffe.dom/clusters/kindcluster`；

接下来以 Node 条目为上级，使用 `k8s:ns:default` + `k8s:sa:default` 的 Selector，创建一个 SPIFFE ID `spiffe://spiffe.dom/ns/default/sa/default`，代表在 `default` 命名空间中用 `default` Service Account 身份运行的 Pod。

## 创建 Agent

在运行 Agent 之前，首先要制作一个镜像，这里偷懒的使用现成二进制进行构建：

```Dockerfile
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 片段：

```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 的配置文件如下：

```plaintext
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 SERVER
    
* `trust_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 识别，因此运行如下工作负载：

```yaml
...
      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 了：

```plaintext
$ 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 的版图。
