ACK通过ack-kms-agent-webhook-injector组件快速集成KMS Agent

本文介绍阿里云ACK如何通过ack-kms-agent-webhook-injector组件集成KMS Agent。

概述

架构说明

阿里云容器服务 Kubernetes 版 ACK(Container Service for Kubernetes)提供了ack-kms-agent-webhook-injector组件,通过配置特定注解(kms-agent-webhook-injector/inject),将KMS Agent作为Sidecar容器注入Pod,从而允许容器中的应用通过本地HTTP接口,借助KMS AgentKMS实例获取凭据并缓存在内存中,避免敏感信息硬编码,保障数据安全。此外,KMS Agent 的缓存机制,能提升高并发及网络不稳定场景下访问KMS的稳定性,优化使用体验。架构图如下所示:

image

使用限制

  • ACK集群类型限制:支持ACK托管与专有集群ACK Serverless集群

  • 地域限制:ACK集群与KMS实例需要在同一地域。

  • 性能限制:由于每个Pod独立运行KMS Agent Sidercar容器,若业务部署大量Pod,在身份验证过程中如果STS Token请求每分钟超过500次则会触发限流,进而对KMS Agent的正常工作产生影响。

费用说明

  • KMS侧的费用:

    使用KMS Agent前您需要购买KMS实例,计费方式为包年包月,使用KMS Agent本身不会额外收取您的费用。详细介绍,请参见产品计费

  • ACK侧的费用:

    ack-kms-agent-webhook-injector组件本身是完全免费的,但使用该组件的过程中,可能会产生额外的费用。

    • 安装ack-kms-agent-webhook-injector组件后,会生成一个Webhook服务工作负载,该负载将占用一定计算资源并产生费用,您可以在配置文件中,对该负载的 CPU 和内存使用进行限制。

    • 当您创建或更新符合条件的工作负载时,ack-kms-agent-webhook-injector将会把KMS Agent以 Sidecar的形式注入到您的容器中,KMS Agent会使用一定计算资源并产生费用。

身份认证方式

支持RRSAWorker RAM角色两种认证方式。

  • (推荐)方式一:RRSA方式

    可以实现Pod维度的权限隔离。适用于1.22及以上版本的ACK托管集群ACK Serverless集群

    重要

    实现Pod维度的权限隔离需要ack-kms-agent-webhook-injector的版本大于等于v0.2.0。

    例如,通过RRSA方式,您可以让部署在app1-dev中的应用,使用app1-rrsa这个角色,访问KMS中带有secret:app1标签的凭据,。部署在app2-dev中的应用,使用app2-rrsa这个角色,访问KMS中带有secret:app2标签的凭据。

image
  • 方式二:Worker RAM角色方式

    可以实现Worker维度的权限隔离。由于ACK Serverless集群不支持绑定Worker RAM角色,该方式只适用于ACK托管集群ACK专有集群

    例如,您可以通过Worker RAM角色,设置app1-devapp2-dev中的应用访问KMS中管理的凭据。同一个Worker里的应用使用同一个Worker RAM角色,即您无法针对app1-devapp2-dev设置不同的权限。

image

前提条件

步骤一:配置认证信息,使KMS Agent可以访问特定的凭据

方式一:RRSA方式授权

