构建基于GitHub Actions OIDC与SAML的EKS上Ray集群零信任部署管道


在生产环境中,静态的、长期有效的AWS Access Key IDSecret Access Key 是一个必须被根除的安全隐患。将它们作为GitHub Secrets存储,用于CI/CD流水线,本质上只是将风险从开发者的本地机器转移到了GitHub的设置页面。一旦泄露,其后果不堪设想。我们团队的目标很明确:构建一个完全自动化的流程,通过git push部署和更新运行在AWS EKS上的Ray集群,整个过程无需任何长期凭证。同时,我们的算法工程师和数据科学家需要通过公司的SAML身份提供商(IdP)无缝、安全地访问这些集群进行调试和监控,而不是通过共享的、难以审计的kubeconfig文件。

这个任务的核心是解决两种身份的认证与授权问题:机器身份(GitHub Actions Runner)和人类身份(工程师)。我们的解决方案最终融合了GitHub Actions对OIDC的支持,以及AWS IAM对SAML联邦认证的能力,构建了一个端到端的零信任部署与访问管道。

第一阶段:为机器身份建立信任 - GitHub OIDC与AWS IAM的联邦

问题的起点是如何让GitHub Actions的Runner在不持有任何预置密钥的情况下,获得操作AWS资源的临时权限。这正是OpenID Connect (OIDC)的用武之地。其基本流程是:

  1. GitHub Actions在工作流运行时,会向其自身的OIDC提供商请求一个JSON Web Token (JWT)。这个JWT包含了关于当前工作流的详细信息,如代码仓库、分支、触发事件等。
  2. 我们将AWS IAM配置为信任GitHub的OIDC提供商。
  3. 工作流将这个JWT提交给AWS Security Token Service (STS) 的AssumeRoleWithWebIdentity API。
  4. STS验证JWT的签名和声明(claims),如果符合我们在IAM Role信任策略中定义的条件,就会向Runner颁发一组有时效性的、具备特定权限的临时AWS凭证。

这种方式的安全性在于,凭证是临时的,且其权限被严格限定在IAM Role的策略范围内。更重要的是,信任关系是基于可验证的JWT声明建立的,而不是一个可能被泄露的静态密钥。

1.1 在AWS IAM中创建OIDC身份提供商

这是建立信任的第一步。我们需要告诉AWS,我们信任来自token.actions.githubusercontent.com的JWT。

# 使用AWS CLI创建一个OIDC身份提供商
# 注意:你需要从GitHub的OIDC配置端点获取指纹
# 这通常可以通过浏览器访问 https://token.actions.githubusercontent.com/.well-known/openid-configuration
# 然后找到 jwks_uri,再从中提取TLS证书指纹。
# 为方便起见,大部分情况下指纹是固定的,但生产环境务必自行验证。
THUMBPRINT=$(echo | openssl s_client -servername token.actions.githubusercontent.com -showcerts -connect token.actions.githubusercontent.com:443 2>/dev/null | openssl x509 -fingerprint -noout |- sed 's/://g' | cut -d'=' -f2)

aws iam create-open-id-connect-provider \
    --url https://token.actions.githubusercontent.com \
    --client-id-list sts.amazonaws.com \
    --thumbprint-list $THUMBPRINT

# 验证提供商是否创建成功
aws iam list-open-id-connect-providers

这里的$THUMBPRINT是关键的安全环节,它确保了我们正在与真正的GitHub OIDC端点通信。

1.2 创建用于GitHub Actions的IAM Role

这个角色将是GitHub Actions工作流在AWS中行动的“身份”。其核心是信任策略(Trust Policy),它精确地定义了谁(哪个GitHub仓库、哪个分支)可以代入这个角色。

github_actions_role_trust_policy.json:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
                }
            }
        }
    ]
}

