tencent cloud

文档反馈

Pod 使用 CAM 对数据库身份验证

最后更新时间:2024-02-05 17:23:15

    使用背景

    在腾讯云托管集群中运行容器化的工作负载时,通常需要访问存储在 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

    操作步骤

    步骤1:准备托管集群

    1. 登录 容器服务控制台,新建集群。
    说明
    如果您没有托管集群,您可以使用容器服务控制台创建 TKE 标准集群,详情见 创建集群
    如果您已有托管集群,请在集群详情页检查集群版本,当集群版本不满足要求时,请升级集群。对运行中的 Kubernetes 集群进行升级,详情见 升级集群
    2. 执行如下命令,确保您可以通过 kubectl 客户端访问托管集群。
    kubectl get node
    返回如下结果,则说明可正常访问集群。
    NAME STATUS ROLES AGE VERSION
    10.0.4.144 Ready <none> 24h v1.22.5-tke.1
    说明
    您可以通过 Kubernetes 命令行工具 Kubectl 从本地客户端机器连接到 TKE 集群。详情见 连接集群

    步骤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-system
    NAMESPACE NAME READY STATUS RESTARTS AGE
    kube-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 客户端的实例中,确认您使用用户名 root 和您在创建数据库设置的密码连接到数据库。如果无法连接到数据库,请返回查看是否开启 公网 及是否正确配置 安全组
    mysql -h $db_address -P $db_port -uroot -p
    Enter password:
    Welcome to the MariaDB monitor. Commands end with ; or \\g.
    Your MySQL connection id is 4238098
    Server version: 5.7.36-txsql-log 20211230
    
    Copyright (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 值加以区分。
    2. 新建凭据页面,参考如下信息进行数据库账号设置。字段详情可参考 创建数据库凭据
    
    
    关联的实例:选择新建数据库实例或者已存在数据库实例。
    主机:是指客户端 IP,不指定时填写%
    权限配置:根据对数据库实例的操作需求进行授权。
    创建第一个数据库凭证
    创建第二个数据库凭证
    单击授权,在“权限配置”页面,勾选如下权限:
    
    
    单击授权,在权限配置页面,勾选全部权限,如下图所示:
    
    
    注意
    凭据名称的 value 值标识为$ssm_name
    凭据所在地域标识为$ssm_region_name
    3. 单击创建。在凭据列表页面查看已创建的凭据,如下图所示:
    
    

    步骤9:创建 CAM 角色并关联访问腾讯云数据库和凭据管理系统的策略

    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: v1
    kind: ServiceAccount
    metadata:
    name: my-serviceaccount
    namespace: my-namespace
    annotations:
    tke.cloud.tencent.com/role-arn: $my_pod_role_arn
    tke.cloud.tencent.com/audience: $my_pod_audience
    tke.cloud.tencent.com/token-expiration: "86400"
    3. 将以下内容保存到sample-application.yaml中。
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: nginx-deployment
    namespace: my-namespace
    spec:
    selector:
    matchLabels:
    app: my-app
    replicas: 1
    template:
    metadata:
    labels:
    app: my-app
    spec:
    serviceAccountName: my-serviceaccount
    containers:
    - name: nginx
    image: $image
    ports:
    - containerPort: 80
    需注意,在本示例中,$image选择ccr.ccs.tencentyun.com/tkeimages/sample-application:latest,该镜像集成了编译的 demo文件,方便进行示例演示。您可以根据自身业务进行填写。
    4. 部署示例。
    kubectl apply -f my-serviceaccount.yaml
    kubectl apply -f sample-application.yaml
    5. 查看使用示例应用程序部署的 Pod。
    kubectl get pods -n my-namespace
    示例输出如下:
    NAME READY STATUS RESTARTS AGE
    nginx-deployment-6bfd845f47-9zxld 1/1 Running 0 67s
    6. 查看工作负载环境变量信息。
    kubectl describe pod nginx-deployment-6bfd845f47-9zxld -n my-namespace
    示例输出如下:
    
    

    步骤11:访问数据库 demo 伪代码实现

    1. 确认子账号所有访问 AssumeRoleWithWebIdentity 接口的权限。如果没有权限请联系管理员添加。
    2. 确认有访问 AssumeRoleWithWebIdentity 接口的权限后,请参考 凭证管理 中步骤5获取访问 DB + SSM 的临时密钥。
    3. 克隆 ssm-rotation-sdk-golang 代码。
    git clone https://github.com/TencentCloud/ssm-rotation-sdk-golang.git
    4. 替换 demo 中伪代码实现:
    package main
    
    import (
    "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 string
    secretName, dbAddress, dbName, ssmRegionName string
    dbPort uint64
    dbConn *db.DynamicSecretRotationDb
    Header = 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 string
    TmpSecretKey string
    Token string
    ExpiredTime 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 Credentials
    if 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, // 需填写实际可用的SecretId
    SecretKey: credential.TmpSecretKey, // 需填写实际可用的SecretKey
    Token: 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 int
    name 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 示例

    基于 部署示例 的部署结果,进入到 nginx 容器:
    kubectl exec -ti nginx-deployment-6bfd845f47-9zxld -n my-namespace -- /bin/bash
    cd /root/
    $ssm_name$ssm_region_name 标识参照 SSM实例 进行替换,将 $db_address$db_name $db_port 标识参照 数据库实例 进行替换。
    ./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:
    - serviceaccounts
    verbs:
    - get
    - watch
    - list
    - apiGroups:
    - ""
    resources:
    - events
    verbs:
    - patch
    - update
    - apiGroups:
    - "admissionregistration.k8s.io"
    resources:
    - "mutatingwebhookconfigurations"
    verbs:
    - get
    
    联系我们

    联系我们,为您的业务提供专属服务。

    技术支持

    如果你想寻求进一步的帮助,通过工单与我们进行联络。我们提供7x24的工单服务。

    7x24 电话支持