以您有两个应用app1app2为例,app1需要访问KMS实例中打标签secret: app1的凭据,app2需要访问KMS实例中打标签secret: app2的凭据。

  • app1NameSpaceapp1-dev,serviceAccountNameapp1-service,RAM角色为app1-rrsa。

  • app2NameSpaceapp2-dev,serviceAccountNameapp2-service,RAM角色为app2-rrsa。

  1. 启用ACK RRSA功能。

    创建集群时开启

    创建ACK托管集群ACK Edge集群时,您可以在集群配置的高级选项(选填)区域,选中开启RRSA功能。

    image

    在集群信息页面开启

    1. 登录容器服务管理控制台,在左侧导航栏选择集群列表

    2. 集群列表页面,单击目标集群名称,然后在左侧导航栏,选择集群信息

    3. 基本信息页签的安全与审计区域,单击RRSA OIDC右侧的开启image

    4. 在弹出的启用RRSA对话框,单击确定

      基本信息区域,当集群状态由更新中变为运行中后,表明该集群的RRSA特性已变更完成。

  2. 打开集群详情页,在基本信息页签的安全与审计区域,将鼠标悬浮至RRSA OIDC右侧已开启上面,查看提供商的URL链接和ARN信息。image

  3. app1创建一个可信实体为身份提供商RAM角色,并授权其可以访问标签为secret: app1的凭据。具体操作,请参见创建可信实体为身份提供商的RAM角色

    1. 登录RAM控制台

    2. 在左侧导航栏,选择身份管理 > 角色,然后在角色页面,单击创建角色

    3. 创建角色面板,选择可信实体类型为身份提供商,并单击切换编辑器image

    4. 创建角色页面的可视化编辑,配置如下角色信息后,单击确定

      配置项

      描述

      效果

      选择允许

      主体

      选择身份提供商。

      • 身份提供商类型:选择OIDC。

      • 身份提供商:开启RRSA后,ACK集群会默认创建身份提供商,命名格式为ack-rrsa-<cluster_id>。其中,<cluster_id>为您的集群ID。

      操作

      保持默认。即勾选sts:AssumeRole。

      条件

      在默认的oidc:issoidc:aud限制条件基础上,新增一个限制条件:

      • 条件键:选择oidc:sub

      • 运算符:选择StringEquals

      • 条件值:system:serviceaccount:<namespace>:<serviceAccountName>。本文示例为system:serviceaccount:app1-dev:app1-service

        • <namespace>:希望注入KMS Agent的应用所在的命名空间。

        • <serviceAccountName>:希望注入KMS Agent的应用所使用的服务账户名称。服务账户名称为Pod提供身份标识,可以通过RRSA机制与RAM角色动态绑定。

        说明

        若您需要为多个不同命名空间中的不同服务账户配置,您可以配置多个值。

    5. 创建角色对话框中,设置角色名称,然后单击确定。本文角色名称以app1-rrsa为例。

    6. (可选)查看app1-rrsa这个RAM角色的信任策略。

      信任策略表示允许服务账户app1-service通过阿里云RRSA(RAM Roles for Service Accounts) ,在满足OIDC身份验证条件后,担任某个RAM角色。image

    7. 创建权限策略。

      权限策略名称以app1-rrsa-kms-policy为例,策略内容为访问标签为secret: app1的凭据。image

      {
          "Version": "1",
          "Statement": [
              {
                  "Effect": "Allow",
                  "Action": [
                      "kms:Decrypt",
                      "kms:GetSecretValue"
                  ],
                  "Resource": "*",
                  "Condition": {
                      "StringEqualsIgnoreCase": {
                          "kms:tag/secret": [
                              "app1"
                          ]
                      }
                  }
              }
          ]
      }
    8. app1-rrsa-kms-policy权限策略,授权给app1-rrsa角色。具体操作,请参见RAM角色授权image

  4. app2创建一个可信实体为身份提供商RAM角色,并授权其可以访问标签为secret: app1的凭据。具体操作,请参见创建可信实体为身份提供商的RAM角色

    1. 登录RAM控制台

    2. 在左侧导航栏,选择身份管理 > 角色,然后在角色页面,单击创建角色

    3. 创建角色面板,选择可信实体类型为身份提供商,并单击切换编辑器

    4. 创建角色页面的可视化编辑,配置如下角色信息后,单击确定

      配置项

      描述

      效果

      选择允许

      主体

      选择身份提供商。

      • 身份提供商类型:选择OIDC。

      • 身份提供商:开启RRSA后,ACK集群会默认创建身份提供商,命名格式为ack-rrsa-<cluster_id>。其中,<cluster_id>为您的集群ID。

      操作

      保持默认。即勾选sts:AssumeRole。

      条件

      在默认的oidc:issoidc:aud限制条件基础上,新增一个限制条件:

      • 条件键:选择oidc:sub

      • 运算符:选择StringEquals

      • 条件值:system:serviceaccount:<namespace>:<serviceAccountName>。本文示例为system:serviceaccount:app2-dev:app2-service

        • <namespace>:希望注入KMS Agent的应用所在的命名空间。

        • <serviceAccountName>:希望注入KMS Agent的应用所使用的服务账户名称。服务账户名称为Pod提供身份标识,可以通过RRSA机制与RAM角色动态绑定。

        说明

        若您需要为多个不同命名空间中的不同服务账户配置,您可以配置多个值。

    5. 创建角色对话框中,设置角色名称,然后单击确定。本文角色名称以app2-rrsa为例。

    6. (可选)查看app2-rrsa这个RAM角色的信任策略。

      信任策略表示允许服务账户app2-service通过阿里云RRSA(RAM Roles for Service Accounts) ,在满足OIDC身份验证条件后,担任某个RAM角色。

    7. 创建权限策略。

      权限策略名称以app2-rrsa-kms-policy为例,策略内容为访问标签为secret: app2的凭据。

      {
          "Version": "1",
          "Statement": [
              {
                  "Effect": "Allow",
                  "Action": [
                      "kms:Decrypt",
                      "kms:GetSecretValue"
                  ],
                  "Resource": "*",
                  "Condition": {
                      "StringEqualsIgnoreCase": {
                          "kms:tag/secret": [
                              "app2"
                          ]
                      }
                  }
              }
          ]
      }
    8. app2-rrsa-kms-policy权限策略,授权给app2-rrsa角色。具体操作,请参见RAM角色授权