这个策略非常重要:

  • Principal.Federated: 指明了我们信任的OIDC提供商。
  • Condition.StringLike: 这是实现最小权限原则的关键。sub声明包含了仓库和分支信息。这里的配置意味着只有your-org/your-repo仓库的main分支上的工作流才能代入这个角色。你可以使用通配符,例如repo:your-org/*:ref:refs/heads/feature-*来匹配所有feature分支。

接下来,创建角色并附加权限策略。

# 创建IAM Role
aws iam create-role \
    --role-name GitHubActions-EKS-Ray-Deployer \
    --assume-role-policy-document file://github_actions_role_trust_policy.json

# 创建权限策略 (Permissions Policy)
# 这个策略需要赋予操作EKS集群、可能还有EC2、VPC等资源的权限。
# 这是一个示例,生产环境需要更精细化的权限。
cat <<EOF > eks_ray_deployer_permissions.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "eks:DescribeCluster",
                "eks:ListClusters",
                "eks:AccessKubernetesApi"
            ],
            "Resource": "arn:aws:eks:REGION:ACCOUNT_ID:cluster/YOUR_EKS_CLUSTER_NAME"
        },
        {
            "Effect": "Allow",
            "Action": "sts:GetCallerIdentity",
            "Resource": "*"
        }
        // ... 其他部署可能需要的权限
    ]
}
EOF

# 创建策略并附加到角色
POLICY_ARN=$(aws iam create-policy --policy-name EKS-Ray-Deployer-Policy --policy-document file://eks_ray_deployer_permissions.json --query 'Policy.Arn' --output text)

aws iam attach-role-policy \
    --role-name GitHubActions-EKS-Ray-Deployer \
    --policy-arn $POLICY_ARN

至此,AWS端的配置完成。我们已经建立了一个安全的信任通道。

第二阶段:构建自动化的Git驱动部署流水线

现在,我们来构建GitHub Actions工作流。这个工作流将在每次向main分支推送代码时,自动部署或更新EKS上的Ray集群。

.github/workflows/deploy-ray-cluster.yml:

name: Deploy Ray Cluster to EKS

on:
  push:
    branches:
      - main
  workflow_dispatch: # 允许手动触发

permissions:
  id-token: write # 必须,用于向GitHub OIDC提供商请求JWT
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/GitHubActions-EKS-Ray-Deployer
          aws-region: YOUR_AWS_REGION
          # role-session-name 可选,但有助于审计追踪
          role-session-name: GitHubActions-Ray-Deploy-${{ github.sha }}

      - name: Setup kubectl and helm
        run: |
          echo "Setting up kubectl..."
          curl -o kubectl https://s3.us-west-2.amazonaws.com/amazon-eks/1.27.1/2023-04-19/bin/linux/amd64/kubectl
          chmod +x ./kubectl
          sudo mv ./kubectl /usr/local/bin/
          kubectl version --client

          echo "Setting up helm..."
          curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
          chmod 700 get_helm.sh
          ./get_helm.sh
          helm version

      - name: Configure kubectl for EKS
        run: |
          aws eks update-kubeconfig --name YOUR_EKS_CLUSTER_NAME --region YOUR_AWS_REGION
          # 验证是否能连接到集群
          kubectl get nodes

      - name: Deploy KubeRay Operator
        run: |
          # 幂等地安装或升级KubeRay Operator
          helm repo add kuberay https://ray-project.github.io/kuberay-helm/
          helm repo update
          helm upgrade --install kuberay-operator kuberay/kuberay-operator --version 1.0.0 --namespace ray-system --create-namespace

      - name: Deploy Ray Cluster Manifest
        run: |
          # 将RayCluster的定义文件应用到集群
          # 这个YAML文件应该在你的代码仓库中进行版本控制
          kubectl apply -f ./ray-cluster/production.yaml

这个工作流有几个关键点:

  • permissions: id-token: write: 这是授权工作流从GitHub获取OIDC JWT的魔法。没有它,一切都无法工作。
  • aws-actions/configure-aws-credentials: 这个官方Action负责处理JWT交换和设置临时凭证为环境变量,后续的awskubectl命令可以直接使用。
  • 幂等性:使用helm upgrade --installkubectl apply确保了流水线可以重复运行,无论是初次部署还是后续更新,行为都是一致的。

我们的Ray集群定义文件ray-cluster/production.yaml也应该是生产级的:

# ray-cluster/production.yaml
apiVersion: ray.io/v1
kind: RayCluster
metadata:
  name: production-ray-cluster
  namespace: ray-system
spec:
  rayVersion: '2.9.0'
  # Ray head pod
  headGroupSpec:
    rayStartParams:
      dashboard-host: '0.0.0.0'
      # 在生产环境中,启用日志归档和指标非常重要
      metrics-export-port: '8080'
    template:
      spec:
        containers:
        - name: ray-head
          image: rayproject/ray:2.9.0
          ports:
          - containerPort: 6379 # GCS
          - containerPort: 8265 # Dashboard
          - containerPort: 10001 # Client
          - containerPort: 8080 # Metrics
          resources:
            requests:
              cpu: "2"
              memory: "8Gi"
            limits:
              cpu: "4"
              memory: "16Gi"
          # 生产环境中必须配置存活和就绪探针
          livenessProbe:
            httpGet:
              path: /
              port: 8265
            initialDelaySeconds: 10
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /
              port: 8265
            initialDelaySeconds: 5
            periodSeconds: 5

  # Ray worker pods
  workerGroupSpecs:
  - groupName: small-cpu-worker
    replicas: 2
    minReplicas: 1
    maxReplicas: 10 # 启用自动伸缩
    rayStartParams: {}
    template:
      spec:
        containers:
        - name: ray-worker
          image: rayproject/ray:2.9.0
          resources:
            requests:
              cpu: "4"
              memory: "16Gi"
            limits:
              cpu: "4"
              memory: "16Gi"

现在,每当main分支有更新,一个安全的、无凭证的CI/CD流程就会自动将最新的Ray集群配置同步到EKS。机器身份的问题已经解决。

第三阶段:为人类身份建立信任 - SAML与EKS的集成

接下来是更复杂的部分:如何让我们的工程师通过他们的公司账户(例如Okta, Azure AD)安全地访问这个EKS集群。我们需要将SAML IdP与AWS IAM集成,然后将IAM身份映射到Kubernetes的RBAC角色。

流程如下:

  1. 工程师执行kubectl命令。
  2. aws-iam-authenticator(由aws cli在后台调用)发现需要认证,将用户重定向到公司SAML IdP的登录页面。
  3. 用户完成SSO登录。IdP向浏览器返回一个SAML断言。
  4. 浏览器将SAML断言POST到AWS登录端点。
  5. AWS STS验证SAML断言,并代入一个预先配置好的IAM Role,返回临时AWS凭证给aws-iam-authenticator
  6. aws-iam-authenticator使用这些临时凭证生成一个Kubernetes令牌,并将其交给kubectl
  7. kubectl向EKS API Server出示令牌。
  8. EKS API Server将令牌交回给AWS进行验证,确认其有效性,并获取该令牌对应的IAM身份(ARN)。
  9. EKS通过aws-auth ConfigMap查找该IAM ARN被映射到了哪个Kubernetes用户或组。
  10. 基于Kubernetes的RBAC策略,EKS判断该用户/组是否有权限执行请求的操作。

3.1 配置AWS IAM与SAML IdP

这一步高度依赖于你使用的IdP,但通用步骤是:

  1. 在你的IdP(如Okta)中,创建一个新的SAML 2.0应用,集成AWS。
  2. 从AWS IAM控制台下载联邦元数据XML文件,并上传到IdP应用配置中。
  3. 在AWS IAM中,创建一个SAML身份提供商,并上传从IdP下载的元数据XML文件。
  4. 创建一个专供SAML用户代入的IAM Role。其信任策略将指向SAML提供商。

saml_user_role_trust_policy.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:saml-provider/YOUR_SAML_PROVIDER_NAME"
      },
      "Action": "sts:AssumeRoleWithSAML",
      "Condition": {
        "StringEquals": {
          "SAML:aud": "https://signin.aws.amazon.com/saml"
        }
      }
    }
  ]
}

创建角色:

aws iam create-role \
    --role-name EKS-SAML-User-Access \
    --assume-role-policy-document file://saml_user_role_trust_policy.json

注意:这个角色不需要附加任何AWS权限策略。它的唯一目的就是作为AWS和Kubernetes之间的身份桥梁。对集群资源的访问权限将完全由Kubernetes RBAC控制。这是一个常见的错误认知,即试图在这个IAM Role上附加EKS相关的权限。

3.2 映射IAM Role到Kubernetes RBAC

EKS通过一个名为aws-auth的特殊ConfigMap来管理IAM身份到Kubernetes身份的映射。这个ConfigMap位于kube-system命名空间。

# 获取现有的aws-auth ConfigMap
kubectl get configmap aws-auth -n kube-system -o yaml > aws-auth.yaml

编辑aws-auth.yaml文件,在mapRoles部分添加我们的SAML角色映射。

aws-auth.yaml (片段):

apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    - rolearn: arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/EKS-SAML-User-Access
      username: "{{SessionName}}"
      groups:
        - ray-cluster-users
    # ... 其他已存在的角色映射,例如EKS节点组的角色
  • rolearn: 我们在上一步创建的SAML角色的ARN。
  • username: "{{SessionName}}": 这是一个模板变量。当用户通过SAML登录时,他们的IdP用户名(例如[email protected])会被作为SessionName传递。这确保了每个用户在Kubernetes中都有一个唯一的身份。
  • groups: 这是最关键的部分。我们将所有通过SAML登录的用户都分配到了ray-cluster-users这个Kubernetes组。

现在,我们可以创建一个ClusterRoleClusterRoleBinding来赋予这个组具体的权限。

ray-user-rbac.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ray-user-role
rules:
- apiGroups: ["", "ray.io"]
  resources: ["pods", "pods/log", "services", "rayclusters", "rayjobs"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["pods/portforward"]
  verbs: ["create"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ray-user-binding
subjects:
- kind: Group
  name: ray-cluster-users # 绑定到我们在aws-auth中定义的组
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: ray-user-role
  apiGroup: rbac.authorization.k8s.io

这个RBAC策略赋予了ray-cluster-users组的成员查看Ray相关资源、获取日志以及执行port-forward(用于访问Dashboard)的权限,但禁止他们修改或删除任何东西。

应用这些配置:

# 更新aws-auth ConfigMap
kubectl apply -f aws-auth.yaml

# 创建RBAC规则
kubectl apply -f ray-user-rbac.yaml

3.3 工程师的本地kubeconfig配置

最后一步是告诉工程师如何配置他们的本地环境。他们的kubeconfig文件不再包含任何密钥,而是定义了一个执行器。

~/.kube/config (片段):

# ... cluster和context定义 ...
users:
- name: saml-user
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      command: aws
      args:
        - "eks"
        - "get-token"
        - "--cluster-name"
        - "YOUR_EKS_CLUSTER_NAME"
        # --role-arn 是可选的,但建议显式指定以避免混淆
        # - "--role-arn"
        # - "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/EKS-SAML-User-Access"
      env:
        - name: "AWS_PROFILE"
          value: "saml" # 假设用户的AWS CLI配置了一个用于SAML登录的profile

当工程师运行任何kubectl命令时,exec部分会被触发,aws eks get-token命令会启动SAML登录流程,获取临时凭证,并最终为kubectl提供一个有效的令牌。

整体架构与流程的可视化

为了更清晰地展示这两个并行的认证流程,我们可以用Mermaid图来描绘它们。

CI/CD OIDC认证流程:

sequenceDiagram
    participant GH as GitHub Actions Runner
    participant GHOIDC as GitHub OIDC Provider
    participant AWS_STS as AWS STS
    participant EKS as EKS API Server

    GH->>GHOIDC: 请求JWT (携带仓库/分支信息)
    GHOIDC-->>GH: 返回JWT

    GH->>AWS_STS: 调用AssumeRoleWithWebIdentity(JWT)
    Note right of AWS_STS: 验证JWT签名和声明
(repo: your-org/your-repo:ref:refs/heads/main) AWS_STS-->>GH: 颁发临时AWS凭证 GH->>EKS: 使用临时凭证执行kubectl/helm命令 EKS-->>GH: 操作成功

人类用户SAML认证流程:

sequenceDiagram
    participant User as 工程师
    participant CLI as kubectl / aws-iam-authenticator
    participant IdP as SAML IdP
    participant AWS_STS as AWS STS
    participant EKS as EKS API Server
    participant AuthMap as aws-auth ConfigMap

    User->>CLI: 执行 kubectl get pods
    CLI->>IdP: 重定向用户进行SSO登录 (通过浏览器)
    IdP-->>CLI: 返回SAML断言
    CLI->>AWS_STS: 调用AssumeRoleWithSAML(SAML断言)
    AWS_STS-->>CLI: 颁发临时AWS凭证

    CLI->>EKS: 使用凭证生成并出示K8s令牌
    EKS->>AWS_STS: 验证令牌,获取IAM Role ARN
    AWS_STS-->>EKS: 令牌有效,返回ARN: EKS-SAML-User-Access
    
    EKS->>AuthMap: 查找ARN映射
    AuthMap-->>EKS: 映射到K8s组: ray-cluster-users
    Note right of EKS: 基于RBAC检查 'ray-cluster-users' 组
是否有 'get pods' 权限 EKS-->>User: 返回pods列表

当前方案的局限性与未来展望

我们成功地构建了一个健壮、安全、可审计的自动化部署与访问系统。它消除了长期密钥,并将身份验证的责任委托给了专门的身份提供商。然而,这个方案并非没有改进空间。

aws-auth ConfigMap的管理是一个潜在的痛点。随着角色和用户的增多,这个单一文件会变得臃肿且难以维护,容易因手动编辑而出错。未来的一个迭代方向是探索使用AWS IAM Roles for Service Accounts (IRSA)来更精细化地管理Pod级别的权限,或者引入像Teleport这样的身份感知访问代理,将Kubernetes的认证与授权从aws-auth中解耦出来,实现更动态和基于角色的访问控制。

对于Ray Dashboard的访问,当前的RBAC配置允许用户通过kubectl port-forward来访问,这在安全和易用性上都不是最优解。更优雅的方案是在EKS中部署一个OAuth2-proxy,将其置于Dashboard Service的前面,并将它与我们的SAML IdP集成,从而为Web界面提供真正的SSO体验。

最后,当前的流水线专注于部署。一个完整的GitOps循环还应包括环境清理、预览环境的动态创建与销毁,以及更复杂的发布策略(如金丝雀发布)。这些都可以基于当前这个坚实的身份认证基础之上,作为后续平台工程建设的一部分逐步完善。


  目录