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/node
docker 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:reviewed

USER 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: false
kubectl 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 会话。

搜索文档

输入关键词,搜索所有文档。

↑ ↓ 选择 · Enter 打开 · Esc 关闭