KMS支持您在ACK集群中以Sidecar形式部署KMS Agent,Agent通过RRSA机制向KMS获取凭据值,业务应用通过本地接口向Agent获取KMS凭据。该方式无需集成SDK,可以降低应用改造成本,确保统一的集成标准,适用于大规模应用访问KMS的场景。本文介绍如何在ACK容器环境中部署KMS Agent获取凭据。
前置概念
在您了解本最佳实践前,请先了解以下内容:
- 什么是KMS Agent:KMS Agent是一个HTTP代理服务,负责获取KMS服务中的凭据值并缓存在内存中,应用通过HTTP请求向KMS Agent获取凭据值。 
- 通过RRSA配置ServiceAccount的RAM权限实现Pod权限隔离:如果您的应用程序部署在阿里云ACK容器集群上,则可以基于RRSA(RAM Roles for Service Accounts)功能,在容器集群内实现应用隔离的RAM角色功能,各个应用可以扮演独立的RAM角色,访问阿里云OpenAPI。 
应用场景
在ACK容器环境中部署KMS Agent获取凭据,使用RRSA作为访问凭证,适用于以下业务场景。
- 企业需要解决last key问题。 - 企业将敏感凭证托管至阿里云KMS以提升安全性,但由于访问KMS本身仍需进行身份认证,若长期依赖固定AccessKey访问云服务,会导致KMS的访问凭证成为新的潜在安全隐患。 - 通过在阿里云ACK集群上开启RRSA功能,基于RRSA可以为不同Pod关联不同的RAM角色,不同Pod内的各个应用可以扮演独立的RAM角色并使用获取的临时凭证访问云资源,从而实现应用RAM权限最小化以及无AccessKey访问阿里云OpenAPI,避免AccessKey泄露。 
- 企业需要隔离不同应用对凭据的访问权限。 - 例如同一应用的测试与正式环境以及不同应用间,都需要进行权限隔离,避免访问权限扩大,凭据被非法获取。 - RRSA基于RAM策略,不同的应用运行在不同的namespace和service account,绑定不同的ramrole,通过为ramrole授予不同的权限,实现不同应用对KMS凭据的访问权限控制。 
- 降低应用与KMS的集成的研发成本。 - 如果只有少数几个应用需要访问KMS,应用可以集成SDK,但对于大中型企业来说,面对成百上千的应用,让每个应用改造去集成SDK,并且都按照一致的标准去设计容灾和缓存是非常困难的。同时由于一般中国内地企业的安全、运维、开发分属不同团队,已经存在的开源external secret方式在中国内地企业也不太适用,其更适合于集开发运维一体的devops模式。 - 此时您可以通过KMS Agent来获取凭据。在ACK容器集群环境中,KMS Agent作为Sidecar容器运行在与业务容器相同的Pod里。当ACK集群启动RRSA后,KMS Agent会自动以RRSA RamRole的身份访问KMS,不需要再设置任何访问凭证。Agent从远端KMS获取凭证并缓存到内存,业务应用再从Agent获取凭据。 
解决方案架构
下图以用户拥有两个应用dev和prod为例,通过ACK容器环境中部署KMS Agent获取凭据,不同环境的应用只能访问各自对应的KMS凭据。
使用限制
- ACK集群支持ACK托管集群、ACK专有集群、ACK注册集群、ACK Serverless集群。 
- ACK集群与KMS实例需要在同一地域。 
操作步骤
以下流程演示了ACK、RAM、KMS如何配置,以实现业务应用通过KMS Agent获取凭据。研发只需要在KMS创建凭据,然后在应用里从本地检索即可,其余配置由运维和安全团队提前配置。整个过程中,研发不需要在业务代码里集成SDK,也不需要关心应用访问KMS的认证鉴权以及缓存容灾等。
步骤一:构建KMS Agent可执行文件
- 安装Golang环境。具体操作,请参见Go安装指南。 
- 下载源码和依赖。 - 请访问Git官网,下载并安装Git工具。 
- 执行以下命令下载源码和依赖。 - git clone https://github.com/aliyun/alibabacloud-kms-agent go mod download
 