方式二:Worker RAM角色方式授权

Worker RAM角色属于普通服务角色,您可以为Worker节点池指定默认角色自定义角色

  • 默认角色:ACK托管集群会自动创建一个所有节点共享的默认Worker RAM角色。当您通过默认的Worker RAM角色授权时,权限将会共享给集群内所有的节点,可能会存在非预期的权限扩散的风险。

  • (推荐)自定义角色:需要您提前在访问控制台创建一个普通服务角色,并为节点池指定这个角色。通过为不同的节点池分配特定的角色,可以将每个节点池的权限隔离开,降低集群内所有节点共享相同权限的风险。本文以使用自定义角色为例。

  1. 创建普通服务角色。角色名称以ack-secret-manager为例。

    1. 登录RAM控制台,在左侧导航栏,选择身份管理 > 角色

    2. 角色页面,单击创建角色

    3. 创建角色页面,选择信任主体类型云服务信任主体名称请选择容器服务Kubernetes,最后单击确定image

    4. 创建角色对话框,输入角色名称,单击确定

  2. 创建如下自定义权限策略。策略名称以ack-secret-manager-policy为例。具体操作,请参见创建自定义权限策略image

    {
      "Version": "1",
      "Statement": [
        {
          "Action": [
            "kms:GetSecretValue",
            "kms:Decrypt"
          ],
          "Resource": [
            "*"
          ],
          "Effect": "Allow"
        }
      ]
    }
  3. 将自定义权限策略ack-secret-manager-policy,授权给自定义角色ack-secret-manager。image

  4. 创建节点池时,为节点池指定Worker RAM角色。

    Worker RAM角色选择自定义,然后选择ack-secret-managerimage

    创建集群时同步创建节点池,详细介绍,请参见创建ACK托管集群创建ACK Serverless集群。为已有集群创建节点池,请参见创建和管理节点池

步骤二:在ACK集群中创建NamespaceService Account

NamespaceACK集群划分为逻辑隔离的虚拟空间,用于区分开发、测试、生产等环境,不同Namespace中的应用默认无法互访资源,为RAM角色绑定提供物理边界。

