ACK RRSA 跨账号免密访问

2026/11/09 kubernetes

ACK RRSA 跨账号免密认证流程:换取角色 A 凭证、访问账号 A 资源、链式扮演角色 B、访问账号 B 资源

Pod 里要读阿里云的资源,比如 SLS 日志,最省事的做法是把 AccessKey 塞进 Secret。但 AK 是长期有效的,泄露了很难察觉,跨账号的时候更麻烦:每个账号都要维护一份。

ACK 提供的 RRSA(RAM Roles for Service Accounts)可以解决这个问题:Pod 不保存任何 AK,只靠 Kubernetes 签发的 OIDC Token 换取临时凭证。

RRSA 是怎么工作的?

开启 RRSA 后,集群会成为一个 OIDC Provider。绑定了 RAM 角色的 ServiceAccount,它对应的 Pod 会被注入 OIDC Token 文件,以及角色和 Provider 的 ARN(通常是 ALIBABA_CLOUD_ROLE_ARN、ALIBABA_CLOUD_OIDC_PROVIDER_ARN、ALIBABA_CLOUD_OIDC_TOKEN_FILE 这几个环境变量)。

SDK 读到这些信息后,拿 Token 去调用 STS 的 AssumeRoleWithOIDC,换回一份有时效的 STS 凭证。整个过程应用代码基本无感。

第一步:配置 Role A 并投射 OIDC Token

跨账号之前,先要保证 Pod 能访问本账号的资源。这需要配三样东西:Role A 的信任策略、Role A 的权限策略,以及 Deployment 里的 Token 投射。

Role A 需要信任什么?

Role A 要信任集群的 OIDC Provider,并用 Condition 限定到具体的 ServiceAccount。不加 oidc:sub 的话,集群里任何 ServiceAccount 都能扮演这个角色。

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Principal": {
        "Federated": ["acs:ram::<账号A-UID>:oidc-provider/ack-rrsa-<cluster-id>"]
      },
      "Condition": {
        "StringEquals": {
          "oidc:aud": "sts.aliyuncs.com",
          "oidc:iss": "<集群的 OIDC Issuer URL>",
          "oidc:sub": "system:serviceaccount:<namespace>:<serviceaccount>"
        }
      }
    }
  ]
}

Provider 的 ARN 和 Issuer URL 可以在 ACK 控制台开启 RRSA 之后看到。

Role A 需要什么权限?

Role A 本身要有读取本账号 SLS 的权限,同时要允许它去扮演账号 B 的 Role B:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["log:GetProject", "log:GetLogStore", "log:GetLogStoreLogs", "log:ListLogStores"],
      "Resource": "acs:log:*:<账号A-UID>:project/<project>/*"
    },
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "acs:ram::<账号B-UID>:role/<role-b>"
    }
  ]
}

上面的 SLS Action 只是读日志的最小示例,按实际需要增减。

Deployment 如何投射 OIDC Token?

Token 是通过 Kubernetes 的 projected volume 投射的,audience 必须和信任策略里的 oidc:aud 一致:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sls-reader
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sls-reader
  template:
    metadata:
      labels:
        app: sls-reader
    spec:
      serviceAccountName: sls-reader
      containers:
        - name: app
          image: <your-image>
          env:
            - name: ALIBABA_CLOUD_ROLE_ARN
              value: acs:ram::<账号A-UID>:role/<role-a>
            - name: ALIBABA_CLOUD_OIDC_PROVIDER_ARN
              value: acs:ram::<账号A-UID>:oidc-provider/ack-rrsa-<cluster-id>
            - name: ALIBABA_CLOUD_OIDC_TOKEN_FILE
              value: /var/run/secrets/ack.alibabacloud.com/rrsa-tokens/token
          volumeMounts:
            - name: rrsa-token
              mountPath: /var/run/secrets/ack.alibabacloud.com/rrsa-tokens
              readOnly: true
      volumes:
        - name: rrsa-token
          projected:
            sources:
              - serviceAccountToken:
                  audience: sts.aliyuncs.com
                  expirationSeconds: 3600
                  path: token

serviceAccountName 要和信任策略里 oidc:sub 的 ServiceAccount 对上。ACK 也提供了 ack-pod-identity-webhook,可以按命名空间标签和 ServiceAccount 注解自动注入上面的环境变量和 Volume,不用每个 Deployment 手写。具体用法见 官方文档。

Pod 里的 SDK 读到这三个变量后,就会自动完成 AssumeRoleWithOIDC,拿到 Role A 的临时凭证。

第二步:配置 Role B,跨账号访问账号 B 的 SLS

Pod 拿到 Role A 的凭证以后,访问账号 B 要再扮演一次角色,也就是“链式扮演角色”:

  1. Pod 读取 OIDC Token,调用 STS 换取账号 A 下 Role A 的临时凭证。
  2. 用 Role A 的凭证访问账号 A 自己的 SLS。
  3. 用 Role A 的凭证再请求 STS,扮演账号 B 下的 Role B,拿到 Role B 的临时凭证。
  4. 切换为 Role B 的凭证,读取账号 B 的 SLS。

前两步已经在第一步里配好了。第 3 步要成功,需要两边同时放行:Role A 一侧允许去扮演 Role B,Role B 一侧允许被 Role A 扮演。

Role A 一侧

Role A 的权限策略里要有对 Role B 的 sts:AssumeRole,这条在第一步已经加好了:

{
  "Effect": "Allow",
  "Action": "sts:AssumeRole",
  "Resource": "acs:ram::<账号B-UID>:role/<role-b>"
}

Role B 需要信任什么?

在账号 B 里创建 Role B,信任策略只信任账号 A 的 Role A,不要信任整个账号 A:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Principal": {
        "RAM": ["acs:ram::<账号A-UID>:role/<role-a>"]
      }
    }
  ]
}

如果 Principal 写成 acs:ram::<账号A-UID>:root,账号 A 下被授权了 sts:AssumeRole 的任何用户或角色都能扮演 Role B,范围比实际需要大得多。

Role B 需要什么权限?

Role B 的权限策略和第一步类似,只是 Resource 换成账号 B 的 SLS Project:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["log:GetProject", "log:GetLogStore", "log:GetLogStoreLogs", "log:ListLogStores"],
      "Resource": "acs:log:*:<账号B-UID>:project/<project>/*"
    }
  ]
}

账号 B 这边不需要知道集群、OIDC Provider 或 ServiceAccount 的存在,只和 Role A 打交道。

应用侧要做什么?

Deployment 不用再改。应用拿到 Role A 的凭证后,用它请求 STS 的 AssumeRole,参数里的 RoleArn 填 Role B 的 ARN,返回的就是 Role B 的临时凭证。这一步需要在应用里显式调用,阿里云 Credentials 库默认的凭证链只会处理第一次 OIDC 换取。

为什么这样设计?

  • 没有长期密钥:全程使用短时效的 STS 凭证,不需要保存明文 AK。
  • 身份隔离到 ServiceAccount:凭证绑定的是 ServiceAccount 和命名空间,而不是整个集群或节点,一个业务拿不到另一个业务的权限。
  • 职责清楚:账号 B 只需要信任 Role A,不关心集群里有多少 Pod、怎么部署,跨账号的配置和集群本身是解耦的。

总结

RRSA 把“Pod 是谁”交给 OIDC Token 证明,把“能做什么”交给 RAM 角色的策略。跨账号时再多套一层 Role B,就能做到全程没有 AK。

TODO:上面的策略和 YAML 是按文档整理的模板,尚未在实际集群验证。应用侧调用 AssumeRole 的具体写法也待补充。