自建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 Credentials
  1. Pod 创建: 当一个 Pod 被创建时,如果其关联的 ServiceAccount (SA) 带有特定的注解(指定要扮演的 IAM 角色),Kubernetes API Server 会调用 pod-identity-webhook
  2. Webhook 修改 Pod: 这个 mutating webhook 会向 Pod 中注入两个环境变量:AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE
  3. Pod 调用 AWS STS: Pod 中的应用程序(使用 AWS SDK)利用其 Service Account Token 调用 AWS STS 的 AssumeRoleWithWebIdentity API,请求扮演 AWS_ROLE_ARN 指定的角色。
  4. STS 验证 Token: AWS STS 收到请求后,需要验证 Service Account Token 的有效性。它会访问预先注册的 OIDC Discovery 端点以获取公钥。
  5. Discovery 端点返回公钥: OIDC Discovery 端点(通常是一个公开的 S3 存储桶)返回用于签署 Service Account Token 的 JWKS (JSON Web Key Set)。
  6. STS 完成验证: STS 使用从 Discovery 端点获取的公钥来验证 Pod 的 Service Account Token 的签名。
  7. 返回临时凭证: 如果 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 的密钥。

  1. 生成 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
  2. 生成 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. 设置环境变量

    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"
  2. 创建 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
  3. 生成并上传 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. 设置环境变量

     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"
  2. 创建私有 S3 存储桶

    注意:这次我们不设置任何公开访问策略。

    1
    2
    3
    4
    
    aws s3api create-bucket \
      --bucket $DISCOVERY_BUCKET \
      --region $AWS_DEFAULT_REGION \
      --create-bucket-configuration LocationConstraint=$AWS_DEFAULT_REGION
  3. 生成并上传 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
  4. 配置 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 的存在。根据您选择的方案,获取指纹的命令有所不同。

  1. 获取服务器证书指纹 (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 \':\')
  2. 创建 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 的启动参数,添加以下标志:

1
2
3
4
--service-account-key-file=/path/to/your/keys/oidc-issuer.pub
--service-account-signing-key-file=/path/to/your/keys/oidc-issuer.key
--service-account-issuer=$ISSUER_URL
--api-audiences=sts.amazonaws.com
  • 确保 kube-apiserver 容器可以访问您在步骤 1 中生成的 oidc-issuer.keyoidc-issuer.pub 文件。您可能需要将它们作为 Secret 挂载或直接放在 Master 节点的某个路径下。
  • 重启 kube-apiserver 使配置生效。

步骤 4: 部署 Pod Identity Webhook

此 Webhook 会自动为带有特定注解的 Service Account 的 Pod 注入 AWS 相关的环境变量。

  1. 安装 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 完全启动并运行。

  2. 部署 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 配置

现在,我们将创建一个示例应用来验证整个流程是否正常工作。

  1. 创建 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"
  2. 创建 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
  3. 检查结果

    • 查看 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)提供。

引导流程解析

  1. 节点 IAM 角色是基础:您的 EC2 Worker 节点必须附加一个 IAM 实例配置文件,该文件授予了 AWS CNI 运行所需的最低权限(例如 ec2:CreateNetworkInterface, ec2:DescribeNetworkInterfaces 等)。

  2. CNI 继承节点权限:当集群启动时,aws-node Pod(AWS CNI 的实现)作为 DaemonSet 在每个节点上运行。此时,由于 IRSA 尚未就绪,它会直接继承所在 EC2 节点的 IAM 角色权限。

  3. 网络就绪:CNI Pod 利用从节点继承的权限成功连接到 AWS API,为集群建立起 Pod 网络。

  4. Webhook 启动:一旦 Pod 网络可用,pod-identity-webhook 和其他集群组件(如 CoreDNS)才能被成功调度和启动。

  5. IRSA 生效:只有当 pod-identity-webhook 正常运行后,IRSA 才能开始为其他应用程序提供细粒度的 IAM 角色。

结论:IRSA 并非用于授权所有 Pod。对于像 CNI 这样需要在集群引导阶段就必须工作的“系统级”组件,它们的权限必须由更底层的节点 IAM 实例配置文件来保障。IRSA 则专注于为运行在正常网络之上的“用户级”应用提供更安全、动态的权限管理。

Q2: 节点 IAM 角色是否存在安全风险?如何防范?

既然 CNI 依赖节点 IAM 角色,那么节点上的任何 Pod 不就能通过访问 EC2 元数据接口(169.254.169.254)来“窃取”这个角色的权限吗?

这个安全顾虑完全正确!如果不加防护,这就是一个严重的安全漏洞。幸运的是,我们有成熟的、多层防御的方案来解决这个问题。

第 1 层:遵循最小权限原则

附加给节点的 IAM 角色应严格遵循最小权限原则。它只应包含 kubeletaws-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 的流量访问元数据接口。

一个常见的做法是在节点启动脚本中加入如下规则:

1
2
3
4
5
6
# 假设您的 Pod 网段是 10.244.0.0/16
POD_CIDR="10.244.0.0/16"
METADATA_IP="169.254.169.254"

# 阻止所有来自 Pod 网段的流量访问元数据接口
iptables -A FORWARD -s $POD_CIDR -d $METADATA_IP -j DROP

通过这三层防御,我们可以安全地使用节点 IAM 实例配置文件来完成集群引导,同时杜绝了 Pod 越权获取节点权限的风险。这套组合拳是自建 Kubernetes on AWS 的安全最佳实践。

Q3: 为何不能手动注入环境变量,必须依赖 Webhook?