分别创建两个Namespace,app1-devapp2-dev。对应的serviceAccountName分别为app1-serviceapp2-service。

  1. 通过YAML文件创建名为app1-devNamespace。

    1. 创建YAML,文件名称以app1-namespace.yaml为例。

      apiVersion: v1
      kind: Namespace
      metadata:
        name: app1-dev
    2. 执行如下命令在ACK集群中创建名为 app1-dev 的 Namespace。

      kubectl apply -f app1-namespace.yaml
    3. 查看Namespace是否创建成功。

      kubectl get namespaces

      若输出包含app1-dev,即代表创建成功。

  2. 通过YAML文件创建名为app1-serviceService Account。

    1. 创建YAML,文件名称以app1-serviceaccount.yaml为例。

      apiVersion: v1
      kind: ServiceAccount
      metadata:
        name: app1-service
        namespace: app1-dev
    2. 执行如下命令在 app1-dev 中创建一个名为 app1-service 的ServiceAccount。

      kubectl apply -f app1-serviceaccount.yaml
    3. 查看 ServiceAccount 是否创建成功。

      kubectl get serviceaccount -n app1-dev

      若输出包含 app1-service,即代表创建成功。

  3. 重复上述操作,创建命名空间app2-dev和服务账户app2-service。

步骤三:在ACK集群中安装ack-kms-agent-webhook-injector组件

  1. 登录容器服务管理控制台,在左侧导航栏选择集群列表

  2. 集群列表页面,单击目标集群名称,然后在左侧导航栏,选择应用 > Helm

  3. Helm页面,单击创建,配置基本信息,单击下一步

    配置项

    说明

    应用名

    请输入您的应用名。建议您使用默认应用名,此处无需输入,单击下一步后会提示使用默认应用名ack-kms-agent-webhook-injector,选择是即可。

    命名空间

    选择应用所在的命名空间。建议您使用默认命名空间,此处无需选择保持默认,单击下一步后会提示使用默认命名空间kube-system,选择是即可。

    一个ACK集群中安装到一个命名空间即可,无需重复安装。

    来源

    默认为应用市场,不支持修改。

    Chart

    搜索并选中ack-kms-agent-webhook-injector。

  4. 在弹出的对话框中确认无误后,单击

    选择是即使用默认安装路径,组件默认安装在kube-system命名空间中,并以组件名称发布应用。image

  5. 参数配置页面,完成各项配置,然后单击确定

    配置项

    说明

    Chart 版本

    建议您选择最新版本。

    参数

    agent.auth.roleArnagent.auth.roleArnMapping参数配置说明:

    • 采用RRSA方式授权时:agent.auth.roleArn为空,agent.auth.roleArnMapping格式为<NameSpace>:<serviceAccountName>:<RAM Role ARN>

      agent:
        auth:
          roleArn: 
          roleArnMapping:
            app1-dev:app1-service: acs:ram::190325303126****:role/app1-rrsa
            app2-dev:app2-service: acs:ram::190325303126****:role/app2-rrsa
    • 采用Worker RAM角色方式授权:agent.auth.roleArnagent.auth.roleArnMapping均设置为空。

    其余参数如何配置,请参见组件详情。image

  6. 请等待约30 秒,进入命名空间(默认为kube-system),在无状态工作负载中检查组件状态,确认组件是否已经就绪。

步骤四:为工作负载注入KMS Agent

通过配置注解将KMS Agent注入Pod,根据您是为新建工作负载的Pod还是为现存工作负载的Pod进行配置,具体操作不同。

