使用背景
在腾讯云托管集群中运行容器化的工作负载时,通常需要访问存储在 Kubernetes 集群之外的一个或多个 SQL 或 NoSQL 数据库,但是将 SQL 数据库与 Kubernetes 一起使用时,存在定期轮换凭证和敏感信息传递到 Kubernetes 集群中的问题。为此,借助凭据管理系统(SSM)和腾讯云访问控制管理(CAM)来简化访问腾讯云数据库的整个过程,从而消除验证腾讯云数据库用户名和密钥存在的安全风险;同时凭据管理系统(SSM)定时轮转访问凭证的特性,间接解决人为操作所带来的负担。
本文向您介绍运行在腾讯云容器服务 TKE 上的工作负载如何使用 CAM 对数据库身份验证。在示例中,首先在腾讯云数据库和凭据管理系统(SSM)中分别创建一个数据库实例和数据库凭据;然后开启 OIDC 资源访问控制能力,将创建的 CAM OIDC 提供商作为创建角色的载体,并关联访问腾讯云数据库和凭据管理系统(SSM)的策略;最后利用 Kubernetes 服务账户、腾讯云访问控制管理(CAM)以及凭据管理系统(SSM)安全地连接到腾讯云数据库。整体架构如下图所示:
限制条件
本示例中,假定您已完成以下限制条件:
该功能仅支持 TKE 托管集群。
集群版本 ≥ v1.20.6-tke.27/v1.22.5-tke.1。
业务 Pod 可访问外网。
操作步骤
步骤1:准备托管集群
1. 登录 容器服务控制台,新建集群。
说明:
如果您没有托管集群,您可以使用容器服务控制台创建 TKE 标准集群,详情见 创建集群。
如果您已有托管集群,请在集群详情页检查集群版本,当集群版本不满足要求时,请升级集群。对运行中的 Kubernetes 集群进行升级,详情见 升级集群。
2. 执行如下命令,确保您可以通过 kubectl 客户端访问托管集群。
kubectl get node
返回如下结果,则说明可正常访问集群。
NAME STATUS ROLES AGE VERSION10.0.4.144 Ready <none> 24h v1.22.5-tke.1
说明:
步骤2:开启 OIDC 资源访问控制能力
1. 在集群详情页中,单击 ServiceAccountIssuerDiscovery 右侧的
。如下图所示:
2. 进入修改 ServiceAccountIssuerDiscovery 相关参数页面,若系统提示您无法修改相关参数,请先进行服务授权。
在角色管理页面,查看授权策略 QcloudAccessForTKERoleInOIDCConfig,单击同意授权。
3. 授权完毕后,勾选“创建 CAM OIDC 提供商”和“创建webhook组件”,并填写客户端 ID,单击确定。如下图所示:
说明:
客户端 ID 是选填参数,当不填写时,默认值是 "sts.cloud.tencent.com",本文示例中创建 CAM OIDC 提供商采用默认值。
4. 返回集群详情页,当 ServiceAccountIssuerDiscovery 可再次编辑时,表明本次开启 OIDC 资源访问控制结束。
注意:
"service-account-issuer" 和 "service-account-jwks-uri" 参数值不允许编辑,采用默认规则。
步骤3:检查 CAM OIDC 提供商和 WEBHOOK 组件是否创建成功
1. 在集群详情页中,单击 ServiceAccountIssuerDiscovery 右侧的
。 2. 进入修改 ServiceAccountIssuerDiscovery 相关参数页面,系统将提示“您创建的身份提供商已存在,前往查看”。单击前往查看。如下图所示:
3. 查看您刚创建的 CAM OIDC 提供商详细信息。如下图所示:
4. 在集群信息 > 组件管理中,如在列表看到 pod-identity-webhook 组件状态是“成功”,即表示安装组件成功。如下图所示:
您也可以执行查看命令,以 "pod-identity-webhook" 作为前缀的 Pod 状态是 Running,即表示安装组件成功。kubectl get pod -n kube-systemNAMESPACE NAME READY STATUS RESTARTS AGEkube-system pod-identity-webhook-78c76****-9qrpj 1/1 Running 0 43h
步骤4:确认数据库实例
您需要确认是否存在腾讯云数据库实例,若没有腾讯云数据库实例,请您先行创建,并在数据库实例中创建数据库。若已有腾讯云数据库实例,请您跳过数据库创建。
本示例采用腾讯云 MySQL 实例,同时开启 MySQL 实例公网。创建步骤请参考 创建 MySQL 实例。
注意:
外网地址的 value 值标识为
$db_address
。端口的 value 值标识为
$db_port
。步骤5:更新数据库安全组
托管集群上的 Pod 想要被允许访问腾讯云 MySQL 数据库,需要给腾讯云 MySQL 数据库的安全组添加一些规则。在数据库实例的安全组页面中,修改安全组规则。如下图所示:
为了给 Kubernetes Pod 创建入站规则,您需要单击安全组 ID后跳转到安全组实例页面。在安全组实例详情页,选择安全组规则 > 入站规则 > 添加规则。在“添加入站规则”弹窗中,进行入站规则的创建。本示例中使用来源为
0.0.0.0/0
,协议端口为 TCP:3306
。
步骤6:测试数据库连接性
mysql -h $db_address -P $db_port -uroot -pEnter password:Welcome to the MariaDB monitor. Commands end with ; or \\g.Your MySQL connection id is 4238098Server version: 5.7.36-txsql-log 20211230Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement.MySQL [(none)]>
步骤7:创建数据库、表、数据
为了对数据库连通性和操作权限进行验证,请您先创建个人数据库。
MySQL [(none)]> CREATE DATABASE mydb;Query OK, 1 row affected (0.00 sec)MySQL [(none)]> CREATE TABLE mydb.user (Id VARCHAR(120), Name VARCHAR(120));Query OK, 0 rows affected (0.00 sec)MySQL [(none)]> INSERT INTO mydb.user (Id,Name) VALUES ('123','tke-oidc');Query OK, 1 row affected (0.01 sec)MySQL [(none)]> SELECT * FROM mydb.user;+------+----------+| Id | Name |+------+----------+| 123 | tke-oidc |+------+----------+1 row in set (0.01 sec)
创建完成后,在控制台查看已创建的数据库。如下图所示:
注意:
数据库名的 value 值标识为
$db_name
。步骤8:在凭据管理系统中创建数据库凭据实例
请您检查是否存在数据库凭据。如果不存在数据库凭据,请您在凭据管理系统控制台中创建数据库凭据,开启凭据轮转及选择加密,降低账号的泄露风险与安全威胁。在本文示例中,将创建两个数据库凭证,两者的区别是是否具备对数据库的 select 权限,为了增强可读性,通过描述 value 值加以区分。
1. 登录 凭据管理系统控制台。
2. 在新建凭据页面,参考如下信息进行数据库账号设置。字段详情可参考 创建数据库凭据。
关联的实例:选择新建数据库实例或者已存在数据库实例。
主机:是指客户端 IP,不指定时填写
%
。权限配置:根据对数据库实例的操作需求进行授权。
单击授权,在“权限配置”页面,勾选如下权限:
单击授权,在“权限配置”页面,勾选全部权限,如下图所示:
注意:
凭据名称的 value 值标识为
$ssm_name
凭据所在地域标识为
$ssm_region_name
3. 单击创建。在凭据列表页面查看已创建的凭据,如下图所示:
步骤9:创建 CAM 角色并关联访问腾讯云数据库和凭据管理系统的策略
1. 登录 访问管理控制台。
2. 在角色页中,单击新建角色 > 身份提供商。
3. 在新建自定义角色页,参考以下信息进行设置。
注意:
oidc:aud 的 value 值需要和 CAM OIDC 提供商的客户端 ID value 值保持一致。
oidc:aud 的 value 值标识为
$my_pod_audience
,当oidc:aud的 value 值有多个时,任选其中之一即可。
注意:
根据您的业务需求,您可以选择或创建自定义的策略来进行关联。在本示例中,您可以在搜索框中搜索 QcloudSSMReadOnlyAccess 和 QcloudCDBReadOnlyAccess,然后将它们与角色进行关联。
注意:
RoleArn的 value 值标识为
$my_pod_role_arn
。步骤10:部署示例应用程序
1. 创建一个 Kubernetes 命名空间来部署资源。
kubectl create namespace my-namespace
2. 将以下内容保存到 my-serviceaccount.yaml 中。将
$my_pod_role_arn
替换为 RoleArn 的 value 值,将$my_pod_audience
替换为 oidc:aud 的 value 值。apiVersion: v1kind: ServiceAccountmetadata:name: my-serviceaccountnamespace: my-namespaceannotations:tke.cloud.tencent.com/role-arn: $my_pod_role_arntke.cloud.tencent.com/audience: $my_pod_audiencetke.cloud.tencent.com/token-expiration: "86400"
3. 将以下内容保存到sample-application.yaml中。
apiVersion: apps/v1kind: Deploymentmetadata:name: nginx-deploymentnamespace: my-namespacespec:selector:matchLabels:app: my-appreplicas: 1template:metadata:labels:app: my-appspec:serviceAccountName: my-serviceaccountcontainers:- name: nginximage: $imageports:- containerPort: 80
需注意,在本示例中,
$image
选择ccr.ccs.tencentyun.com/tkeimages/sample-application:latest
,该镜像集成了编译的 demo文件,方便进行示例演示。您可以根据自身业务进行填写。4. 部署示例。
kubectl apply -f my-serviceaccount.yamlkubectl apply -f sample-application.yaml
5. 查看使用示例应用程序部署的 Pod。
kubectl get pods -n my-namespace
示例输出如下:
NAME READY STATUS RESTARTS AGEnginx-deployment-6bfd845f47-9zxld 1/1 Running 0 67s
6. 查看工作负载环境变量信息。
kubectl describe pod nginx-deployment-6bfd845f47-9zxld -n my-namespace
示例输出如下:
步骤11:访问数据库 demo 伪代码实现
1. 确认子账号所有访问 AssumeRoleWithWebIdentity 接口的权限。如果没有权限请联系管理员添加。
3. 克隆 ssm-rotation-sdk-golang 代码。
git clone https://github.com/TencentCloud/ssm-rotation-sdk-golang.git
4. 替换 demo 中伪代码实现:
package mainimport ("flag""fmt"_ "github.com/go-sql-driver/mysql""github.com/tencentcloud/ssm-rotation-sdk-golang/lib/db""github.com/tencentcloud/ssm-rotation-sdk-golang/lib/ssm""github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common""log""time")var (roleArn, tokenPath, providerId, regionName, saToken stringsecretName, dbAddress, dbName, ssmRegionName stringdbPort uint64dbConn *db.DynamicSecretRotationDbHeader = map[string]string{"Authorization": "SKIP","X-TC-Action": "AssumeRoleWithWebIdentity","Host": "sts.internal.tencentcloudapi.com","X-TC-RequestClient": "PHP_SDK","X-TC-Version": "2018-08-13","X-TC-Region": regionName,"X-TC-Timestamp": "1659944952","Content-type": "application/json",})type Credentials struct {TmpSecretId stringTmpSecretKey stringToken stringExpiredTime uint64}func main() {flag.StringVar(&secretName, "ssmName", "", "ssm名称")flag.StringVar(&ssmRegionName, "ssmRegionName", "", "ssm地域")flag.StringVar(&dbAddress, "dbAddress", "", "数据库地址")flag.StringVar(&dbName, "dbName", "", "数据库名称")flag.Uint64Var(&dbPort, "dbPort", 0, "数据库端口")flag.Parse()provider, err := common.DefaultTkeOIDCRoleArnProvider()if err != nil {log.Fatal("failed to assume role with web identity, err:", err)}assumeResp, err := provider.GetCredential()if err != nil {log.Fatal("failed to assume role with web identity, err:", err)}var credential Credentialsif assumeResp != nil {credential = Credentials{TmpSecretId: assumeResp.GetSecretId(),TmpSecretKey: assumeResp.GetSecretKey(),Token: assumeResp.GetToken(),}}log.Printf("secretId:%v,secretey%v,token%v\\n", credential.TmpSecretId, credential.TmpSecretKey, credential.Token)DB(credential)}func DB(credential Credentials) {// 初始化数据库连接dbConn = &db.DynamicSecretRotationDb{}err := dbConn.Init(&db.Config{DbConfig: &db.DbConfig{MaxOpenConns: 100,MaxIdleConns: 50,IdleTimeoutSeconds: 100,ReadTimeoutSeconds: 5,WriteTimeoutSeconds: 5,SecretName: secretName, // 凭据名IpAddress: dbAddress, // 数据库地址Port: dbPort, // 数据库端口DbName: dbName, // 可以为空,或指定具体的数据库名ParamStr: "charset=utf8&loc=Local",},SsmServiceConfig: &ssm.SsmAccount{SecretId: credential.TmpSecretId, // 需填写实际可用的SecretIdSecretKey: credential.TmpSecretKey, // 需填写实际可用的SecretKeyToken: credential.Token,Region: ssmRegionName, // 选择凭据所存储的地域},WatchChangeInterval: time.Second * 10, // 多长时间检查一下 凭据是否发生了轮转})if err != nil {fmt.Errorf("failed to init dbConn, err:%v\\n", err)return}// 模拟业务处理中,每过一段时间(一般是几毫秒),需要拿到db连接,来操作数据库的场景t := time.Tick(time.Second)for {select {case <-t:accessDb()queryDb()}}}func accessDb() {fmt.Println("--- accessDb start")c := dbConn.GetConn()if err := c.Ping(); err != nil {log.Fatal("failed to access db with err:", err)}log.Println("--- succeed to access db")}func queryDb() {var (id intname string)log.Println("--- queryDb start")c := dbConn.GetConn()rows, err := c.Query("select id, name from user where id = ?", 1)if err != nil {log.Printf("failed to query db with err: ", err)log.Fatal(err)}defer rows.Close()for rows.Next() {err := rows.Scan(&id, &name)if err != nil {log.Fatal(err)}log.Println(id, name)}err = rows.Err()if err != nil {log.Fatal(err)}log.Println("--- succeed to query db")}
步骤12:测试 demo 示例
kubectl exec -ti nginx-deployment-6bfd845f47-9zxld -n my-namespace -- /bin/bashcd /root/
./demo --ssmName=$ssm_name --ssmRegionName=$ssm_region_name --dbAddress=$db_address --dbName=$db_name --dbPort=$db_port
在本示例中,当 $ssm_name=tke-oidc-1 时,没有数据库的 select 权限。
在本示例中,当 $ssm_name=tke-oidc-2 时,有数据库的 select 权限。
测试结论
测试表明满足预期的效果。通过 CAM 对托管集群工作负载短暂的身份验证令牌的验证,确保了身份验证的安全性;另外借助凭据管理系统对数据库用户名和密码的轮转和加密特性,使得您不必担心数据库凭据的存储和生命周期问题,这样您在托管集群连接到数据库时无需使用用户名和密码。
pod-identity-webhook 权限说明
权限说明
该组件权限是当前功能实现的最小权限依赖。
权限场景
功能 | 涉及对象 | 涉及操作权限 |
需要查询创建的 pod 上指定的 serviceaccounts 的资源情况。 | serviceaccount | list/watch/get |
创建组件时需要在 mutatingwebhookconfigurations 的资源注入客户端的证书。 | mutatingwebhookconfigurations | get/update |
权限定义
rules:- apiGroups:- ""resources:- serviceaccountsverbs:- get- watch- list- apiGroups:- ""resources:- eventsverbs:- patch- update- apiGroups:- "admissionregistration.k8s.io"resources:- "mutatingwebhookconfigurations"verbs:- get