- 在项目根目录下,执行 - go build .命令编译可执行文件。文件默认名称alibabacloud-kms-agent,默认保存在项目根目录下 。- 编译环境与部署环境一致,执行 - go build .命令即可。如果编译环境和部署环境不一致,请参考以下命令进行跨平台编译,生成64位可执行文件。- 编译环境 - 部署环境为Mac - 部署环境为Linux - 部署环境为Windows - Mac - go build .- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build .- CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build .- Linux - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build .- go build .- CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build .- Windows - SET CGO_ENABLED=0 SET GOOS=darwin SET GOARCH=amd64 go build .- SET CGO_ENABLED=0 SET GOOS=linux SET GOARCH=amd64 go build .- go build .
- 在项目根目录下,查看是否存在可执行文件alibabacloud-kms-agent。  
步骤二:启用ACK RRSA并授权其访问特定的凭据
- 启用ACK RRSA功能。 - 创建集群时开启- 创建ACK托管集群和ACK Edge集群时,您可以在集群配置的高级选项(选填)区域,选中开启RRSA功能。  - 在集群信息页面开启- 登录容器服务管理控制台,在左侧导航栏选择集群列表。 
- 在集群列表页面,单击目标集群名称,然后在左侧导航栏,选择集群信息。 
- 在基本信息页签的安全与审计区域,单击RRSA OIDC右侧的开启。  
- 在弹出的启用RRSA对话框,单击确定。 - 在基本信息区域,当集群状态由更新中变为运行中后,表明该集群的RRSA特性已变更完成。 
 
- 打开集群详情页,在基本信息页签的安全与审计区域,将鼠标悬浮至RRSA OIDC右侧已开启上面,查看提供商的URL链接和ARN信息。  
- 创建一个可信实体为身份提供商的RAM角色。 - 使用阿里云账号登录RAM控制台。 
- 在左侧导航栏,选择,然后在角色页面,单击创建角色。 
- 在创建角色面板,选择可信实体类型为身份提供商,并单击切换编辑器。  
- 在创建角色页面的可视化编辑,配置如下角色信息后,单击确定。 - 配置项 - 描述 - 效果 - 选择允许。 - 主体 - 选择身份提供者。 - 身份提供者类型:选择OIDC。 
- 身份提供者:开启RRSA后,ACK集群会默认创建身份提供者,命名格式为ack-rrsa-<cluster_id>。其中,<cluster_id>为您的集群ID。 
 - 操作 - 保持默认。即勾选sts:AssumeRole。 - 条件 - 在默认的oidc:iss和oidc:aud限制条件基础上,新增一个限制条件: - 条件键:选择oidc:sub。 
- 运算符:选择StringEquals。 
- 条件值:system:serviceaccount:<namespace>:<serviceAccountName>。 - <namespace>:应用所在的命名空间。 
- <serviceAccountName>:服务账户名称。 
 - 本文以 - system:serviceaccount:rrsa-dev:dev-sa为例, 其中 rsa-dev 是下一步需要创建 k8s namespace,dev-sa 是下一步需要创建的 k8s service account。
 
