Kubernetes Pod 作为云身份:EKS IRSA 与 GKE Workload Identity Federation 对比
Kubernetes ServiceAccount 如何在 AWS 和 Google Cloud 上成为真正的云身份 - EKS IRSA 和 GKE Workload Identity Federation 背后的 OIDC 联合信任链,一对一映射,并指出那些打破你肌肉记忆的细节。
AWS 和 Google Cloud 都以相同的方式解决了相同的问题:通过将其 Kubernetes ServiceAccount 转换为联合云身份,Pod 可以 以自身身份 认证到云 API,使用短期令牌且不存储任何秘密。在 AWS 上,此功能是 IRSA (IAM Roles for Service Accounts);在 Google Cloud 上,它是 Workload Identity Federation for GKE(2024 年从“Workload Identity”更名)。从底层来看,两者都是相同的 OIDC 联合技巧 - 但 GKE 隐藏了更多的底层实现细节,这改变了你如何连接、调试和理解它。
如果你只读一段话,简而言之就是:集群为每个 Pod 的 ServiceAccount 签署一个短期令牌,云服务将该令牌交换为限定于一个身份的真实凭据。 在 AWS 上,你为每个集群注册一个 IAM OIDC 提供商,创建一个 IAM 角色,并用信任策略对其进行限制;Pod 内的 SDK 调用 AssumeRoleWithWebIdentity。在 GKE 上,你 无需 注册任何东西 - 每个项目都有一个永久的、由 Google 管理的工作负载身份池,名为 PROJECT_ID.svc.id.goog,每个 ServiceAccount 都会自动成为其中的主体,并且节点本地的元数据服务器会执行令牌交换,因此你的应用程序只需使用 Application Default Credentials,就像它运行在普通虚拟机上一样。有三点差异足以让你犯错:GKE 需要两个启用开关(集群 和 节点池),GKE 可以在 完全没有云身份对象 的情况下授予 Kubernetes ServiceAccount 云权限,并且 - 一如既往 - 拉取容器镜像的身份与你的工作负载身份是 不同 的。
两个云服务都解决的问题
在联合之前,授予 Pod 云权限意味着糟糕的选择:将静态密钥烘焙到镜像或 Secret 中(泄露、轮换痛苦),或者将权限授予整个节点,以便其上 每个 Pod 都继承它(无隔离,权限范围过大)。AWS 方面的权宜之计是像 kube2iam 和 kiam 这样的节点 IMDS 拦截器;GCP 方面是将节点服务账号密钥挂载到 Pod 中。所有这些方法都繁琐、范围过广且容易出现竞态条件。
联合用加密信任取代了这些。集群投射一个签名过的、短期有效的 OIDC 令牌,其范围限定于 Pod 的 ServiceAccount。云服务根据集群发布的密钥验证该令牌,如果令牌的身份与你配置的规则匹配,则返回恰好一个云身份的凭据。不存储任何秘密;令牌自动轮换(默认生命周期约一小时),并且在集群外部无用。
共享机制:OIDC 联合
无论在哪种云上,每个工作负载身份设置都是相同的信任三角:
- 集群是 OIDC 颁发者。 它公开一个发现文档和一个 JWKS 端点,其中包含用于签署 ServiceAccount 令牌的公钥。该颁发者是信任锚点。
- 云服务信任该颁发者 用于特定的 ServiceAccount 身份。AWS 按角色表达信任;GCP 为每个项目永久表达一次信任,并由 Google 为你管理。
- 工作负载将投射的令牌交换为云凭据 并接收单个云身份的短期凭据。
其他一切都是命名 - 以及你必须自己构建多少步骤 2。这就是两个云服务差异最大的地方。
一对一映射
- Kubernetes 侧。 在 AWS 上,你使用
eks.amazonaws.com/role-arn: <role arn>注解 ServiceAccount。在 GKE 中,在现代 直接 模型下,你根本不需要在 ServiceAccount 上添加任何注解 - 你只需将其作为 IAM 主体引用。(较旧的模拟模型确实使用注解;详见下文。) - “OIDC 提供商”。 在 AWS 上,这是 EKS 集群的 OIDC 颁发者,你需要在 IAM 中将其注册为 OIDC 身份提供商一次,以便 IAM 信任它签名的令牌。在 GKE 上,它是项目的 工作负载身份池
PROJECT_ID.svc.id.goog,由 Google 自动创建和管理;你无需注册它,项目中每个集群中的每个 ServiceAccount 都已是其中的主体。 - 身份。 AWS:一个 IAM 角色。GKE 直接 模型:没有专用的身份对象 - Kubernetes ServiceAccount 本身就是主体。GKE 模拟 模型:一个 KSA 模拟的 Google 服务账号。
- 信任规则。 AWS:IAM 角色的 信任策略 绑定
sub = system:serviceaccount:<ns>:<sa>和aud = sts.amazonaws.com。GKE 直接:没有单独的信任文档 - 你直接将角色授予一个编码了命名空间和 ServiceAccount 的 主体标识符。GKE 模拟:你授予 KSA 在 Google 服务账号上的roles/iam.workloadIdentityUser角色。 - 权限。 AWS:附加到角色的 IAM 策略。GKE:目标资源(或项目)上的 IAM 允许策略角色绑定 -
roles/storage.objectViewer、roles/secretmanager.secretAccessor等。这是更深层次的区别:AWS 将策略附加到身份;GCP 在资源范围内授予角色。 - 交换。 AWS:Pod 内的 SDK 使用投射的令牌调用
sts:AssumeRoleWithWebIdentity。GKE:节点的元数据服务器 透明地执行交换,你的应用程序使用 Application Default Credentials,完全不需要交换代码。
本文的其余部分将端到端地介绍每个云服务,然后详细说明那些实际会让人犯错的差异。
演练:AWS EKS IRSA,端到端
各个组成部分,按信任流动的顺序:
1. 集群 OIDC 颁发者, 位于 https://oidc.eks.<region>.amazonaws.com/id/<hash> 这样的 URL。你需要在 IAM 中将其注册为 OIDC 身份提供商一次,以便 IAM 信任它签名的令牌。
2. IAM 角色及其信任策略。 信任策略使该角色只能由一个 ServiceAccount 承担,不能由其他任何实体承担:
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::<account>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<hash>" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.<region>.amazonaws.com/id/<hash>:sub": "system:serviceaccount:apps:checkout",
"oidc.eks.<region>.amazonaws.com/id/<hash>:aud": "sts.amazonaws.com"
}
}
}
同时绑定 :sub 和 :aud - 如果只绑定颁发者,将允许集群中 任何 ServiceAccount 承担该角色。
3. 附加到角色的 IAM 策略 授予应用程序所需的权限(例如对一个存储桶的 s3:GetObject 等)。
4. ServiceAccount 包含一个注解:
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account>:role/checkout
5. 投射。 EKS 内置了 Pod Identity Webhook。当 Pod 使用带注解的 ServiceAccount 时,webhook 会注入 AWS_ROLE_ARN 和 AWS_WEB_IDENTITY_TOKEN_FILE,并挂载一个投射令牌(受众 sts.amazonaws.com,自动轮换)。
6. 交换。 AWS SDK 注意到 AWS_WEB_IDENTITY_TOKEN_FILE,调用 sts:AssumeRoleWithWebIdentity,并缓存返回的短期凭据。你的代码只需 boto3.client("s3")。
新的替代方案:EKS Pod Identity (2023) 通过集群内代理和关联 API 完成相同的工作,而不是通过每个集群的 IAM OIDC 提供商和每个角色的信任策略 - 值得注意的是,这使其感觉更像 GKE 的托管模型。IRSA 和 Pod Identity 在生产环境中并存;IRSA 是与 OIDC 联合框架清晰映射的那个,因此在这里你需要记住它。
演练:GKE Workload Identity Federation,端到端
相同的信任流,需要构建的少得多:
1. 两个启用开关。 在 集群 上启用工作负载身份池,并在 节点池 上启用元数据服务器:
gcloud container clusters update <cluster> --workload-pool=<project-id>.svc.id.goog
gcloud container node-pools update <pool> --cluster=<cluster> --workload-metadata=GKE_METADATA
两者都是必需的。集群标志将集群加入项目的池中;节点池标志部署 GKE 元数据服务器(每个节点上的 DaemonSet),它将拦截凭据请求。没有 GKE_METADATA 的节点池上的 Pod 将获得节点的身份,而不是其自身的身份 - 这是一种静默的、范围过大的回退。
2. 池已存在。 <project-id>.svc.id.goog 会为项目自动创建;你无需注册 OIDC 提供商,也没有每个账号的提供商限制。项目中每个集群中的每个 ServiceAccount 都已是主体。
3a. 直接访问(现代默认)。 直接将角色授予 ServiceAccount,将其作为 IAM 主体。无需 Google 服务账号,无需注解:
gcloud storage buckets add-iam-policy-binding gs://<bucket> \
--role=roles/storage.objectViewer \
--member="principal://iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<project-id>.svc.id.goog/subject/ns/apps/sa/checkout"
主体路径编码了 ns/apps/sa/checkout - 命名空间和 ServiceAccount 是信任规则;无需编写单独的信任文档。
3b. 模拟(旧模型,少数服务仍需要)。 创建一个 Google 服务账号,让 KSA 模拟它,并注解 KSA:
gcloud iam service-accounts add-iam-policy-binding checkout@<project-id>.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:<project-id>.svc.id.goog[apps/checkout]"
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
iam.gke.io/gcp-service-account: checkout@<project-id>.iam.gserviceaccount.com
在这里,Google 服务账号持有权限(通过正常的角色绑定),KSA 借用这些权限。
4. 交换。 当你的应用程序请求凭据时,GKE 元数据服务器 会拦截对 http://metadata.google.internal 的请求,从 Kubernetes API 为该 Pod 获取一个 ServiceAccount JWT,通过 Security Token Service 将其交换为短期联合访问令牌(默认 1 小时,在过期前主动刷新),并返回。你的应用程序使用 Application Default Credentials 和标准的 Google Cloud 客户端库 - 它的行为就像运行在 GCE 虚拟机上一样。没有 webhook 注入环境变量,没有 AZURE_* 风格的变量,你的代码中也没有交换调用:在 Python 中,Cloud Storage 读取只是 storage.Client()。
主体是关键(在两个云上)
在两个云上都必须精确匹配的值是 ServiceAccount 身份:
- AWS 在角色的信任策略中绑定令牌主题
system:serviceaccount:<namespace>:<serviceaccount>。 - GKE direct 在主体路径
.../subject/ns/<namespace>/sa/<serviceaccount>中编码相同的配对。 - GKE impersonation 在成员
serviceAccount:<project-id>.svc.id.goog[<namespace>/<serviceaccount>]中编码它。
在任一云上,最常见的“它以无人身份认证”失败是这里的 Mismatch:Chart 部署到了不同的命名空间,ServiceAccount 获得了发布名称而不是应用程序名称,或者 Pod 回退到了 default ServiceAccount。当联合静默失败时,首先检查命名空间和 ServiceAccount 名称。
实际会让你犯错的差异
1. GKE 为你注册“OIDC 提供商” - 每个项目一次,永久有效。 在 EKS 上,你为每个集群注册一个 IAM OIDC 提供商,在大规模部署时可能会达到每个 AWS 账号 100 个 OIDC 提供商的软限制,需要进行规避。在 GKE 上,每个项目只有一个池 (PROJECT_ID.svc.id.goog),由 Google 创建和管理,由项目中的每个集群共享。无需注册,也没有上限。另一方面:信任默认是项目范围的,因此你通过 IAM 绑定(你授予哪个命名空间和 ServiceAccount)来限定范围,而不是通过划分提供商。
2. GKE 在节点本地元数据服务器上交换令牌,而不是通过 Pod 内的 SDK 调用。 IRSA 会修改 Pod(webhook 注入环境变量和投射令牌),并且 SDK 调用 STS。GKE 拦截节点上的 metadata.google.internal 请求并透明地返回凭据,因此应用程序使用 Application Default Credentials,就像在普通虚拟机上一样。这带来两个后果:你的应用程序 不需要 任何云特定的凭据代码,并且节点池 必须 启用元数据服务器 (GKE_METADATA),否则 Pod 会静默地获得节点的身份而不是其自身的身份。
3. GKE 可以在没有云身份对象的情况下授予 ServiceAccount 云权限。 在 IRSA 中,总会有一个 IAM 角色。在 GKE 的直接模型中,除了角色绑定本身之外,云端 无需 创建任何东西 - Kubernetes ServiceAccount 就是 主体 (principal://.../subject/ns/NS/sa/SA)。较旧的模拟模型确实引入了 Google 服务账号和 iam.gke.io/gcp-service-account 注解,并且对于少数不直接接受联合主体的服务,你仍然需要它 - 但首先应尝试直接绑定。
4. GKE 需要两个开关,就像 Azure 一样。 集群 (--workload-pool) 加上节点池 (--workload-metadata=GKE_METADATA)。如果缺少节点池部分,不会有故障,只会得到错误的(节点)身份。IRSA 将投射功能集成到 EKS 本身,因此没有等效的第二个开关。
5. 拉取镜像的身份与工作负载身份不同 - 在两个云上都是如此。 镜像拉取是 节点 身份的工作:在 GKE 上,节点池的 Google 服务账号需要 roles/artifactregistry.reader 才能从 Artifact Registry 拉取;在 EKS 上,节点组的实例角色需要 AmazonEC2ContainerRegistryReadOnly(或者你使用拉取秘密)。工作负载身份仅用于 应用程序代码 调用云 API 的情况。一个从不与云 SDK 通信的纯 HTTP 服务根本不需要工作负载身份 - 并且授予工作负载身份 artifactregistry.reader 对镜像拉取没有任何作用,因为拉取发生在应用程序(及其联合令牌)运行之前。
6. 权限附加方式不同。 AWS 将 IAM 策略附加到角色 - 权限随身份一起移动。GCP 在 目标资源 的范围(或项目)上授予 IAM 角色绑定 - 授权存在于被访问的对象上,而不是身份上。最终状态相同,但审计位置不同:在 AWS 上读取角色的附加策略;在 GCP 上列出资源的 IAM 策略(或使用 Policy Analyzer 查看主体可以访问的内容)。
应用程序代码的作用
两个云都以“无秘密,无凭据代码”为终点,但它们实现的方式不同:
- AWS: webhook 设置
AWS_ROLE_ARN和AWS_WEB_IDENTITY_TOKEN_FILE;SDK 的默认链调用AssumeRoleWithWebIdentity。你的代码:boto3.client("s3")。 - GKE: 没有东西注入到 Pod 中;SDK 的 Application Default Credentials 链命中节点元数据服务器,由其执行交换。你的代码:
storage.Client()。
在这两种情况下,凭据都是短期有效的,并透明地刷新。无需轮换,无需存储,也无需担心泄露。
适用于两个云的调试清单
当 Pod“无法认证”时,按顺序检查信任链:
- 集群/项目联合是否开启? EKS:IAM OIDC 提供商已注册。GKE:集群具有
--workload-pool并且 节点池具有GKE_METADATA。在 GKE 上,缺少节点池开关是经典的静默失败 - Pod 获得的是节点身份,而不是其自身的身份。 - ServiceAccount 是否正确? Pod 实际使用的是你认为的 ServiceAccount(而不是
default)。进入 Pod 内部检查。 - 身份是否匹配? 比较信任规则与真实的命名空间和 ServiceAccount:AWS 上是
system:serviceaccount:<ns>:<sa>,GKE 上是.../subject/ns/<ns>/sa/<sa>(或[ns/sa])。这是两者最常见的失败。 - 交换是否正常工作? EKS:Pod 具有
AWS_WEB_IDENTITY_TOKEN_FILE。GKE:从 Pod 中执行 curl -H "Metadata-Flavor: Google" metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email 返回 预期 的身份。 - 权限是否足够? 仅在身份解析后:角色上的 IAM 策略 (AWS) 或目标资源上的角色绑定 (GCP)。此处为空结果是授权失败,而非认证失败 - 这表明联合本身正在工作。
哪些认证会深入探讨此主题
工作负载身份恰好位于 Kubernetes、云 IAM 和 OIDC 联合的交汇处,因此它出现在三个考试轨道中。如果你已经拥有一个云的凭据,那么在另一个云上理解相同的概念将是轻而易举的。
在 AWS 方面:
- AWS Certified Solutions Architect - Associate (SAA-C03) - IAM 角色、EKS 以及身份如何获取云权限。
- AWS Certified Security - Specialty (SCS-C03) - IAM 信任策略、OIDC 联合以及最小权限范围(正是 IRSA 信任链)。
在 Google Cloud 方面:
- Google Cloud Associate Cloud Engineer - GKE、IAM、服务账号和角色绑定。
- Google Cloud Professional Cloud Security Engineer - 工作负载身份、IAM 深度、服务账号安全和最小权限。
在 Kubernetes 方面:
- CNCF Certified Kubernetes Administrator (CKA) - ServiceAccounts、投射令牌和 Pod 规范。
- CNCF Certified Kubernetes Security Specialist (CKS) - ServiceAccount 令牌安全、最小权限和减少静态秘密暴露。
总结
IRSA 和 GKE Workload Identity Federation 是同一个概念,只是底层实现细节不同:集群为 ServiceAccount 签署一个短期 OIDC 令牌,云服务将其交换为限定于单个身份的凭据。AWS 让你自己组装信任 - 每个集群一个 OIDC 提供商、一个 IAM 角色、一个信任策略、一个由 SDK 交换的投射令牌。GKE 则为你提供一个托管的、项目范围的池,允许你直接将角色绑定到 Kubernetes ServiceAccount,完全不需要云身份对象,并在节点元数据服务器上执行交换,因此你的应用程序永远不会看到凭据。一旦理解了信任三角,转换就是机械的:角色变为主体绑定(或你模拟的服务账号),信任策略变为 IAM 成员字符串,附加策略变为资源上的角色绑定,而 AssumeRoleWithWebIdentity 变为不可见的元数据服务器交换。牢记三件事 - GKE 的两个开关、其无身份对象的直接模型,以及镜像拉取完全是另一个身份的事实 - 那么过去看起来随机的拒绝就会被理解为一种连贯、深思熟虑的设计。