本文介绍阿里云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 Agent从KMS实例获取凭据并缓存在内存中,避免敏感信息硬编码,保障数据安全。此外,KMS Agent 的缓存机制,能提升高并发及网络不稳定场景下访问KMS的稳定性,优化使用体验。架构图如下所示:
使用限制
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会使用一定计算资源并产生费用。
身份认证方式
支持RRSA和Worker 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标签的凭据。
方式二:Worker RAM角色方式
可以实现Worker维度的权限隔离。由于ACK Serverless集群不支持绑定Worker RAM角色,该方式只适用于ACK托管集群和ACK专有集群。
例如,您可以通过Worker RAM角色,设置app1-dev和app2-dev中的应用访问KMS中管理的凭据。同一个Worker里的应用使用同一个Worker RAM角色,即您无法针对app1-dev和app2-dev设置不同的权限。
前提条件
已创建KMS实例,并将凭据托管在KMS实例中。详细介绍,请参见购买和启用KMS实例、凭据管理快速入门。
已创建ACK集群,且集群与您的KMS实例在同一地域。请参见创建ACK托管集群、创建ACK Serverless集群。
步骤一:配置认证信息,使KMS Agent可以访问特定的凭据
方式一:RRSA方式授权
以您有两个应用app1和app2为例,app1需要访问KMS实例中打标签secret: app1的凭据,app2需要访问KMS实例中打标签secret: app2的凭据。
app1的NameSpace为app1-dev,serviceAccountName为app1-service,RAM角色为app1-rrsa。
app2的NameSpace为app2-dev,serviceAccountName为app2-service,RAM角色为app2-rrsa。
启用ACK RRSA功能。
创建集群时开启
创建ACK托管集群和ACK Edge集群时,您可以在集群配置的高级选项(选填)区域,选中开启RRSA功能。
在集群信息页面开启
登录容器服务管理控制台,在左侧导航栏选择集群列表。
在集群列表页面,单击目标集群名称,然后在左侧导航栏,选择集群信息。
在基本信息页签的安全与审计区域,单击RRSA OIDC右侧的开启。
在弹出的启用RRSA对话框,单击确定。
在基本信息区域,当集群状态由更新中变为运行中后,表明该集群的RRSA特性已变更完成。
打开集群详情页,在基本信息页签的安全与审计区域,将鼠标悬浮至RRSA OIDC右侧已开启上面,查看提供商的URL链接和ARN信息。
为app1创建一个可信实体为身份提供商的RAM角色,并授权其可以访问标签为secret: app1的凭据。具体操作,请参见创建可信实体为身份提供商的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>。本文示例为
system:serviceaccount:app1-dev:app1-service
。<namespace>:希望注入KMS Agent的应用所在的命名空间。
<serviceAccountName>:希望注入KMS Agent的应用所使用的服务账户名称。服务账户名称为Pod提供身份标识,可以通过RRSA机制与RAM角色动态绑定。
说明若您需要为多个不同命名空间中的不同服务账户配置,您可以配置多个值。
在创建角色对话框中,设置角色名称,然后单击确定。本文角色名称以app1-rrsa为例。
(可选)查看app1-rrsa这个RAM角色的信任策略。
信任策略表示允许服务账户app1-service通过阿里云RRSA(RAM Roles for Service Accounts) ,在满足OIDC身份验证条件后,担任某个RAM角色。
创建权限策略。
权限策略名称以app1-rrsa-kms-policy为例,策略内容为访问标签为secret: app1的凭据。
{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": [ "kms:Decrypt", "kms:GetSecretValue" ], "Resource": "*", "Condition": { "StringEqualsIgnoreCase": { "kms:tag/secret": [ "app1" ] } } } ] }
将app1-rrsa-kms-policy权限策略,授权给app1-rrsa角色。具体操作,请参见为RAM角色授权。
为app2创建一个可信实体为身份提供商的RAM角色,并授权其可以访问标签为secret: app1的凭据。具体操作,请参见创建可信实体为身份提供商的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>。本文示例为
system:serviceaccount:app2-dev:app2-service
。<namespace>:希望注入KMS Agent的应用所在的命名空间。
<serviceAccountName>:希望注入KMS Agent的应用所使用的服务账户名称。服务账户名称为Pod提供身份标识,可以通过RRSA机制与RAM角色动态绑定。
说明若您需要为多个不同命名空间中的不同服务账户配置,您可以配置多个值。
在创建角色对话框中,设置角色名称,然后单击确定。本文角色名称以app2-rrsa为例。
(可选)查看app2-rrsa这个RAM角色的信任策略。
信任策略表示允许服务账户app2-service通过阿里云RRSA(RAM Roles for Service Accounts) ,在满足OIDC身份验证条件后,担任某个RAM角色。
创建权限策略。
权限策略名称以app2-rrsa-kms-policy为例,策略内容为访问标签为secret: app2的凭据。
{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": [ "kms:Decrypt", "kms:GetSecretValue" ], "Resource": "*", "Condition": { "StringEqualsIgnoreCase": { "kms:tag/secret": [ "app2" ] } } } ] }
将app2-rrsa-kms-policy权限策略,授权给app2-rrsa角色。具体操作,请参见为RAM角色授权。
方式二:Worker RAM角色方式授权
Worker RAM角色属于普通服务角色,您可以为Worker节点池指定默认角色或自定义角色。
默认角色:ACK托管集群会自动创建一个所有节点共享的默认Worker RAM角色。当您通过默认的Worker RAM角色授权时,权限将会共享给集群内所有的节点,可能会存在非预期的权限扩散的风险。
(推荐)自定义角色:需要您提前在访问控制台创建一个普通服务角色,并为节点池指定这个角色。通过为不同的节点池分配特定的角色,可以将每个节点池的权限隔离开,降低集群内所有节点共享相同权限的风险。本文以使用自定义角色为例。
创建普通服务角色。角色名称以ack-secret-manager为例。
登录RAM控制台,在左侧导航栏,选择 。
在角色页面,单击创建角色。
在创建角色页面,选择信任主体类型为云服务,信任主体名称请选择容器服务Kubernetes版,最后单击确定。
在创建角色对话框,输入角色名称,单击确定。
创建如下自定义权限策略。策略名称以ack-secret-manager-policy为例。具体操作,请参见创建自定义权限策略。
{ "Version": "1", "Statement": [ { "Action": [ "kms:GetSecretValue", "kms:Decrypt" ], "Resource": [ "*" ], "Effect": "Allow" } ] }
将自定义权限策略ack-secret-manager-policy,授权给自定义角色ack-secret-manager。
创建节点池时,为节点池指定Worker RAM角色。
Worker RAM角色选择自定义,然后选择ack-secret-manager。
创建集群时同步创建节点池,详细介绍,请参见创建ACK托管集群、创建ACK Serverless集群。为已有集群创建节点池,请参见创建和管理节点池。
步骤二:在ACK集群中创建Namespace和Service Account
Namespace将ACK集群划分为逻辑隔离的虚拟空间,用于区分开发、测试、生产等环境,不同Namespace中的应用默认无法互访资源,为RAM角色绑定提供物理边界。
分别创建两个Namespace,app1-dev和app2-dev。对应的serviceAccountName分别为app1-service和app2-service。
通过YAML文件创建名为app1-dev的Namespace。
创建YAML,文件名称以app1-namespace.yaml为例。
apiVersion: v1 kind: Namespace metadata: name: app1-dev
执行如下命令在ACK集群中创建名为 app1-dev 的 Namespace。
kubectl apply -f app1-namespace.yaml
查看Namespace是否创建成功。
kubectl get namespaces
若输出包含
app1-dev
,即代表创建成功。
通过YAML文件创建名为app1-service的Service Account。
创建YAML,文件名称以app1-serviceaccount.yaml为例。
apiVersion: v1 kind: ServiceAccount metadata: name: app1-service namespace: app1-dev
执行如下命令在
app1-dev
中创建一个名为 app1-service 的ServiceAccount。kubectl apply -f app1-serviceaccount.yaml
查看 ServiceAccount 是否创建成功。
kubectl get serviceaccount -n app1-dev
若输出包含
app1-service
,即代表创建成功。
重复上述操作,创建命名空间app2-dev和服务账户app2-service。
步骤三:在ACK集群中安装ack-kms-agent-webhook-injector组件
登录容器服务管理控制台,在左侧导航栏选择集群列表。
在集群列表页面,单击目标集群名称,然后在左侧导航栏,选择
。在Helm页面,单击创建,配置基本信息,单击下一步。
配置项
说明
应用名
请输入您的应用名。建议您使用默认应用名,此处无需输入,单击下一步后会提示使用默认应用名ack-kms-agent-webhook-injector,选择是即可。
命名空间
选择应用所在的命名空间。建议您使用默认命名空间,此处无需选择保持默认,单击下一步后会提示使用默认命名空间kube-system,选择是即可。
一个ACK集群中安装到一个命名空间即可,无需重复安装。
来源
默认为应用市场,不支持修改。
Chart
搜索并选中ack-kms-agent-webhook-injector。
在弹出的对话框中确认无误后,单击是。
选择是即使用默认安装路径,组件默认安装在kube-system命名空间中,并以组件名称发布应用。
在参数配置页面,完成各项配置,然后单击确定。
配置项
说明
Chart 版本
建议您选择最新版本。
参数
agent.auth.roleArn和agent.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.roleArn和agent.auth.roleArnMapping均设置为空。
其余参数如何配置,请参见组件详情。
请等待约30 秒,进入命名空间(默认为kube-system),在无状态工作负载中检查组件状态,确认组件是否已经就绪。
步骤四:为工作负载注入KMS Agent
通过配置注解将KMS Agent注入Pod,根据您是为新建工作负载的Pod还是为现存工作负载的Pod进行配置,具体操作不同。
以无状态工作负载Deployment为例。本文示例中,请为app1-dev和app2-dev分别执行如下操作,进行设置。
登录容器服务管理控制台。
进入到目标集群,在左侧导航栏选择
。创建或修改工作负载时,为添加如下Pod注解:名称为kms-agent-webhook-injector/inject,值为true(除此之外,填入值1,T,t,True,TRUE亦可视为有效值)。
场景一:通过镜像新建工作负载
单击使用镜像创建,完成各项配置,单击创建。
填写高级配置时,请在标签与注解区域,添加如下Pod注解:名称填入kms-agent-webhook-injector/inject,值填入true。
场景二:通过 YAML 新建工作负载
单击使用YAML创建资源,完成各项配置,单击创建。
编辑 YAML 文件中的 spec.template.metadata.annotations 一项(若不存在,您需要自主创建),向其中添加键值 kms-agent-webhook-injector/inject: "true"。其他参数如何配置,请参见工作负载YAML示例。
场景三:现存工作负载
定位到目标工作负载,单击操作列的详情。
在工作负载详情页,单击右上角的YAML编辑按钮,寻找到 spec.template.metadata.annotations 一项(若不存在,您需要自主创建),向其中添加键值
kms-agent-webhook-injector/inject: "true"
。单击更新,等待工作负载重新就绪。
返回无状态页面,进入工作负载,可以观察到容器组页签下各个容器,在镜像列即可看到 KMS Agent 已经作为 Sidecar被注入到您的工作负载中。
重要在容器组页签下查看镜像时,您可能注意到Pod被注入了两次KMS Agent镜像,这是因为我们使用了初始化容器(initContainer)来进行必要的初始化工作,该初始化容器在初始化完成后就会终止(Terminated),不会对您的应用产生负面影响,也不会持续占用您的计算资源。
如果您使用RRSA方式授权,请修改YAML配置文件,将serviceAccountName参数改为步骤一中授权的服务账户。
本文示例为app1-service和app2-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,即仅允许同一台机器上的应用或进程与其通信,外部网络设备无法连接。访问地址仅支持localhost或127.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))
}