本文为 SPIRE 官方文档 plugin_agent_workloadattestor_k8s 的中文译本,以英文原文为准。 🔗 穿越源码分析:K8S Agent 详解 · k8s 工作负载认证
Agent 插件:WorkloadAttestor "k8s"
k8s 插件为调用 agent 的工作负载(workload)生成基于 Kubernetes 的选择器(selector)。其做法是:先从工作负载的 cgroup 归属中取得其 pod ID,再向 kubelet 查询该 pod 的信息。
插件可通过不安全的只读端口或安全端口与 kubelet 通信。对安全端口,同时支持 X509 客户端认证和 bearer token(如 service account token)认证。
对 kubelet 在安全端口上出示的证书进行验证是可选的。默认会基于经 kubelet_ca_path 传入的证书文件进行验证。可设置 skip_kubelet_verification 以禁用验证。
agent 会使用通过 node_name_env 或 node_name 配置项获得的节点名来联系 kubelet。若未取得节点名,则通过 127.0.0.1 联系 kubelet(需要启用 host networking)。在后一种情况下,会使用主机名对 kubelet 证书执行证书服务器名(server name)校验。
注意 通过 bearer token 进行 kubelet 认证,要求 kubelet 以
--authentication-token-webhook标志启动。 详见 Kubelet authentication/authorization。
注意 kubelet 使用 TokenReview API 校验 bearer token。 这要求能够访问 Kubernetes API server。因此,API server 停机可能中断工作负载认证。 kubelet 的
--authentication-token-webhook-cache-ttl标志控制 kubelet 缓存 TokenReview 响应的时长,或有助于缓解该问题。 但不建议将缓存 ttl 设得过大,因为那会影响权限撤销的及时性。
注意 与 kubelet 的匿名认证要求 kubelet 以
--anonymous-auth标志启动。不鼓励在生产中使用匿名认证 模式,因为它需要授权匿名用户访问nodes/proxy资源,而该资源映射到一些特权操作,例如在容器中执行命令、 读取 pod 日志。
注意 若要在 Windows 容器上运行,需要 Kubernetes v1.24+ 和 containerd v1.6+, 因为 agent 容器上需要 hostprocess 容器。
| 配置项 | 说明 |
|---|---|
disable_container_selectors | 若为 true,则不产生容器选择器。可用于在已知工作负载 pod、但认证时工作负载容器尚未就绪的情况下,产生 pod 选择器。 |
kubelet_read_only_port | kubelet 只读端口。与 kubelet_secure_port 互斥。 |
kubelet_secure_port | kubelet 安全端口。除非设置了 kubelet_read_only_port,否则默认为 10250。 |
kubelet_ca_path | 磁盘上一个文件的路径,该文件包含用于验证 kubelet 证书的 CA 证书。除非设置了 skip_kubelet_verification,否则必填。默认为集群 CA bundle /run/secrets/kubernetes.io/serviceaccount/ca.crt。 |
skip_kubelet_verification | 若为 true,则跳过 kubelet 证书验证。 |
token_path | 磁盘上用于 kubelet 认证的 bearer token 的路径。默认为 service account token /run/secrets/kubernetes.io/serviceaccount/token。 |
certificate_path | 磁盘上用于 kubelet 认证的客户端证书的路径。 |
private_key_path | 磁盘上用于 kubelet 认证的客户端密钥的路径。 |
use_anonymous_authentication | 若为 true,则对 kubelet 通信使用匿名认证。 |
node_name_env | 用于获取节点名的环境变量。默认为 MY_NODE_NAME。 |
node_name | 节点名。覆盖由 node_name_env 指定的环境变量所获得的值。 |
experimental | 实验性选项,可能变更或移除(见下文)。 |
sigstore | Sigstore 选项。选项见下文。参见 Sigstore 选项。设置后,启用对容器镜像签名与证明(attestation)的验证。 |
use_new_container_locator | 若为 true,启用支持 cgroups v2 的新容器定位算法。默认为 true。 |
verbose_container_locator_logs | 若为 true,启用用于定位容器的 mountinfo 与 cgroup 信息的详细日志。默认为 false。 |
以下是当前的实验性配置。
| experimental | 说明 | 默认值 |
|---|---|---|
api_server.cache.enabled | 若为 true,为对象引用查找启用 controller-runtime 的 Kubernetes API server 缓存。 | false |
broker | 用于 AttestReference 的 Broker API 选项。当此插件处理 Broker API 引用时必填。参见 Broker API。 |
Sigstore 功能
该功能为 k8s 工作负载认证器扩展了使用 Sigstore 生态验证容器镜像签名与证明(attestation)的能力。它是可选的,仅在配置了 sigstore 块时启用。
Sigstore 选项
| 选项 | 说明 |
|---|---|
allowed_identities | 将 OIDC Provider URI 映射到允许的主体(subject)列表。支持正则表达式模式。默认为空。若未指定,则接受来自任意签发者的签名。(例如 "https://accounts.google.com" = ["subject1@example.com","subject2@example.com"])。 |
skipped_images | 列出要从 Sigstore 签名验证中排除的镜像 ID。对这些镜像,不会生成任何 Sigstore 选择器。默认为空列表。 |
rekor_url | 指定用于透明日志(transparency log)验证的 Rekor URL。默认是公共 Rekor 实例 https://rekor.sigstore.dev。 |
ignore_tlog | 若设为 true,则跳过透明日志验证,且不生成基于 Rekor bundle 的选择器。 |
ignore_attestations | 若设为 true,则跳过镜像证明验证,且不生成 image-attestations:verified 选择器。 |
ignore_sct | 若设为 true,则跳过签名证书时间戳(Signed Certificate Timestamp, SCT)验证。 |
registry_credentials | 将每个镜像仓库 URL 映射到对应的认证凭据。示例:{"docker.io": {"username": "user", "password": "pass"}}。 |
自定义 CA 根
可通过 cosign initialize 命令提供经 TUF 签名的自定义 CA 根。此方法会安全地固定(pin)CA 根,确保验证过程中只使用受信任的证书。此外,还可通过 SIGSTORE_ROOT_FILE 环境变量指定用于证书验证的受信任根。有关 Cosign 配置的更多细节,请参阅文档。
K8s 选择器
| 选择器 | 取值 |
|---|---|
| k8s:ns | 工作负载的命名空间(namespace) |
| k8s:sa | 工作负载的 service account |
| k8s:container-image | 工作负载 pod 中正在请求 SVID 的那个容器的 Image 或 ImageID(由 K8S 报告)。选择器值可能是镜像标签,如 docker.io/envoyproxy/envoy-alpine:v1.16.0,也可能是解析后的 SHA256 镜像摘要,如 docker.io/envoyproxy/envoy-alpine@sha256:bf862e5f5eca0a73e7e538224578c5cf867ce2be91b5eaed22afc153c00363eb。使用基于标签的选择器时的重要注意事项,参见镜像选择器的局限性。 |
| k8s:container-name | 工作负载容器的名称 |
| k8s:node-name | 工作负载所在节点的名称 |
| k8s:pod-label | 赋予工作负载 pod 的一个标签 |
| k8s:pod-owner | 工作负载 pod 所有者(owner)的名称 |
| k8s:pod-owner-uid | 工作负载 pod 所有者的 UID |
| k8s:pod-uid | 工作负载 pod 的 UID |
| k8s:pod-name | 工作负载 pod 的名称 |
| k8s:pod-image | 工作负载 pod 中任意容器的 Image 或 ImageID(由 K8S 报告)。选择器值可能是镜像标签,如 docker.io/envoyproxy/envoy-alpine:v1.16.0,也可能是解析后的 SHA256 镜像摘要,如 docker.io/envoyproxy/envoy-alpine@sha256:bf862e5f5eca0a73e7e538224578c5cf867ce2be91b5eaed22afc153c00363eb。使用基于标签的选择器时的重要注意事项,参见镜像选择器的局限性。 |
| k8s:pod-image-count | 工作负载 pod 中容器镜像的数量 |
| k8s:pod-init-image | 工作负载 pod 中任意 init 容器的 Image 或 ImageID(由 K8S 报告)。选择器值可能是镜像标签,如 docker.io/envoyproxy/envoy-alpine:v1.16.0,也可能是解析后的 SHA256 镜像摘要,如 docker.io/envoyproxy/envoy-alpine@sha256:bf862e5f5eca0a73e7e538224578c5cf867ce2be91b5eaed22afc153c00363eb。使用基于标签的选择器时的重要注意事项,参见镜像选择器的局限性。 |
| k8s:pod-init-image-count | 工作负载 pod 中 init 容器镜像的数量 |
启用 Sigstore 后可用的选择器(当配置使用 sigstore 时可用)
| 选择器 | 取值 |
|---|---|
| k8s:image-signature:verified | 镜像签名已验证且有效时。 |
| k8s:image-attestations:verified | 镜像证明已验证且有效时。 |
| k8s:image-signature-value | 签名的 base64 编码值(例如 k8s:image-signature-content:MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smA=) |
| k8s:image-signature-subject | 签名镜像的 OIDC 主体(principal)(例如 k8s:image-signature-subject:spirex@example.com) |
| k8s:image-signature-issuer | 签名的 OIDC 签发者(例如 k8s:image-signature-issuer:https://accounts.google.com) |
| k8s:image-signature-log-id | Rekor 透明日志条目的唯一 LogID(例如 k8s:image-signature-log-id:c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b95918123) |
| k8s:image-signature-log-index | Rekor 透明日志条目的日志索引(例如 k8s:image-signature-log-index:105695637) |
| k8s:image-signature-integrated-time | 镜像签名被并入签名透明日志的时间(Unix 时间戳格式)(例如 k8s:image-signature-integrated-time:1719237832) |
| k8s:image-signature-signed-entry-timestamp | 已签名条目的 base64 编码值(对 logID、logIndex、body 和 integratedTime 的签名)(例如 k8s:image-signature-integrated-time:MEQCIDP77vB0/MEbR1QKZ7Ol8PgFwGEEvnQJiv5cO7ATDYRwAiB9eBLYZjclxRNaaNJVBdQfP9Y8vGVJjwdbisme2cKabc) |
若 ignore_tlog 设为 true,则不会生成基于 Rekor bundle 的选择器(-log-id、-log-index、-integrated-time 和 -signed-entry-timestamp)。
注意
container-image只会匹配 pod 中代表 pod 联系 SPIRE 的那个特定容器,而pod-image和pod-init-image会分别匹配 Pod 中的任意容器或任意 init 容器。
Broker API
当启用 SPIRE Agent 的 SPIFFE Broker API 时,k8s 工作负载认证器会处理针对 WorkloadPIDReference 和 KubernetesObjectReference 的 Broker API AttestReference 请求。AttestReference 要求插件配置中包含 experimental.broker 块。每个 experimental.broker.brokers 条目标识一个可使用此插件的 broker SPIFFE ID。Broker ID 必须合法、唯一且非空。块级的必填设置 access_policy 控制插件是否为已解析的对象创建 Kubernetes SubjectAccessReview 请求。使用 access_policy = "enforced",在返回选择器之前用 Kubernetes 对每个已解析对象进行授权。使用 access_policy = "permissive" 则跳过该授权检查。每个 broker 可将 pod_reference_scope 设为 agent_node(默认)或 cluster;这只影响 pod KubernetesObjectReference 的解析。
示例:
WorkloadAttestor "k8s" {
plugin_data {
experimental {
broker {
access_policy = "enforced"
brokers = [
{
id = "spiffe://example.org/broker"
pod_reference_scope = "cluster"
}
]
}
}
}
}WorkloadPIDReference 遵循基于 PID 的 k8s 认证路径来解析工作负载 pod 及选择器。当该 Broker API 引用解析到一个 pod 且 experimental.broker.access_policy = "enforced" 时,插件会创建一个 SubjectAccessReview,询问该 broker SPIFFE ID 是否可对已解析的 pod 使用 SPIRE 自定义的 Kubernetes 授权动词 impersonate-via-spire。该审查以 broker SPIFFE ID 作为 SAR 用户名,且不设置 groups。经工作负载认证器的 Attest RPC 进行的、基于 PID 的认证,不会使用 broker 配置,也不会运行此审查。
对于 KubernetesObjectReference,该引用通过资源(<plural>.<group>,核心资源的 group 字符串为 core)以及其命名空间内名称(namespace + name)、其 uid、或两者兼有,来标识目标对象。Pod 引用会先尝试本地 kubelet 的 pod 列表。在默认的 pod_reference_scope = "agent_node" 下,pod 引用仅限于本地 kubelet 返回的信息,不会回退到 Kubernetes API server。设为 pod_reference_scope = "cluster" 时,pod 引用可回退到 Kubernetes API server,并解析任意节点上的 pod。非 pod 的对象引用通过 Kubernetes API server 解析。当 experimental.broker.access_policy = "enforced" 时,插件随后会为被引用对象创建同样的 SubjectAccessReview。该审查以 broker SPIFFE ID 作为 SAR 用户名、不设 groups,并使用该引用的资源 group 与 plural,以及解析出的 namespace 与 name。若授权方拒绝该审查,认证将以 PermissionDenied 失败。Kubernetes API server 查找要求 agent 的 ServiceAccount 对被引用资源具有权限。
Pod(pods/core)。 指向 pod 的 KubernetesObjectReference 会经由与基于 PID 的引用相同的 pod 解析路径进行认证,并生成与上表所记录相同的 pod 形态选择器(k8s:ns、k8s:sa、k8s:pod-name、k8s:container-name、k8s:pod-uid、k8s:pod-label、k8s:pod-image、k8s:pod-owner ……)。默认情况下,broker 的 pod 引用仅限于本地 kubelet 返回的 pod(agent_node)。若某 broker 必须通过 Kubernetes API server 引用其他节点上的 pod,请设置 pod_reference_scope = "cluster"。为基于 PID 的流程编写的注册条目,对两种引用类型都能继续匹配。
其他资源(agent 拥有权限的任意 <plural>.<group>)。 agent 通过 Kubernetes API server 获取对象的 metadata(使用 PartialObjectMetadata 请求),并生成一套与资源种类(kind)无关的统一词汇:
| 选择器 | 取值 |
|---|---|
| k8s:uid | 对象的 UID。 |
| k8s:resource | 该资源的 <plural>.<group>(如 deployments.apps、pods.core)。 |
| k8s:plural | 资源的复数名(如 deployments)。 |
| k8s:group | API group(如 apps);核心资源为 core。 |
| k8s:version | 发现到的版本(如 v1、v1beta1)。 |
| k8s:apiVersion | Kubernetes 线上(wire)形式:核心资源为 v1,否则为 <group>/<version>。 |
| k8s:kind | 对象种类(如 Deployment)。 |
| k8s:name | 对象名称。 |
| k8s:namespace | 对象命名空间;集群级(cluster-scoped)对象则省略。 |
| k8s:key | 命名空间内对象为 <namespace>/<name>,集群级对象则仅为 <name>。 |
| k8s:label | 对象上的一个标签,格式为 <key>:<value>(每个标签条目一个选择器)。 |
| k8s:owner-key | metadata.ownerReferences 中每个条目的 <group>/<Kind>/<name>。 |
| k8s:owner-uid | metadata.ownerReferences 中每个条目的 <group>/<Kind>/<uid>。 |
| k8s:controller-key | 同 k8s:owner-key,但仅对 Controller: true 的 owner 引用生成。 |
| k8s:controller-uid | 同 k8s:owner-uid,但仅对 Controller: true 的 owner 引用生成。 |
注解(annotation)有意不作为选择器暴露。Kubernetes 标签由 API server 校验并建立索引,是恰当的身份锚点;而注解是一个不受约束的元数据大杂烩(常为多行 JSON),不适合用于等值匹配的选择器。
agent 的 ServiceAccount 需要对它预期通过此路径解析的每种资源具有 get 权限(当引用仅以 uid 标识对象时还需要 list)。当 experimental.broker.access_policy = "enforced" 时,还需要对 subjectaccessreviews.authorization.k8s.io 具有 create 权限,以执行 broker 授权检查。例如:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: spire-agent-k8s-attestor
rules:
# 对象引用解析。加入 broker 可能引用的每一种资源。
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: ["kustomize.toolkit.fluxcd.io"]
resources: ["kustomizations"]
verbs: ["get", "list"]
# 仅当 experimental.broker.access_policy = "enforced" 时需要,以便 k8s 工作负载
# 认证器在处理 Broker API 引用请求时能创建 SubjectAccessReview 对象。
- apiGroups: ["authorization.k8s.io"]
resources: ["subjectaccessreviews"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: spire-agent-k8s-attestor
subjects:
- kind: ServiceAccount
name: spire-agent
namespace: spire
roleRef:
kind: ClusterRole
name: spire-agent-k8s-attestor
apiGroup: rbac.authorization.k8s.io当 experimental.broker.access_policy = "enforced" 时,Kubernetes 授权方还必须允许该 broker SPIFFE ID 对 broker 可能引用的资源使用 SPIRE 自定义的 impersonate-via-spire 动词。该 SubjectAccessReview 以 broker SPIFFE ID 作为 Kubernetes 用户名进行检查,且不设置 groups。broker pod 的 Kubernetes ServiceAccount 不会被用作被审查的主体。
SPIRE 有意不为此检查使用 Kubernetes 内置的 impersonate 动词。内置动词在 Kubernetes RBAC 中含义更广:它可以授权对用户、组、ServiceAccount 及其他冒充目标的原生冒充(impersonation)。把该内置动词授予 broker 相关的 Kubernetes 身份,会创建出权限超出 SPIRE broker 授权决策范围的、能力强大的 RBAC 主体。impersonate-via-spire 动词是一个仅由此 SubjectAccessReview 检查的、SPIRE 专属的授权闸门;授予它可让 Kubernetes 回答 SPIRE 的问题,而不授予 Kubernetes 原生的冒充能力。
例如,对于 broker ID spiffe://example.org/broker:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: spire-broker-impersonation
rules:
# 这是由 SubjectAccessReview 强制执行的 broker 授权决策。
# 使用 SPIRE 的自定义动词,而非 Kubernetes 内置的 `impersonate` 动词。
- apiGroups: [""]
resources: ["pods"]
verbs: ["impersonate-via-spire"]
- apiGroups: ["kustomize.toolkit.fluxcd.io"]
resources: ["kustomizations"]
verbs: ["impersonate-via-spire"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: spire-broker-impersonation
subjects:
- kind: User
name: spiffe://example.org/broker
roleRef:
kind: ClusterRole
name: spire-broker-impersonation
apiGroup: rbac.authorization.k8s.io当 broker 只应被允许对特定命名空间内的命名空间级资源授权 SPIRE broker 引用时,可改用 Role 和 RoleBinding。
镜像选择器的局限性
container-image、pod-image 和 pod-init-image 选择器派生自 Kubernetes ContainerStatus 中的两个字段:Image(通常是基于标签的名称,如 myimage:v1.2.3)与 ImageID(通常是基于摘要的标识,如 myimage@sha256:abc...)。这两个值都会为每个容器生成选择器,以支持按任一形式匹配。
基于标签的镜像名可能不一致。 Image 字段由容器运行时通过 CRI API 填充,而当单个镜像摘要关联多个标签时,CRI 并未规定应返回哪个镜像名。例如,若 myimage:latest 和 myimage:v1.2.3 都指向同一 SHA256 摘要,容器运行时在 Image 字段中可能返回其中任一标签,且该选择在不同节点间或随时间可能变化。这已在多个 CRI 实现中被观察到(如 cri-dockerd)。
因此,依赖基于标签的镜像选择器的注册条目,可能产生不一致的认证结果。若两个标签在某节点上解析到同一摘要,以 myimage:v1.2.3 部署的工作负载可能会被以 container-image:myimage:latest 选择器认证(反之亦然)。
建议: 为使工作负载注册可靠,选择器中应优先使用基于摘要的镜像标识(ImageID 形式),而非基于标签的名称。例如,使用:
k8s:container-image:docker.io/envoyproxy/envoy-alpine@sha256:bf862e5f5eca0a73e7e538224578c5cf867ce2be91b5eaed22afc153c00363eb而不是:
k8s:container-image:docker.io/envoyproxy/envoy-alpine:v1.16.0基于标签的选择器在便于人类阅读、以及摘要尚未可知的动态工作负载注册场景(如镜像拉取尚未发起时)中仍然有用。不过,运维者应了解可读性与唯一性之间的权衡,并在多个标签可能引用同一镜像摘要的环境中避免依赖基于标签的镜像选择器。更多细节见 #4287。
示例
使用 kubelet 只读端口:
WorkloadAttestor "k8s" {
plugin_data {
kubelet_read_only_port = 10255
}
}使用安全 kubelet 端口,通过 /run/secrets/kubernetes.io/serviceaccount/ca.crt 验证,并使用默认的 service account token 认证:
WorkloadAttestor "k8s" {
plugin_data {
}
}使用安全 kubelet 端口,跳过验证,并使用默认的 service account token 认证:
WorkloadAttestor "k8s" {
plugin_data {
skip_kubelet_verification = true
}
}使用安全 kubelet 端口,跳过验证,并使用其他某个 token 认证:
WorkloadAttestor "k8s" {
plugin_data {
skip_kubelet_verification = true
token_path = "/path/to/token"
}
}使用安全 kubelet 端口,验证 kubelet 证书,并使用 X509 客户端证书认证:
WorkloadAttestor "k8s" {
plugin_data {
kubelet_ca_path = "/path/to/kubelet-ca.pem"
certificate_path = "/path/to/cert.pem"
private_key_path = "/path/to/key.pem"
}
}启用对象引用查找所用的 Kubernetes API server 缓存:
WorkloadAttestor "k8s" {
plugin_data {
experimental {
api_server {
cache {
enabled = true
}
}
}
}
}平台支持
本插件仅在 Unix 系统上受支持。
已知问题
对于使用生命周期钩子(lifecycle hook)改变 pod 启动行为的 pod,本插件可能无法正确认证其中的工作负载。这包括将
holdApplicationUntilProxyStarts配置项设为 true 的 Istio 工作负载。更多信息请见 #3092。在这种情况下,可使用disable_container_selectors配置项成功认证工作负载,尽管选择器粒度会降低(即仅有 pod 选择器)。当多个镜像标签引用同一摘要时,基于标签的镜像选择器(
container-image、pod-image、pod-init-image)可能产生不一致的认证结果。容器运行时可能非确定性地报告其中任一关联标签,这会导致工作负载在某些节点上认证失败。请使用基于摘要的镜像标识以获得可靠匹配。参见镜像选择器的局限性一节以及 #4287。