Kubernetes Agent:镜像、Pod 身份与 EKS IAM
验证:未记录
- 适用版本
- 未记录
- 验证范围
- 尚未登记验证记录。
本篇以 EKS 上的 Jenkins Controller + Kubernetes 插件为例,做两类 Agent:ci-test 跑不可信 PR 的 Node 测试,不授 Kubernetes API 或 AWS 权限;ci-fixtures 跑受保护主分支的集成测试,只能读一个 S3 测试数据前缀。镜像构建与生产发布仍使用独立可信 Agent和发布流程。Kubernetes 插件支持 YAML Pod 模板和多容器 Agent。插件文档
1. 是否需要自制基础镜像#
Pod 至少有承载 Jenkins remoting 的 jnlp 容器;工具放在其他容器。本例保留官方 jenkins/inbound-agent 作为 jnlp,只定制跑测试的 node-ci 容器,让 remoting 和业务工具链独立升级。
| 工具 | 放置位置 | 决策 |
|---|---|---|
| Java / Jenkins remoting | jnlp 官方镜像 |
只负责连 Controller |
| Node、npm、Git、CA、curl、jq | 自制 node-ci 镜像 |
多仓库共用固定版本,镜像构建时验证 |
| AWS CLI | 仅受信 ci-fixtures 工具镜像 |
普通 PR 不需要云 API |
| kubectl、Helm、Podman、Docker CLI | 本例不装 | 测试 Job 不部署,也不构建容器 |
只有一个仓库偶尔用单一工具时,可直接采用审核过、固定 digest 的语言镜像。多个仓库反复在启动时 apt install、版本漂移或联网下载失败时,才做共享工具镜像。不要把“常用工具”全部预装。以下 Dockerfile 使用平台审核的 Node 基础镜像 digest;Debian 包源也应使用组织快照,并记录最终镜像 digest/SBOM,否则不能声称完整可复现。
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git curl jq \
&& rm -rf /var/lib/apt/lists/*
USER node
RUN node --version && npm --version && git --version && curl --version && jq --version
WORKDIR /home/nodedocker build --file Dockerfile.node-ci \
--build-arg BASE_IMAGE='node:24-bookworm-slim@sha256:REPLACE_VERIFIED_DIGEST' \
-t registry.example.com/ci/node-ci:reviewed .
docker push registry.example.com/ci/node-ci:reviewed
docker buildx imagetools inspect registry.example.com/ci/node-ci:reviewedUSER node 的 UID/GID 要与 Pod 的 runAsUser/runAsGroup 和 workspace 写权限匹配;在目标集群实际运行 npm ci,不要只检查 node --version。项目依赖仍由仓库锁文件管理。两个运行镜像都要固定 digest,并验证 Controller/Agent 的 Java 兼容性和目标 CPU 架构。
2. Controller 和 Agent 是两套 ServiceAccount#
Controller 使用 jenkins/jenkins-controller SA,在 ci-agents namespace 创建、观察、删除构建 Pod;Role/RoleBinding 见动态 Agent 基线。能创建 Pod 本身就是强权限:可能借 Pod 使用该 namespace 的其他 SA、挂载 Secret 或请求特权。ci-agents 不放业务 Secret;用准入策略限制 SA、镜像来源、hostPath、宿主机网络和特权容器,并用独立 Controller 处理不可信代码。Kubernetes RBAC 良好实践
普通测试 Agent 建一个没有 RoleBinding 的 SA;npm test 不需要 get pods 或 get secrets:
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-test
namespace: ci-agents
automountServiceAccountToken: falsekubectl apply -f ci-test-sa.yaml
kubectl auth can-i get secrets -n ci-agents --as=system:serviceaccount:ci-agents:ci-test
kubectl auth can-i create pods -n ci-agents --as=system:serviceaccount:ci-agents:ci-test两项预期均为 no;身份模拟检查要求操作者有 impersonate 权限。还应确认 Controller 的 can-i create pods 为 yes、跨 namespace 读 Secret 为 no。这些静态检查不能替代实际 Pod 的负向测试。
3. 测试 Pod 模板和 Jenkinsfile#
在 Manage Jenkins → Clouds → Kubernetes 中固定 cloud k8s-test 的 namespace 为 ci-agents,Jenkins URL 指向 Pod 可达的内部地址,连接方式选 WebSocket。把下列 Jenkinsfile 放到测试仓库,替换两个审核过的镜像 digest;jnlp 不设置自定义 command/args,由插件填写连接参数。
podTemplate(cloud: 'k8s-test', yaml: '''
apiVersion: v1
kind: Pod
spec:
serviceAccountName: ci-test
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile: {type: RuntimeDefault}
containers:
- name: jnlp
image: jenkins/inbound-agent@sha256:REPLACE_VERIFIED_DIGEST
resources:
requests: {cpu: 100m, memory: 256Mi}
limits: {cpu: '1', memory: 512Mi}
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
- name: node
image: registry.example.com/ci/node-ci@sha256:REPLACE_VERIFIED_DIGEST
command: [cat]
tty: true
workingDir: /home/jenkins/agent
resources:
requests: {cpu: 500m, memory: 512Mi}
limits: {cpu: '2', memory: 2Gi}
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
''') {
node(POD_LABEL) {
checkout scm
container('node') { sh 'node --version && npm ci && npm test' }
}
}私有仓库拉镜像需要的 imagePullSecrets 只用于拉镜像,不放应用运行秘密。给 namespace 设置 ResourceQuota 和默认拒绝网络策略,再按 DNS、内部 Jenkins、Git、包仓库实际地址开放出口;所用 CNI 必须支持并验证 NetworkPolicy。封锁节点角色的 IMDS 访问;不挂 Kubernetes token 不等于没有 AWS 权限。EKS 身份最佳实践
4. 受信 Agent 的 EKS IAM:独立 SA + 精确 IRSA#
假设受保护 main 的集成测试要读 s3://ci-fixtures-example/orders/。新建 ci-trusted namespace、受信 Controller/cloud 与 ci-fixtures SA;不要给 ci-test 绑定 IAM。受信 Controller 使用另一 SA,例如 jenkins-trusted/jenkins-controller。将动态 Agent 基线中的 Role/RoleBinding 复制到 ci-trusted,并把 RoleBinding 的 subject 改为这个受信 Controller;ci-agents 的 Controller 不应能在 ci-trusted 创建 Pod。用下列检查验收:
kubectl auth can-i create pods -n ci-trusted --as=system:serviceaccount:jenkins-trusted:jenkins-controller
kubectl auth can-i create pods -n ci-trusted --as=system:serviceaccount:jenkins:jenkins-controller预期依次为 yes、no。受信 Controller 的 Kubernetes cloud namespace 固定为 ci-trusted;隔离还需准入策略和节点/网络边界,不能只靠这个 RoleBinding。这里选 IRSA:trust 明确锁定集群 OIDC issuer、namespace 和 SA;同一个 SA 不叠配 EKS Pod Identity。若改用 Pod Identity,先安装 Pod Identity Agent、确认 SDK 支持,并创建绑定该 namespace/SA 的 association。IRSA 官方步骤 · Pod Identity 官方步骤
管理员先确认集群 OIDC provider 已建立。以下 IAM policy 存为 fixtures-policy.json;替换成批准的测试桶。列表权限只限前缀,对象读取只限 orders/*:
{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::ci-fixtures-example",
"Condition": {"StringLike": {"s3:prefix": ["orders/", "orders/*"]}}},
{"Effect": "Allow", "Action": "s3:GetObject",
"Resource": "arn:aws:s3:::ci-fixtures-example/orders/*"}
]
}先核对账户、集群 issuer 与 IAM OIDC provider:
export AWS_REGION='REPLACE_REGION' CLUSTER='REPLACE_CLUSTER'
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
OIDC_URL=$(aws eks describe-cluster --name "$CLUSTER" --region "$AWS_REGION" --query cluster.identity.oidc.issuer --output text)
OIDC_HOST=${OIDC_URL#https://}
aws iam get-open-id-connect-provider \
--open-id-connect-provider-arn "arn:aws:iam::$ACCOUNT_ID:oidc-provider/$OIDC_HOST"用实际 ACCOUNT_ID 和 OIDC_HOST 替换以下占位符(包括 JSON key 中的占位符),保存为 trust.json。此例使用商业 AWS 分区;其他分区要调整 ARN。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Federated": "arn:aws:iam::REPLACE_ACCOUNT_ID:oidc-provider/REPLACE_OIDC_HOST"},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {"StringEquals": {
"REPLACE_OIDC_HOST:aud": "sts.amazonaws.com",
"REPLACE_OIDC_HOST:sub": "system:serviceaccount:ci-trusted:ci-fixtures"
}}
}]
}aws iam create-role --role-name ci-fixtures-read --assume-role-policy-document file://trust.json
aws iam put-role-policy --role-name ci-fixtures-read --policy-name fixtures-read --policy-document file://fixtures-policy.json在 ci-trusted 创建下列 SA,再让该环境的 Pod 模板设置 serviceAccountName: ci-fixtures。IRSA 会注入 Web Identity 环境变量和投射 token。不要在 Pod 再配置静态 AWS_ACCESS_KEY_ID,也不要归档投射 token。此 SA 不授 Kubernetes RoleBinding:IAM 与 Kubernetes RBAC 是两套授权。
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-fixtures
namespace: ci-trusted
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::REPLACE_ACCOUNT_ID:role/ci-fixtures-read受信 Job 使用下列 Pod 模板。AWS CLI 容器用经过审核、固定 digest 的官方镜像,不与普通 PR 的 node-ci 镜像复用;aws 容器的 shell 和 UID 需在目标镜像验证。ci-trusted cloud 由受信 Controller 管理并固定 namespace。不要把这段 Pod 模板交给普通 PR 修改。
podTemplate(cloud: 'k8s-trusted', yaml: '''
apiVersion: v1
kind: Pod
spec:
serviceAccountName: ci-fixtures
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile: {type: RuntimeDefault}
containers:
- name: jnlp
image: jenkins/inbound-agent@sha256:REPLACE_VERIFIED_DIGEST
resources:
requests: {cpu: 100m, memory: 256Mi}
limits: {cpu: '1', memory: 512Mi}
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
- name: aws
image: public.ecr.aws/aws-cli/aws-cli@sha256:REPLACE_VERIFIED_DIGEST
command: [sh, -c]
args: ["sleep 99d"]
resources:
requests: {cpu: 100m, memory: 256Mi}
limits: {cpu: '1', memory: 512Mi}
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
''') {
node(POD_LABEL) {
container('aws') {
sh 'aws sts get-caller-identity'
sh 'aws s3api get-object --bucket ci-fixtures-example --key orders/smoke.json fixture.json >/dev/null'
}
}
}镜像在目标集群应通过 UID、shell、workspace 与准入策略检查。运行后应看到 ci-fixtures-read 的 assumed role,且指定对象可读。用同一个 Pod 测试其他前缀和桶的已知对象应被拒;ci-test Pod 运行身份查询应失败,不能退回节点角色。检查 CloudTrail 的角色与 S3 请求。跨账号桶或 SSE-KMS 还需单独审核桶策略与 KMS 授权,本例没有授予。
5. 故障注入与回退证据#
| 注入 | 预期 |
|---|---|
| 删除一次测试 Agent Pod | 该任务失败或受控重试,其他任务不受影响 |
ci-test 读 Kubernetes Secret |
因无 token 或无 RBAC 被拒 |
ci-test 调 aws sts get-caller-identity |
无凭据,绝不能返回节点角色 |
ci-fixtures 读允许与禁止的 S3 前缀 |
前者成功,后者 AccessDenied;CloudTrail 为指定角色 |
| 撤去测试前缀权限 | 新会话读取失败;既有会话按 STS 有效期处理 |
| Controller 断开 Kubernetes API | 新 Pod 创建失败并告警,不把任务报成成功 |
晋级时保存 Controller 镜像、插件清单、两种 Agent 镜像 digest、Pod 模板、RBAC、IAM trust/policy 和测试结果。先在隔离 Controller/namespace 验证,再晋级;失败按插件生命周期恢复。镜像回退不会自动撤销已签发的 IAM 会话。