以无状态工作负载Deployment为例。本文示例中,请为app1-devapp2-dev分别执行如下操作,进行设置。

  1. 登录容器服务管理控制台

  2. 进入到目标集群,在左侧导航栏选择工作负载 > 无状态

  3. 创建或修改工作负载时,为添加如下Pod注解:名称kms-agent-webhook-injector/injecttrue(除此之外,填入值1,T,t,True,TRUE亦可视为有效值)。

    场景一:通过镜像新建工作负载

    单击使用镜像创建,完成各项配置,单击创建

    填写高级配置时,请在标签与注解区域,添加如下Pod注解:名称填入kms-agent-webhook-injector/inject填入true。image

    场景二:通过 YAML 新建工作负载

    单击使用YAML创建资源,完成各项配置,单击创建

    编辑 YAML 文件中的 spec.template.metadata.annotations 一项(若不存在,您需要自主创建),向其中添加键值 kms-agent-webhook-injector/inject: "true"。其他参数如何配置,请参见工作负载YAML示例image.png

    场景三:现存工作负载

    1. 定位到目标工作负载,单击操作列的详情

    2. 在工作负载详情页,单击右上角的YAML编辑按钮,寻找到 spec.template.metadata.annotations 一项(若不存在,您需要自主创建),向其中添加键值kms-agent-webhook-injector/inject: "true"image.png

    3. 单击更新,等待工作负载重新就绪。

  4. 返回无状态页面,进入工作负载,可以观察到容器组页签下各个容器,在镜像列即可看到 KMS Agent 已经作为 Sidecar被注入到您的工作负载中。

    重要

    容器组页签下查看镜像时,您可能注意到Pod被注入了两次KMS Agent镜像,这是因为我们使用了初始化容器(initContainer)来进行必要的初始化工作,该初始化容器在初始化完成后就会终止(Terminated),不会对您的应用产生负面影响,也不会持续占用您的计算资源。

    image

  5. 如果您使用RRSA方式授权,请修改YAML配置文件,将serviceAccountName参数改为步骤一中授权的服务账户。

    本文示例为app1-serviceapp2-service。修改后该应用和KMS Agent能被正确授权访问KMS凭据。

    说明

    Worker RAM角色方式授权时无需修改配置文件,服务账户可自动扮演Worker RAM角色获取权限。

步骤五:在应用容器中获取 KMS 凭据

当工作负载注入了KMS Agent后,您可在应用容器中,通过HTTP协议向KMS Agent发起请求,获取存储在KMS中的凭据。

示例说明:

  • 本地主机HTTP端口号取默认值2025,如果您设置为其他端口号,请将示例中的2025修改为实际使用端口号。

  • token路径取默认路径file:///var/run/kmstoken/token,如果您设置其他路径,例如file:///var/run/path1/path2,请将示例中的/var/run/kmstoken/token替换为/var/run/path1/path2

KMS Agent默认获取凭据的ACSCurrent 版本。要获取其他版本的凭据值,您可以设置 versionStage 或 versionId

重要

KMS Agent只监听127.0.0.1,即仅允许同一台机器上的应用或进程与其通信,外部网络设备无法连接。访问地址仅支持localhost127.0.0.1,不支持改为应用的本地IP。以下示例以localhost为例。

使用curl

实际使用时请将示例代码中的<SecretId>替换为您实际的凭据名称。

 # 从文件读取 token
 curl -v -H "X-KMS-Token:$(</var/run/kmstoken/token)" 'http://localhost:2025/secretsmanager/get?secretId=<SecretId>'
 
 # 直接写 token
 curl -v -H "X-KMS-Token:<token>" 'http://localhost:2025/secretsmanager/get?secretId=<SecretId>'

您可以指定versionStage 或 versionId以获取特定凭据值。以获取指定versionId的凭据值为例,使用时请将0a7513ee719da740807b15b77500****替换为您实际的凭据版本。

 # 从文件读取 token
 curl -v -H "X-KMS-Token:$(</var/run/kmstoken/token)" 'http://localhost:2025/secretsmanager/get?secretId=<SecretId>&versionId=0a7513ee719da740807b15b77500****'
 
 # 直接写 token
 curl -v -H "X-KMS-Token:<token>" 'http://localhost:2025/secretsmanager/get?secretId=<SecretId>&versionId=0a7513ee719da740807b15b77500****'

Go代码示例

使用时请将示例代码中的agent-test替换为您实际的凭据名称。

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	
	//支持指定versionStage或versionId以获取特定凭据值。
	//以获取指定versionId的凭据值为例,url := fmt.Sprintf("http://localhost:2025/secretsmanager/get?secretId=%s&versionId=%s", "agent-test", "version-id")。
	url := fmt.Sprintf("http://localhost:2025/secretsmanager/get?secretId=%s", "agent-test")

	token, err := ioutil.ReadFile("/var/run/kmstoken/token")
	if err != nil {
		fmt.Printf("error reading token file: %v\n", err)
	}

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		fmt.Printf("error creating request: %v\n", err)
	}

	req.Header.Add("X-KMS-Token", string(token))

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("error sending request: %v \n", err)
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Printf("status code %d - %s \n", resp.StatusCode, string(body))
}