- 在创建角色对话框中,设置角色名称,然后单击确定。本文角色名称以dev-role-for-rrsa为例。 
- 查看dev-role-for-rrsa这个RAM角色的信任策略。 - 该策略表示允许特定的服务账户通过阿里云RRSA(RAM Roles for Service Accounts) ,在满足 OIDC 身份验证条件后,担任某个RAM角色。 - { "Statement": [ { "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "oidc:aud": [ "sts.aliyuncs.com" ], "oidc:iss": [ "https://oidc-ack-cn-hongkong.oss-cn-hongkong.aliyuncs.com/cf01******" ], "oidc:sub": [ "system:serviceaccount:rrsa-dev:dev-sa" ] } }, "Effect": "Allow", "Principal": { "Federated": [ "acs:ram::5269************:oidc-provider/ack-rrsa-cf01******" ] } } ], "Version": "1" }
 
- 创建权限策略并授权给RAM角色。 - 权限策略名称以dev-role-for-rrsa-kms-policy为例,策略内容以仅允许访问带有 - env:dev标签的凭据为例。 - 权限策略内容如下所示: - { "Version": "1", "Statement": [ { "Effect": "Allow", "Action": [ "kms:Decrypt", "kms:GetSecretValue" ], "Resource": "*", "Condition": { "StringEqualsIgnoreCase": { "kms:tag/env": [ "dev" ] } } } ] }
- 将dev-role-for-rrsa-kms-policy权限策略,授权给dev-role-for-rrsa角色。  
 
步骤三:在ACK中创建Namespace和Service Account
Namespace将ACK集群划分为逻辑隔离的虚拟空间,用于区分开发、测试、生产等环境,不同Namespace中的应用默认无法互访资源,为RAM角色绑定提供物理边界。Service Account为Pod提供身份标识,通过RRSA机制与RAM角色动态绑定。
- 通过YAML文件创建Namespace。 - Namespace名称以rrsa-dev为例,YAML文件名称以namespace.yaml为例。 - apiVersion: v1 kind: Namespace metadata: name: rrsa-dev
- 执行如下命令在ACK集群中创建名为 - rrsa-dev的 Namespace。- kubectl apply -f namespace.yaml- 查看Namespace是否创建成功: - kubectl get namespaces- 若输出包含 - rrsa-dev,即代表创建成功。
- 通过YAML文件创建Service Account。 - Service Account名称以dev-sa为例,YAML文件名称以serviceaccount.yaml为例。 - apiVersion: v1 kind: ServiceAccount metadata: name: dev-sa namespace: rrsa-dev
- 执行如下命令在 - rrsa-dev中创建一个名为- dev-sa的ServiceAccount。- kubectl apply -f serviceaccount.yaml- 查看 ServiceAccount 是否创建成功: - kubectl get serviceaccount -n rrsa-dev- 若输出包含 - dev-sa,即代表创建成功。
步骤四:制作sidecar容器镜像
将KMS Agent作为sidecar容器与应用程序容器一起部署到阿里云的ACK容器服务。
- 在config.toml中配置KMS Agent的运行参数。 说明- config.toml是Agent的配置文件,您可以在步骤一获取的alibabacloud-kms-agent源码的configs目录下查看到该文件。 - [Server] HttpPort = 2025 [KMS] Region = "cn-hangzhou" [Cache] CacheType = "InMemory" CacheSize = 1000 TtlSeconds = 300 [Log] LogLevel = "Debug" LogPath = "./logs/" MaxSize = 100 MaxBackups = 2
- 创建Dockerfile文件。 - 以Agent部署在 - deploy/ack/agent路径下为例,在该路径下创建Dockerfile。文件内容,请参考agent_Dockerfile。- # Use the centos image as the base FROM centos:centos7 # Set the working directory inside the container WORKDIR /usr/local/alibabacloudkmsagent # Copy the binary and config to the container COPY alibabacloud-kms-agent . COPY config.toml . # Set the entry point to run the kms agent ENTRYPOINT ["./alibabacloud-kms-agent", "agent", "./config.toml"]
- 将步骤1和步骤2创建的文件拷贝到deploy/ack/agent目录下。 - deploy/ack/agent ├── alibabacloud-kms-agent ├── config.toml ├── Dockerfile.agent
- 编译sidecar容器镜像并上传到阿里云镜像服务。 - # 编译 docker build -t registry.cn-hangzhou.aliyuncs.com/<ns>/<repo>:kmsagent-v1.0 . # 上传 docker push registry.cn-hangzhou.aliyuncs.com/<ns>/<repo>:kmsagent-v1.0
步骤五:制作业务容器镜像
- 以业务应用部署在 - deploy/ack/app路径下为例,在该路径下编写应用容器镜像的Dockerfile。- # Use the centos image as the base FROM centos:centos7 # Set the working directory inside the container WORKDIR / # Set the entry point to run the kms agent ENTRYPOINT ["sleep", "360000"]
- 编译应用容器镜像,并将镜像上传到阿里云镜像仓库。 - # 编译 docker build -t registry.cn-hangzhou.aliyuncs.com/<ns>/<repo>:app-v1.0 . # 上传 docker push registry.cn-hangzhou.aliyuncs.com/<ns>/<repo>:app-v1.0
步骤六:发布应用
方法一:自定义Deployment模板
编写一个自定义的deployment模板,将KMS Agent与应用容器部署在同一个Pod中作为附加容器。如下所示,将app与sidecar容器放到同一个Pod或者Deployment里一起部署。如何运行您的容器应用,请参见创建无状态工作负载Deployment。
- KMS_TOKEN:指定Agent启动时生成的SSRF Token文件的存储路径。 
- ALIBABA_CLOUD_ROLE_ARN:RAM角色的ARN。 
- ALIBABA_CLOUD_OIDC_PROVIDER_ARN :OIDC身份提供商的ARN,此处为ACK集群ARN。 
- ALIBABA_CLOUD_OIDC_TOKEN_FILE:包含OIDC Token的文件路径。 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: rrsa-dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      serviceAccountName: dev-sa
      containers:
      - name: kmsagent
        image: registry-vpc.cn-hangzhou.aliyuncs.com/<ns>/<repo>:kmsagent-v1.0
        env:
        - name: ALIBABA_CLOUD_ROLE_ARN
          value: acs:ram::<uid>:role/dev-role-for-rrsa
        - name: ALIBABA_CLOUD_OIDC_PROVIDER_ARN
          value: acs:ram::<uid>:oidc-provider/ack-rrsa-<ackClusterId>
        - name: ALIBABA_CLOUD_OIDC_TOKEN_FILE
          value: /var/run/secrets/ack.alibabacloud.com/rrsa-tokens/token
        - name: KMS_TOKEN
          value: file:///var/run/kmstoken/token
        volumeMounts:
          - name: shared-volume
            mountPath: /var/run/kmstoken
          - name: rrsa-oidc-token
            mountPath: /var/run/secrets/ack.alibabacloud.com/rrsa-tokens
            readOnly: true
        command: ["/bin/sh", "-c", "./alibabacloud-kms-agent token /var/run/kmstoken/token && ./alibabacloud-kms-agent agent config.toml"]
      - name: my-app
        image: registry-vpc.cn-hangzhou.aliyuncs.com/<ns/<repo>:app-v1.0
        volumeMounts:
          - name: shared-volume
            mountPath: /var/run/kmstoken
      volumes:
        - name: shared-volume
          emptyDir: {}
        - name: rrsa-oidc-token
          projected:
            defaultMode: 420
            sources:
            - serviceAccountToken:
               audience: sts.aliyuncs.com
               expirationSeconds: 3600
               path: token方法二:安装OpenKruise,配置注入规则
OpenKruise是基于Kubernetes的一个标准扩展组件,可以配合原生Kubernetes使用,高效管理应用容器、Sidecar容器及镜像分发。详细介绍,请参见使用OpenKruise部署云原生应用。
- 安装OpenKruise。  - 登录容器服务管理控制台,在左侧导航栏选择集群列表。 
- 在集群列表页面,单击目标集群名称,然后在左侧导航栏,单击组件管理。 
- 在组件管理页面,单击应用管理页签。在ack-kruise区域,单击安装。 - 在安装ack-cruise对话框确认组件信息后,单击确认。 
 
- 设置Sidecar注入规则。 - 设置Sidecar容器注入规则,为带有标签label: app的Pod注入Sidecar容器。 - 环境变量定义: - KMS_TOKEN:指定Agent启动时生成的SSRF Token文件的存储路径。 
- ALIBABA_CLOUD_ROLE_ARN:RAM角色的ARN。 
- ALIBABA_CLOUD_OIDC_PROVIDER_ARN :OIDC身份提供商的ARN,此处为ACK集群ARN。 
- ALIBABA_CLOUD_OIDC_TOKEN_FILE:包含OIDC Token的文件路径。 
 
- selector 定义:选择需要注入sidecar的Pod,可以根据实际情况修改,本文以对带有标签app的Pod注入sidecar容器为例。 
 - apiVersion: apps.kruise.io/v1alpha1 kind: SidecarSet metadata: name: kms-agent-sidecarset namespace: rrsa-dev spec: serviceAccountName: dev-sa containers: - name: kms-agent image: registry-vpc.cn-hangzhou.aliyuncs.com/<ns>/<repo>:kmsagent-v1.0 env: - name: ALIBABA_CLOUD_ROLE_ARN value: acs:ram::<uid>:role/dev-role-for-rrsa - name: ALIBABA_CLOUD_OIDC_PROVIDER_ARN value: acs:ram::<uid>:oidc-provider/ack-rrsa-<ackClusterId> - name: ALIBABA_CLOUD_OIDC_TOKEN_FILE value: /var/run/secrets/ack.alibabacloud.com/rrsa-tokens/token - name: KMS_TOKEN value: file:///var/run/kmstoken/token volumeMounts: - name: shared-volume mountPath: /var/run/kmstoken - name: rrsa-oidc-token mountPath: /var/run/secrets/ack.alibabacloud.com/rrsa-tokens readOnly: true command: ["/bin/sh", "-c", "./alibabacloud-kms-agent token /var/run/kmstoken/token && ./alibabacloud-kms-agent agent config.toml"] # 用于选择需要注入 sidecar 的 Pod,可以根据实际情况修改 selector: matchLabels: app: app volumes: - name: shared-volume emptyDir: {} - name: rrsa-oidc-token projected: defaultMode: 420 sources: - serviceAccountToken: audience: sts.aliyuncs.com expirationSeconds: 3600 path: token
- 自动注入KMS Agent。 - 编写Deployment文件,在ACK上发布应用,labels标注上 - app: app,才能被注入Sidecar容器。- apiVersion: apps/v1 kind: Deployment metadata: name: app-with-kmsagent namespace: rrsa-dev spec: replicas: 1 selector: matchLabels: app: app template: metadata: labels: app: app spec: serviceAccountName: dev-sa containers: - name: my-app image: registry-vpc.cn-hangzhou.aliyuncs.com/<ns>/<repo>:app-v1.0 volumeMounts: - name: shared-volume mountPath: /var/run/kmstoken volumes: - name: shared-volume emptyDir: {}
- 运行您的容器应用。具体操作,请参见创建无状态工作负载Deployment。 - 应用启动,可以看到在应用所在Pod里,已经被注入了kms agent代理容器。  
步骤七:创建并获取凭据值
- 在KMS创建凭据。 - 本文示例中创建凭据时需要加上标签 - env:dev,应用容器只能获取到有这个标签的凭据,没有权限获取其他凭据。- 登录密钥管理服务控制台,在顶部菜单栏选择地域后,在左侧导航栏单击。 
- 单击通用凭据页签,选择实例ID后,单击创建凭据,完成各项配置后单击确定。 - 配置项 - 说明 - 凭据名称 - 自定义的凭据名称。凭据名称在当前地域内唯一。 - 设置凭据值 - 根据您要托管的敏感数据类型,选择凭据键/值或纯文本。 - 长度不超过30720字节(30KB)。 - 初始版本号 - 凭据的初始版本号。默认为v1,也支持自定义版本号。 - 加密主密钥 - 选择用于加密凭据值的密钥。 重要- 密钥和凭据需要属于同一个KMS实例,且密钥必须为对称密钥。关于KMS支持哪些对称密钥,请参见密钥管理类型和密钥规格。 
- 如果是RAM用户、RAM角色,需要具备使用加密主密钥执行GenerateDataKey操作的权限。 
 - 标签 - 凭据的标签,方便您对凭据进行分类管理。每个标签由一个键值对(Key:Value)组成,包含标签键(Key)、标签值(Value)。 说明- 标签键和标签值的格式:最多支持128个字符,可以包含英文大小写字母、数字、正斜线(/)、反斜线(\)、下划线(_)、短划线(-)、半角句号(.)、加号(+)、等于号(=)、半角冒号(:)、字符at(@)、空格。 
- 标签键不能以aliyun或acs:开头。 
- 每个凭据最多可以设置20个标签键值对。 
 - 描述信息 - 凭据的描述信息。 - 凭据的策略配置。详细介绍,请参见凭据策略概述。 - 您可以先选择默认策略,创建凭据后根据业务需要再修改策略。 
 
- 获取凭据值。 - 使用curl命令。 - curl -v -H "X-Vault-Token:$(</var/run/kmstoken/token)" 'http://localhost:2025/secretsmanager/get?secretId=app/dev/secret-1'
- 在业务代码里使用HTTP GET请求获取凭据。 - package main import ( "fmt" "io/ioutil" "net/http" ) func main() { url := fmt.Sprintf("http://localhost:2025/secretsmanager/get?secretId=%s", "app/dev/secret-1") 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)) }