自建Kubernetes与AWS IAM授权认证.md
为什么需要 IRSA?
在 Kubernetes 集群中,当 Pod 需要访问 AWS 资源时,一个常见的做法是创建具有相应权限的 IAM 用户,并将其凭证(Access Key ID 和 Secret Access Key)作为 Kubernetes Secret 存储。这种方法的主要问题是创建了长期有效的凭证。一旦这些凭证泄露,任何持有者都可以滥用它们,直到我们手动在 AWS 账户中吊销它们。
更安全的替代方案是使用 IAM 角色。IAM 角色允许 Kubernetes 服务获取临时 AWS 凭证,这些凭证在一段时间后会自动过期。这样,即使凭证泄露,它们很可能已经失效,从而大大降低了安全风险。
AWS 为此专门设计了 IAM Roles for Service Accounts (IRSA),它允许 Kubernetes 中的 Pod 拥有一个 AWS 身份。虽然 IRSA 是为 AWS 的托管 Kubernetes 服务 (EKS) 设计的,但它同样可以应用于自建的 Kubernetes 集群。
工作原理
下图展示了 Pod 使用 IRSA 进行身份验证的流程:
sequenceDiagram participant Pod participant K8s API participant Pod Identity Webhook participant AWS STS participant OIDC Discovery (S3) Pod->>K8s API: Pod Creation with annotated Service Account K8s API->>Pod Identity Webhook: Mutate Pod Pod Identity Webhook-->>K8s API: Injects AWS_ROLE_ARN & Token Path K8s API-->>Pod: Pod is scheduled and runs Pod->>AWS STS: AssumeRoleWithWebIdentity(SA Token) AWS STS->>OIDC Discovery (S3): Get JWKS (Public Keys) OIDC Discovery (S3)-->>AWS STS: Return Public Keys AWS STS->>AWS STS: Validate SA Token Signature AWS STS-->>Pod: Return Temporary IAM CredentialssequenceDiagram participant Pod participant K8s API participant Pod Identity Webhook participant AWS STS participant OIDC Discovery (S3) Pod->>K8s API: Pod Creation with annotated Service Account K8s API->>Pod Identity Webhook: Mutate Pod Pod Identity Webhook-->>K8s API: Injects AWS_ROLE_ARN & Token Path K8s API-->>Pod: Pod is scheduled and runs Pod->>AWS STS: AssumeRoleWithWebIdentity(SA Token) AWS STS->>OIDC Discovery (S3): Get JWKS (Public Keys) OIDC Discovery (S3)-->>AWS STS: Return Public Keys AWS STS->>AWS STS: Validate SA Token Signature AWS STS-->>Pod: Return Temporary IAM Credentials
- Pod 创建: 当一个 Pod 被创建时,如果其关联的 ServiceAccount (SA) 带有特定的注解(指定要扮演的 IAM 角色),Kubernetes API Server 会调用
pod-identity-webhook
。 - Webhook 修改 Pod: 这个 mutating webhook 会向 Pod 中注入两个环境变量:
AWS_ROLE_ARN
和AWS_WEB_IDENTITY_TOKEN_FILE
。 - Pod 调用 AWS STS: Pod 中的应用程序(使用 AWS SDK)利用其 Service Account Token 调用 AWS STS 的
AssumeRoleWithWebIdentity
API,请求扮演AWS_ROLE_ARN
指定的角色。 - STS 验证 Token: AWS STS 收到请求后,需要验证 Service Account Token 的有效性。它会访问预先注册的 OIDC Discovery 端点以获取公钥。
- Discovery 端点返回公钥: OIDC Discovery 端点(通常是一个公开的 S3 存储桶)返回用于签署 Service Account Token 的 JWKS (JSON Web Key Set)。
- STS 完成验证: STS 使用从 Discovery 端点获取的公钥来验证 Pod 的 Service Account Token 的签名。
- 返回临时凭证: 如果 Token 有效,STS 会向 Pod 返回所请求角色的临时凭证。Pod 现在可以使用这些凭证安全地访问授权的 AWS 资源。
详细执行步骤
准备工作
在开始之前,请确保您的环境中已安装以下工具:
aws-cli
: 用于与 AWS 交互。kubectl
: 用于与 Kubernetes 集群交互。openssl
: 用于处理证书。ssh-keygen
: 用于生成密钥对。jq
: 用于处理 JSON。- 一个正在运行的自建 Kubernetes 集群,并且您拥有其
kube-apiserver
的配置权限。
步骤 1: 生成 OIDC Discovery 相关内容
这部分的目标是生成 OIDC Discovery 所需的 JSON 文件和用于签发 SA Token 的密钥。
生成 RSA 密钥对
这对密钥将用于签署 Kubernetes Service Account Token。
1 2 3
mkdir -p keys ssh-keygen -t rsa -b 2048 -f keys/oidc-issuer.key -m pem -N "" ssh-keygen -e -m PKCS8 -f keys/oidc-issuer.key.pub > keys/oidc-issuer.pub
生成
keys.json
(JWKS)keys.json
包含用于验证签名的公钥。您需要一个辅助工具来从公钥生成 JWKS 文件。1 2 3 4 5
# 克隆包含生成工具的仓库 git clone https://github.com/fanicia/kind-aws-irsa-example.git # 使用 Go 程序生成 keys.json go run -C kind-aws-irsa-example/keys-generator main.go -key "$PWD/keys/oidc-issuer.pub" | jq > keys.json
步骤 2: 托管 OIDC Discovery 端点
您需要一个公开可访问的 HTTPS 端点来托管上一步生成的 keys.json
和一个 OIDC 配置文件。这里提供两种方案。
方案 A: 使用 S3 公开存储桶 (简单快速)
此方案直接利用一个公开的 S3 存储桶作为 OIDC 端点,配置简单,适合快速验证和测试环境。
设置环境变量
1 2 3 4 5 6 7 8
# 可自定义后缀以避免命名冲突 SUFFIX="irsa-demo" # 替换为您的 AWS 区域 AWS_DEFAULT_REGION="us-west-2" DISCOVERY_BUCKET="aws-irsa-oidc-discovery-$SUFFIX" ISSUER_HOSTPATH="s3-$AWS_DEFAULT_REGION.amazonaws.com/$DISCOVERY_BUCKET" ISSUER_URL="https://$ISSUER_HOSTPATH"
创建 S3 存储桶并设置为公开只读
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# 创建存储桶 aws s3api create-bucket \ --bucket $DISCOVERY_BUCKET \ --region $AWS_DEFAULT_REGION \ --create-bucket-configuration LocationConstraint=$AWS_DEFAULT_REGION # 允许公开访问 aws s3api put-public-access-block \ --bucket $DISCOVERY_BUCKET \ --public-access-block-configuration "BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false" # 设置只读策略 cat <<EOF > s3-readonly-policy.json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": "*", "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::$DISCOVERY_BUCKET/*"] } ] } EOF aws s3api put-bucket-policy \ --bucket $DISCOVERY_BUCKET \ --policy file://s3-readonly-policy.json
生成并上传 OIDC Discovery 文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# 创建 discovery.json cat <<EOF > discovery.json { "issuer": "$ISSUER_URL", "jwks_uri": "$ISSUER_URL/keys.json", "authorization_endpoint": "urn:kubernetes:programmatic_authorization", "response_types_supported": ["id_token"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"], "claims_supported": ["sub", "iss"] } EOF # 上传文件到 S3 aws s3 cp ./discovery.json s3://$DISCOVERY_BUCKET/.well-known/openid-configuration aws s3 cp ./keys.json s3://$DISCOVERY_BUCKET/keys.json
方案 B: 使用 Cloudflare CDN + 私有 S3 存储桶 (生产推荐)
此方案使用 Cloudflare 作为 CDN,将 S3 作为源站。S3 存储桶本身保持私有,提高了安全性,同时利用 CDN 获得更好的性能和自定义域名。
设置环境变量
1 2 3 4 5 6 7 8 9 10 11 12
# 替换为您的域名和自定义前缀 YOUR_DOMAIN="your-domain.com" OIDC_SUBDOMAIN="oidc-irsa" # 可自定义后缀以避免命名冲突 SUFFIX="irsa-prod" # 替换为您的 AWS 区域 AWS_DEFAULT_REGION="us-west-2" DISCOVERY_BUCKET="aws-irsa-oidc-private-$SUFFIX" ISSUER_HOSTPATH="$OIDC_SUBDOMAIN.$YOUR_DOMAIN" ISSUER_URL="https://$ISSUER_HOSTPATH"
创建私有 S3 存储桶
注意:这次我们不设置任何公开访问策略。
1 2 3 4
aws s3api create-bucket \ --bucket $DISCOVERY_BUCKET \ --region $AWS_DEFAULT_REGION \ --create-bucket-configuration LocationConstraint=$AWS_DEFAULT_REGION
生成并上传 OIDC Discovery 文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# 创建 discovery.json cat <<EOF > discovery.json { "issuer": "$ISSUER_URL", "jwks_uri": "$ISSUER_URL/keys.json", "authorization_endpoint": "urn:kubernetes:programmatic_authorization", "response_types_supported": ["id_token"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"], "claims_supported": ["sub", "iss"] } EOF # 上传文件到 S3 aws s3 cp ./discovery.json s3://$DISCOVERY_BUCKET/.well-known/openid-configuration aws s3 cp ./keys.json s3://$DISCOVERY_BUCKET/keys.json
配置 Cloudflare
登录您的 Cloudflare 控制台:
a. DNS: 添加一个 CNAME 记录,将
$OIDC_SUBDOMAIN
指向您的 Cloudflare CNAME 目标(通常在 Cloudflare 的 DNS 页面会有提示)。 b. SSL/TLS: 确保 SSL/TLS 加密模式为Full (Strict)
,以保证 Cloudflare 到源站(S3)的流量也是加密的。 c. Origin Rules: 创建一个 Origin Rule,将发往$ISSUER_HOSTPATH
的请求重写到您的 S3 存储桶的静态网站端点。 - Field:Hostname
- Operator:equals
- Value:$ISSUER_HOSTPATH
- Then…: - Rewrite to:s3.amazonaws.com
- Path:/
d. Permissions: 在 S3 存储桶策略中,授权 Cloudflare 的 IP 范围可以访问。或者更简单地,使用 Cloudflare R2(它原生集成了公共访问)。重要: 当您轮换密钥并更新
keys.json
后,必须登录 Cloudflare 控制台清除 CDN 缓存,以确保 AWS STS 能获取到最新的公钥。
步骤 3: 创建 AWS IAM OIDC Provider
这一步是告诉 AWS IAM 您的 OIDC Provider 的存在。根据您选择的方案,获取指纹的命令有所不同。
获取服务器证书指纹 (
CA_THUMBPRINT
)对于方案 A (S3):
1
CA_THUMBPRINT=$(openssl s_client -connect s3-$AWS_DEFAULT_REGION.amazonaws.com:443 -servername s3-$AWS_DEFAULT_REGION.amazonaws.com -showcerts </dev/null 2>/dev/null | openssl x509 -in /dev/stdin -sha1 -noout -fingerprint | cut -d \'=\' -f 2 | tr -d \':\')
对于方案 B (Cloudflare):
1
CA_THUMBPRINT=$(openssl s_client -connect $ISSUER_HOSTPATH:443 -servername $ISSUER_HOSTPATH -showcerts </dev/null 2>/dev/null | openssl x509 -in /dev/stdin -sha1 -noout -fingerprint | cut -d \'=\' -f 2 | tr -d \':\')
创建 OIDC Identity Provider
确保您已根据所选方案正确设置了
ISSUER_URL
环境变量。1 2 3 4
aws iam create-open-id-connect-provider \ --url $ISSUER_URL \ --thumbprint-list $CA_THUMBPRINT \ --client-id-list sts.amazonaws.com
步骤 4: 配置自建 Kubernetes 集群
警告: 修改 API Server 参数可能会使集群中现有的 Service Account Token 失效。请在维护窗口操作,并做好备份。
您需要修改 kube-apiserver
的启动参数,添加以下标志:
|
|
- 确保
kube-apiserver
容器可以访问您在步骤 1 中生成的oidc-issuer.key
和oidc-issuer.pub
文件。您可能需要将它们作为 Secret 挂载或直接放在 Master 节点的某个路径下。 - 重启
kube-apiserver
使配置生效。
步骤 4: 部署 Pod Identity Webhook
此 Webhook 会自动为带有特定注解的 Service Account 的 Pod 注入 AWS 相关的环境变量。
安装 cert-manager (如果您的集群中还没有)
cert-manager
用于为 webhook 自动签发和管理 TLS 证书。1
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.6/cert-manager.yaml
等待
cert-manager
的 Pod 完全启动并运行。部署 Webhook
您可以从 AWS 官方仓库获取部署文件:
https://github.com/aws/amazon-eks-pod-identity-webhook
。1 2 3 4 5 6
# 克隆仓库或下载 deploy/ 目录下的 yaml 文件 git clone https://github.com/aws/amazon-eks-pod-identity-webhook.git cd amazon-eks-pod-identity-webhook/deploy # 部署相关资源 kubectl apply -f ./
这会创建
ServiceAccount
,ClusterRole
,ClusterRoleBinding
,Service
,Deployment
以及MutatingWebhookConfiguration
。
步骤 5: 验证 IRSA 配置
现在,我们将创建一个示例应用来验证整个流程是否正常工作。
创建 IAM 角色和信任策略
此角色将被我们的示例 Pod 所扮演。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) PROVIDER_ARN="arn:aws:iam::$ACCOUNT_ID:oidc-provider/$ISSUER_HOSTPATH" DEMO_ROLE_NAME="s3-echoer-$SUFFIX" DEMO_NAMESPACE="default" DEMO_SA="s3-echoer-sa" # 创建信任策略文件 cat <<EOF > trust-policy.json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "$PROVIDER_ARN" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "$ISSUER_HOSTPATH:sub": "system:serviceaccount:$DEMO_NAMESPACE:$DEMO_SA" } } } ] } EOF # 创建 IAM 角色 aws iam create-role \ --role-name $DEMO_ROLE_NAME \ --assume-role-policy-document file://trust-policy.json # 为角色附加权限(例如,S3 访问权限) aws iam attach-role-policy \ --role-name $DEMO_ROLE_NAME \ --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess # 获取角色 ARN S3_ROLE_ARN=$(aws iam get-role --role-name $DEMO_ROLE_NAME --query Role.Arn --output text) echo "Demo Role ARN: $S3_ROLE_ARN"
创建 Kubernetes 资源
- 创建一个用于测试的 S3 存储桶。
- 创建一个 Service Account 并使用角色 ARN 对其进行注解。
- 创建一个 Job,该 Job 使用此 Service Account 访问 S3。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
# 创建测试用的 S3 存储桶 TARGET_BUCKET="output-bucket-$DEMO_ROLE_NAME" aws s3api create-bucket \ --bucket $TARGET_BUCKET \ --region $AWS_DEFAULT_REGION \ --create-bucket-configuration LocationConstraint=$AWS_DEFAULT_REGION # 创建 Service Account kubectl create sa $DEMO_SA -n $DEMO_NAMESPACE # 注解 Service Account kubectl annotate sa $DEMO_SA -n $DEMO_NAMESPACE eks.amazonaws.com/role-arn=$S3_ROLE_ARN # 创建测试 Job cat <<EOF | kubectl apply -f - apiVersion: batch/v1 kind: Job metadata: name: s3-echoer-job namespace: $DEMO_NAMESPACE spec: template: spec: serviceAccountName: $DEMO_SA containers: - name: main image: "amazon/aws-cli" command: - "sh" - "-c" - | time=\$(date +%s) echo "IRSA test file at \$time" > test.txt aws s3 cp test.txt s3://$TARGET_BUCKET/test-\$time.txt echo "File uploaded to s3://$TARGET_BUCKET/test-\$time.txt" restartPolicy: Never backoffLimit: 1 EOF
检查结果
- 查看 Job 的 Pod 日志,确认文件是否上传成功。
- 在 AWS S3 控制台或使用 AWS CLI 检查目标存储桶中是否存在新文件。
1 2 3 4 5 6 7 8
# 等待 Job 完成 kubectl wait --for=condition=complete job/s3-echoer-job -n $DEMO_NAMESPACE --timeout=120s # 查看 Pod 日志 kubectl logs -n $DEMO_NAMESPACE -l job-name=s3-echoer-job # 检查 S3 存储桶 aws s3 ls s3://$TARGET_BUCKET/
如果一切顺利,您应该能在日志中看到上传成功的消息,并在 S3 存储桶中找到测试文件。这证明您的自建 Kubernetes 集群已成功配置 IRSA。
深入探讨与常见问题
Q1: CNI 等核心组件如何获得初始权限?—— “鸡和蛋”的引导难题
一个核心问题是:像 AWS CNI 这样的网络插件,它本身也需要 IAM 权限才能为 Pod 分配 IP。在 pod-identity-webhook
启动之前(而 Webhook 本身也需要网络才能运行),CNI 插件是如何获得授权的呢?
这其实是一个典型的“先有鸡还是先有蛋”的引导难题。答案是:核心组件(如 CNI)的初始权限必须由节点的 IAM 实例配置文件(IAM Instance Profile)提供。
引导流程解析
节点 IAM 角色是基础:您的 EC2 Worker 节点必须附加一个 IAM 实例配置文件,该文件授予了 AWS CNI 运行所需的最低权限(例如
ec2:CreateNetworkInterface
,ec2:DescribeNetworkInterfaces
等)。CNI 继承节点权限:当集群启动时,
aws-node
Pod(AWS CNI 的实现)作为 DaemonSet 在每个节点上运行。此时,由于 IRSA 尚未就绪,它会直接继承所在 EC2 节点的 IAM 角色权限。网络就绪:CNI Pod 利用从节点继承的权限成功连接到 AWS API,为集群建立起 Pod 网络。
Webhook 启动:一旦 Pod 网络可用,
pod-identity-webhook
和其他集群组件(如 CoreDNS)才能被成功调度和启动。IRSA 生效:只有当
pod-identity-webhook
正常运行后,IRSA 才能开始为其他应用程序提供细粒度的 IAM 角色。
结论:IRSA 并非用于授权所有 Pod。对于像 CNI 这样需要在集群引导阶段就必须工作的“系统级”组件,它们的权限必须由更底层的节点 IAM 实例配置文件来保障。IRSA 则专注于为运行在正常网络之上的“用户级”应用提供更安全、动态的权限管理。
Q2: 节点 IAM 角色是否存在安全风险?如何防范?
既然 CNI 依赖节点 IAM 角色,那么节点上的任何 Pod 不就能通过访问 EC2 元数据接口(169.254.169.254
)来“窃取”这个角色的权限吗?
这个安全顾虑完全正确!如果不加防护,这就是一个严重的安全漏洞。幸运的是,我们有成熟的、多层防御的方案来解决这个问题。
第 1 层:遵循最小权限原则
附加给节点的 IAM 角色应严格遵循最小权限原则。它只应包含 kubelet
和 aws-cni
运行所必需的权限,例如:
ec2:CreateNetworkInterface
ec2:AttachNetworkInterface
ec2:DeleteNetworkInterface
ec2:DescribeNetworkInterfaces
ec2:DescribeInstances
ecr:GetAuthorizationToken
ecr:BatchCheckLayerAvailability
ecr:GetDownloadUrlForLayer
ecr:BatchGetImage
绝不能包含任何业务相关的高权限,如 s3:*
或 dynamodb:*
。
第 2 层:强制启用 IMDSv2
IMDSv2 (Instance Metadata Service Version 2) 引入了会话令牌机制,可以有效防御常见的 SSRF(服务器端请求伪造)攻击。您应该在启动 EC2 实例时,通过参数设置,强制要求必须使用 IMDSv2,并禁用旧的 IMDSv1。
第 3 层:在网络层面阻断 Pod 访问
这是最强有力的防护措施。我们可以通过 iptables
规则,在每个 Worker Node 上,禁止来自 Pod 的流量访问元数据接口。
一个常见的做法是在节点启动脚本中加入如下规则:
|
|
通过这三层防御,我们可以安全地使用节点 IAM 实例配置文件来完成集群引导,同时杜绝了 Pod 越权获取节点权限的风险。这套组合拳是自建 Kubernetes on AWS 的安全最佳实践。
Q3: 为何不能手动注入环境变量,必须依赖 Webhook?
如果 pod-identity-webhook
没有运行,我能否手动在我的 Pod 定义中添加 AWS_ROLE_ARN
和 AWS_WEB_IDENTITY_TOKEN_FILE
环境变量来完成授权?
答案是:不行,这样是无法成功授权的。
关键原因在于 Service Account Token 的受众(Audience)。
令牌受众不匹配:AWS STS 要求用于身份验证的 JWT(即 Service Account Token)的
aud
字段必须是sts.amazonaws.com
。而 Kubernetes 默认提供给 Pod 的令牌,其受众是 Kubernetes API Server。如果直接使用这个默认令牌,AWS STS 会因为受众不匹配而拒绝请求。Webhook 的核心价值:
pod-identity-webhook
的核心价值并不仅仅是注入环境变量。它最关键的操作是向 Pod 注入一个projected
类型的volume
。这个volume
会指示kubelet
去向 API Server 申请一个专门为 AWS STS 定制的、具有正确受众(sts.amazonaws.com
)的令牌,并将其挂载到 Pod 中。
因此,pod-identity-webhook
自动化了这个复杂的过程。理论上,你也可以手动在 Pod 的 YAML 中定义这个复杂的 projected
volume,但这非常繁琐且容易出错,完全违背了 IRSA 简化授权的初衷。所以,正确部署并依赖 pod-identity-webhook
是实现 IRSA 的标准和推荐做法。
Q4: EKS 如何为 CNI 实现 IRSA,自建集群能借鉴吗?
有经验的用户可能会发现,在 AWS EKS(Elastic Kubernetes Service)中,aws-cni
插件本身是可以通过 IRSA 来获取 IAM 权限的。这似乎与我们之前讨论的“必须靠节点 IAM 角色引导”的结论相矛盾。原因何在?
答案在于 EKS 作为托管服务的特殊性。EKS 的控制平面由 AWS 深度定制和管理,它内置了处理 IRSA 的逻辑,无需依赖一个运行在节点上的 pod-identity-webhook
。
EKS 的内部机制
- 内置于控制平面的 Webhook:可以认为 EKS 的 API Server 内部集成了一个私有的、针对 AWS 服务的 Admission Webhook。
- 主动注入:当 CNI Pod 创建时,EKS 的控制平面会直接识别其 ServiceAccount,并主动为 Pod 注入 IRSA 所需的 projected volume 和环境变量。
- 绕过引导难题:这个过程完全在控制平面内部完成,不依赖任何需要 Pod 网络才能运行的组件。因此,EKS 从根本上解决了“鸡和蛋”的问题。
结论:EKS 的这种能力是其作为托管服务深度集成云资源的结果。在自建的、使用原生开源 Kubernetes 的集群中,我们没有这种“超能力”,因此必须遵循“节点 IAM 角色 -> CNI -> Pod 网络 -> Webhook -> 应用 IRSA”的标准引导路径。
自建集群的替代方案探讨
为了解决 Webhook 对 CNI 的依赖,社区也曾探讨过更激进的方案,例如:
让 Webhook Pod 使用
hostNetwork: true
运行:- 理论:Pod 直接使用宿主机网络,无需等待 CNI 就绪。
- 实践问题:这会带来端口冲突、服务发现复杂化和安全风险等一系列问题,因此在生产中强烈不推荐。
将 Webhook 独立于 Kubernetes 集群外部署:
- 理论:Webhook 作为一个外部服务运行,彻底与集群生命周期解耦。
- 实践问题:这需要解决 API Server 到外部服务的网络连通性、外部服务的 K8s 认证授权、以及独立的运维和高可用等难题,大大增加了架构复杂性。
最终结论:这两种方案都得不偿失。最稳健和标准的做法依然是依赖节点的 IAM 实例配置文件来引导 CNI 和其他核心组件。
Q5: OIDC Discovery 端点除了 S3,能否用 CDN 或 Nginx 实现?
完全可以,这是一个很好的架构选型问题。指南中使用 S3 是因为它最简单、最直接,但 CDN 和 Nginx 也是可行的方案。
方案一:使用 CDN(如 AWS CloudFront)
这是一个优秀的生产级替代方案。
- 优点:
- 性能更佳:CDN 会在全球边缘节点缓存 OIDC 文档,AWS STS 可以从最近的节点获取,延迟更低。
- 自定义域名与 TLS:可以轻松配置自定义域名(如
oidc.your-domain.com
)并使用 ACM 免费管理 TLS 证书,使 Issuer URL 更专业。
- 缺点:
- 缓存失效的复杂性:这是关键挑战。当你轮换签名密钥并更新
keys.json
后,必须手动使 CDN 缓存失效,否则 STS 会获取到旧的公钥导致验证失败。这个额外的操作步骤容易被忽略。
- 缓存失效的复杂性:这是关键挑战。当你轮换签名密钥并更新
方案二:使用自建 Nginx
技术上可行,但通常不推荐。
- 优点:
- 完全控制:对服务器、配置和安全有完全的控制权。
- 缺点:
- 极高的运维负担:你需要自己负责 Nginx 的高可用、TLS 证书的生命周期管理、服务器的安全补丁等,这为了托管两个静态文件来说,成本过高。
结论:对于自建集群,S3 是最简单直接的方案。CDN 是一个更优的生产级方案,前提是你有自动化的流程来处理密钥轮换时的缓存失效问题。而 Nginx 则带来了不必要的复杂性,应尽量避免。
Q6: 如何确认我现有的自建 Kubernetes 集群是否已配置了自定义的 service-account-key-file
?
这是一个在对存量集群改造时至关重要的问题。在配置 IRSA 之前,您必须确认 kube-apiserver
是如何管理 Service Account (SA) 令牌签名的。
核心目标:检查 kube-apiserver
进程的启动参数。
有几种方法可以做到这一点,最直接的方法是登录到您的 Kubernetes Master 节点并执行以下操作:
方法一:直接检查进程参数 (最通用)
这是最可靠的方法,因为它直接反映了正在运行的配置。
|
|
在输出中,仔细查找以下几个参数:
--service-account-key-file
: 指定用于验证 SA 令牌的公钥文件。--service-account-signing-key-file
: 指定用于签署 SA 令牌的私钥文件。--service-account-issuer
: OIDC issuer URL。
结果解读:
如果
service-account-key-file
和service-account-signing-key-file
已存在:- 恭喜! 您的集群已经在使用自定义密钥对。您不需要生成新的密钥。
- 您的任务是获取
--service-account-key-file
参数指向的那个公钥文件,并用它来生成keys.json
。 - 您仍然需要按照指南配置
--service-account-issuer
和--api-audiences
,这同样需要重启kube-apiserver
。
如果这些参数不存在:
- 这意味着您的集群正在使用由 Kubernetes 自动管理和轮换的内部密钥。
- 您必须按照指南中的“步骤 1: 生成 OIDC Discovery 相关内容”生成一对新的密钥,并将它们配置到
kube-apiserver
的启动参数中。 - 警告:这是一个破坏性变更。添加这些参数并重启
kube-apiserver
后,所有旧的 SA 令牌都会立即失效,可能会导致正在运行的应用中断。请务必在计划的维护窗口内执行此操作。
方法二:检查静态 Pod 定义 (如果使用 kubeadm)
如果您的集群是使用 kubeadm
安装的,kube-apiserver
通常作为静态 Pod 运行。
|
|
在 YAML 文件的 spec.containers.command
部分查找上述的几个参数。
方法三:检查 systemd 服务单元
如果您的 kube-apiserver
是作为 systemd 服务运行的,您可以检查其服务定义。
|
|
在 ExecStart
行中查找这些参数。
结论:通过以上检查,您可以明确当前集群的状态,并决定是复用现有密钥还是引入新密钥,从而安全、有计划地推进 IRSA 的配置。
Q7: --service-account-key-file
和 --service-account-issuer
有什么关系?如果必须更改 issuer
该怎么办?
这是两个在概念上完全独立但又紧密协作的参数。
1. 两者的关系
--service-account-key-file <PUBLIC_KEY_PATH>
: 这是公钥,kube-apiserver
用它来验证一个 SA 令牌的签名是否有效。--service-account-issuer <ISSUER_URL>
: 这是一个 URL,kube-apiserver
在签发新令牌时,会将这个 URL 作为iss
(issuer) 声明写入令牌中,相当于给令牌盖上一个“签发者”的身份戳。
简单来说,key-file
是验签的工具,而 issuer
是身份的标识。
2. 更改 --service-account-issuer
的严重后果与处理流程
警告:--service-account-issuer
URL 应被视为永久不变的。 在生产环境中更改它是一个高风险、高成本的破坏性操作。除非万不得已,否则应极力避免。
为什么风险高?
- IAM OIDC Provider 强绑定 Issuer URL: AWS IAM 中的 OIDC Provider 与您首次配置的 Issuer URL 是一一对应的,且创建后不可修改。
- 信任链中断: 更改
kube-apiserver
的issuer
后,新签发的令牌将携带新的iss
声明。当这个新令牌被递交给 AWS 时,AWS 会发现它的iss
与 IAM OIDC Provider 中注册的 URL 不匹配,从而拒绝信任该令牌。这导致所有依赖 IRSA 的应用瞬间失去访问 AWS 资源的权限。
如果必须更改,请严格遵循以下流程,并在维护窗口执行:
准备新的 OIDC 托管端点: 确保您的 OIDC Discovery 文档 (
.well-known/openid-configuration
和keys.json
) 可以通过新的 Issuer URL 被公开访问。重建 AWS IAM OIDC Provider (核心破坏步骤): a. 登录 AWS 控制台,删除旧的 IAM OIDC Provider。 b. 创建一个新的 IAM OIDC Provider,填入新的 Issuer URL。 c. 更新所有相关 IAM 角色的信任策略。您必须逐一编辑那些之前信任旧 OIDC Provider 的 IAM 角色,将其信任策略中的 OIDC Provider ARN 更新为新创建的 Provider 的 ARN。这是一个极易出错且工作量巨大的步骤。
更新并重启
kube-apiserver
: a. 修改kube-apiserver
的静态 Pod manifest 或 systemd 服务文件,将--service-account-issuer
参数更新为新的 URL。 b. 滚动重启kube-apiserver
实例,使配置生效。强制刷新集群中所有 Pod 的 SA 令牌:
kube-apiserver
更新后,现存 Pod 中的旧令牌不会自动刷新。您必须强制它们获取包含新issuer
的令牌。- 最稳妥的方式是滚动重启您的所有工作负载(Deployments, StatefulSets, etc.)。
1 2
# 示例:滚动重启一个 Deployment kubectl rollout restart deployment/<deployment-name> -n <namespace>
结论:更改 issuer
的过程复杂且充满风险。请在首次部署 IRSA 时就仔细规划并使用一个长期稳定的 URL。
Q8: 我已经配置了 apiserver
,如何确认某个 Pod 真的获取到了用于 IRSA 的令牌?
这是一个从“配置完成”到“验证生效”的关键问题。apiserver
配置正确只是前提,Pod 能否获取到被正确“签注”的令牌,还需要依赖于 IRSA Webhook 的正常工作。
检查的关键是查看一个正在运行的、且配置了 IRSA 的 Pod 的实时 YAML 定义。
检查步骤
假设您有一个 Pod,它所使用的 ServiceAccount 已经被添加了 eks.amazonaws.com/role-arn
注解。
获取 Pod 的实时 YAML
1
kubectl get pod <your-pod-name> -n <your-namespace> -o yaml
在 YAML 输出中寻找三个关键证据
证据一:检查容器的环境变量 (
spec.containers.env
)Webhook 会自动注入两个环境变量:
1 2 3 4 5 6 7
spec: containers: - env: - name: AWS_ROLE_ARN value: arn:aws:iam::111122223333:role/my-app-role - name: AWS_WEB_IDENTITY_TOKEN_FILE value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
证据二:检查容器的卷挂载 (
spec.containers.volumeMounts
)您会看到一个与
AWS_WEB_IDENTITY_TOKEN_FILE
路径匹配的挂载点:1 2 3 4
volumeMounts: - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount name: aws-iam-token # 记住这个卷的 name readOnly: true
证据三:检查 Pod 的卷定义 (
spec.volumes
) - 最核心的证据根据上一步的卷
name
(aws-iam-token
),在 Pod 的卷列表中找到它的定义。您会发现它是一个projected
(投射) 类型的卷,这正是 IRSA 的魔法所在:1 2 3 4 5 6 7 8 9
volumes: - name: aws-iam-token projected: defaultMode: 420 sources: - serviceAccountToken: audience: sts.amazonaws.com # 核心证据!令牌的受众(audience)被正确设置为 AWS STS expirationSeconds: 86400 # 核心证据!这是一个有时效性的短期令牌 path: token # 令牌文件名
结论
如果您在一个 Pod 的实时 YAML 中找到了全部这三个证据,特别是证据三中带有正确 audience
的 projected
卷,那么就可以 100% 确定,IRSA 机制已为这个 Pod 成功生成并挂载了专用的 AWS OIDC 令牌。