ACK容器环境中部署KMS Agent获取凭据

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配置ServiceAccountRAM权限实现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策略,不同的应用运行在不同的namespaceservice 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获取凭据。

解决方案架构

下图以用户拥有两个应用devprod为例,通过ACK容器环境中部署KMS Agent获取凭据,不同环境的应用只能访问各自对应的KMS凭据。

image

使用限制

  • ACK集群支持ACK托管集群、ACK专有集群、ACK注册集群、ACK Serverless集群。

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

操作步骤

以下流程演示了ACK、RAM、KMS如何配置,以实现业务应用通过KMS Agent获取凭据。研发只需要在KMS创建凭据,然后在应用里从本地检索即可,其余配置由运维和安全团队提前配置。整个过程中,研发不需要在业务代码里集成SDK,也不需要关心应用访问KMS的认证鉴权以及缓存容灾等。

image

步骤一:构建KMS Agent可执行文件

  1. 安装Golang环境。具体操作,请参见Go安装指南

  2. 下载源码和依赖。

    1. 请访问Git官网,下载并安装Git工具。

    2. 执行以下命令下载源码和依赖。

      git clone https://github.com/aliyun/alibabacloud-kms-agent
      go mod download
  3. 在项目根目录下,执行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 .

  4. 在项目根目录下,查看是否存在可执行文件alibabacloud-kms-agent。image

步骤二:启用ACK 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. 创建一个可信实体为身份提供商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>

        • <namespace>:应用所在的命名空间。

        • <serviceAccountName>:服务账户名称。

        本文以system:serviceaccount:rrsa-dev:dev-sa为例, 其中 rsa-dev 是下一步需要创建 k8s namespace,dev-sa 是下一步需要创建的 k8s service account。

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

    6. 查看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"
      }
  4. 创建权限策略并授权给RAM角色。

    1. 权限策略名称以dev-role-for-rrsa-kms-policy为例,策略内容以仅允许访问带有env:dev 标签的凭据为例。image

      权限策略内容如下所示:

      {
          "Version": "1",
          "Statement": [
              {
                  "Effect": "Allow",
                  "Action": [
                      "kms:Decrypt",
                      "kms:GetSecretValue"
                  ],
                  "Resource": "*",
                  "Condition": {
                      "StringEqualsIgnoreCase": {
                          "kms:tag/env": [
                              "dev"
                          ]
                      }
                  }
              }
          ]
      }
    2. dev-role-for-rrsa-kms-policy权限策略,授权给dev-role-for-rrsa角色。image

步骤三:在ACK中创建NamespaceService Account

NamespaceACK集群划分为逻辑隔离的虚拟空间,用于区分开发、测试、生产等环境,不同Namespace中的应用默认无法互访资源,为RAM角色绑定提供物理边界。Service AccountPod提供身份标识,通过RRSA机制与RAM角色动态绑定。

  1. 通过YAML文件创建Namespace。

    Namespace名称以rrsa-dev为例,YAML文件名称以namespace.yaml为例。

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

    kubectl apply -f namespace.yaml

    查看Namespace是否创建成功:

    kubectl get namespaces

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

  3. 通过YAML文件创建Service Account。

    Service Account名称以dev-sa为例,YAML文件名称以serviceaccount.yaml为例。

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

    kubectl apply -f serviceaccount.yaml

    查看 ServiceAccount 是否创建成功:

    kubectl get serviceaccount -n rrsa-dev

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

步骤四:制作sidecar容器镜像

KMS Agent作为sidecar容器与应用程序容器一起部署到阿里云的ACK容器服务。

  1. config.toml中配置KMS Agent的运行参数。

    说明

    config.tomlAgent的配置文件,您可以在步骤一获取的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
  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"]
  3. 将步骤1和步骤2创建的文件拷贝到deploy/ack/agent目录下。

    deploy/ack/agent
    ├── alibabacloud-kms-agent
    ├── config.toml
    ├── Dockerfile.agent
  4. 编译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

步骤五:制作业务容器镜像

  1. 以业务应用部署在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"]
  2. 编译应用容器镜像,并将镜像上传到阿里云镜像仓库。

    # 编译
    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中作为附加容器。如下所示,将appsidecar容器放到同一个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部署云原生应用

  1. 安装OpenKruise。image

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

    2. 集群列表页面,单击目标集群名称,然后在左侧导航栏,单击组件管理

    3. 组件管理页面,单击应用管理页签。在ack-kruise区域,单击安装

      提示对话框确认组件信息后,单击确定

  2. 设置Sidecar注入规则。

    设置Sidecar容器注入规则,为带有标签label: appPod注入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 定义:选择需要注入sidecarPod,可以根据实际情况修改,本文以对带有标签appPod注入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
  3. 自动注入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: {}
  4. 运行您的容器应用。具体操作,请参见创建无状态工作负载Deployment

    应用启动,可以看到在应用所在Pod里,已经被注入了kms agent代理容器。

    image.png

步骤七:创建并获取凭据值

  1. KMS创建凭据。

    本文示例中创建凭据时需要加上标签 env:dev,应用容器只能获取到有这个标签的凭据,没有权限获取其他凭据。

    1. 登录密钥管理服务控制台,在顶部菜单栏选择地域后,在左侧导航栏单击资源 > 凭据管理

    2. 单击通用凭据页签,选择实例ID后,单击创建凭据,完成各项配置后单击确定

      配置项

      说明

      凭据名称

      自定义的凭据名称。凭据名称在当前地域内唯一。

      设置凭据值

      根据您要托管的敏感数据类型,选择凭据键/值纯文本

      长度不超过30720字节(30KB)。

      初始版本号

      凭据的初始版本号。默认为v1,也支持自定义版本号。

      加密主密钥

      选择用于加密凭据值的密钥。

      重要
      • 密钥和凭据需要属于同一个KMS实例,且密钥必须为对称密钥。关于KMS支持哪些对称密钥,请参见密钥管理类型和密钥规格

      • 如果是RAM用户、RAM角色,需要具备使用加密主密钥执行GenerateDataKey操作的权限。

      标签

      凭据的标签,方便您对凭据进行分类管理。每个标签由一个键值对(Key:Value)组成,包含标签键(Key)、标签值(Value)。

      说明
      • 标签键和标签值的格式:最多支持128个字符,可以包含英文大小写字母、数字、正斜线(/)、反斜线(\)、下划线(_)、短划线(-)、半角句号(.)、加号(+)、等于号(=)、半角冒号(:)、字符at(@)、空格。

      • 标签键不能以aliyunacs:开头。

      • 每个凭据最多可以设置20个标签键值对。

      描述信息

      凭据的描述信息。

      策略配置

      凭据的策略配置。详细介绍,请参见凭据策略概述

      您可以先选择默认策略,创建凭据后根据业务需要再修改策略。

  2. 获取凭据值。

    • 使用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))
      }