如果 pod-identity-webhook 没有运行,我能否手动在我的 Pod 定义中添加 AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE 环境变量来完成授权?

答案是:不行,这样是无法成功授权的。

关键原因在于 Service Account Token 的受众(Audience)

  1. 令牌受众不匹配:AWS STS 要求用于身份验证的 JWT(即 Service Account Token)的 aud 字段必须是 sts.amazonaws.com。而 Kubernetes 默认提供给 Pod 的令牌,其受众是 Kubernetes API Server。如果直接使用这个默认令牌,AWS STS 会因为受众不匹配而拒绝请求。

  2. 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 的内部机制

  1. 内置于控制平面的 Webhook:可以认为 EKS 的 API Server 内部集成了一个私有的、针对 AWS 服务的 Admission Webhook。
  2. 主动注入:当 CNI Pod 创建时,EKS 的控制平面会直接识别其 ServiceAccount,并主动为 Pod 注入 IRSA 所需的 projected volume 和环境变量。
  3. 绕过引导难题:这个过程完全在控制平面内部完成,不依赖任何需要 Pod 网络才能运行的组件。因此,EKS 从根本上解决了“鸡和蛋”的问题。

结论:EKS 的这种能力是其作为托管服务深度集成云资源的结果。在自建的、使用原生开源 Kubernetes 的集群中,我们没有这种“超能力”,因此必须遵循“节点 IAM 角色 -> CNI -> Pod 网络 -> Webhook -> 应用 IRSA”的标准引导路径。

自建集群的替代方案探讨

为了解决 Webhook 对 CNI 的依赖,社区也曾探讨过更激进的方案,例如:

  1. 让 Webhook Pod 使用 hostNetwork: true 运行

    • 理论:Pod 直接使用宿主机网络,无需等待 CNI 就绪。
    • 实践问题:这会带来端口冲突、服务发现复杂化和安全风险等一系列问题,因此在生产中强烈不推荐。
  2. 将 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 节点并执行以下操作:

方法一:直接检查进程参数 (最通用)

这是最可靠的方法,因为它直接反映了正在运行的配置。

1
2
# 在 Master 节点上执行
ps aux | grep kube-apiserver

在输出中,仔细查找以下几个参数:

  • --service-account-key-file: 指定用于验证 SA 令牌的公钥文件。
  • --service-account-signing-key-file: 指定用于签署 SA 令牌的私钥文件。
  • --service-account-issuer: OIDC issuer URL。

结果解读:

  1. 如果 service-account-key-fileservice-account-signing-key-file 已存在:

    • 恭喜! 您的集群已经在使用自定义密钥对。您不需要生成新的密钥。
    • 您的任务是获取 --service-account-key-file 参数指向的那个公钥文件,并用它来生成 keys.json
    • 您仍然需要按照指南配置 --service-account-issuer--api-audiences,这同样需要重启 kube-apiserver
  2. 如果这些参数不存在:

    • 这意味着您的集群正在使用由 Kubernetes 自动管理和轮换的内部密钥。
    • 您必须按照指南中的“步骤 1: 生成 OIDC Discovery 相关内容”生成一对新的密钥,并将它们配置到 kube-apiserver 的启动参数中。
    • 警告:这是一个破坏性变更。添加这些参数并重启 kube-apiserver 后,所有旧的 SA 令牌都会立即失效,可能会导致正在运行的应用中断。请务必在计划的维护窗口内执行此操作。

方法二:检查静态 Pod 定义 (如果使用 kubeadm)

如果您的集群是使用 kubeadm 安装的,kube-apiserver 通常作为静态 Pod 运行。

1
2
# 在 Master 节点上查看静态 Pod manifest
cat /etc/kubernetes/manifests/kube-apiserver.yaml

在 YAML 文件的 spec.containers.command 部分查找上述的几个参数。

方法三:检查 systemd 服务单元

如果您的 kube-apiserver 是作为 systemd 服务运行的,您可以检查其服务定义。

1
2
3
4
5
6
# 查找服务文件
sudo systemctl status kube-apiserver
# (从输出中找到服务文件的路径,例如 /etc/systemd/system/kube-apiserver.service)

# 查看服务定义
cat /etc/systemd/system/kube-apiserver.service

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>: 这是一个 URLkube-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-apiserverissuer 后,新签发的令牌将携带新的 iss 声明。当这个新令牌被递交给 AWS 时,AWS 会发现它的 iss 与 IAM OIDC Provider 中注册的 URL 不匹配,从而拒绝信任该令牌。这导致所有依赖 IRSA 的应用瞬间失去访问 AWS 资源的权限。

如果必须更改,请严格遵循以下流程,并在维护窗口执行:

  1. 准备新的 OIDC 托管端点: 确保您的 OIDC Discovery 文档 (.well-known/openid-configurationkeys.json) 可以通过新的 Issuer URL 被公开访问。

  2. 重建 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。这是一个极易出错且工作量巨大的步骤。

  3. 更新并重启 kube-apiserver: a. 修改 kube-apiserver 的静态 Pod manifest 或 systemd 服务文件,将 --service-account-issuer 参数更新为新的 URL。 b. 滚动重启 kube-apiserver 实例,使配置生效。

  4. 强制刷新集群中所有 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 注解。

  1. 获取 Pod 的实时 YAML

    1
    
    kubectl get pod <your-pod-name> -n <your-namespace> -o yaml
  2. 在 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 中找到了全部这三个证据,特别是证据三中带有正确 audienceprojected 卷,那么就可以 100% 确定,IRSA 机制已为这个 Pod 成功生成并挂载了专用的 AWS OIDC 令牌。

0%