使用客户端库访问存储在 GKE 集群外部的 Secret


本教程介绍如何在 Secret Manager 中存储 Google Kubernetes Engine (GKE) 集群使用的敏感数据,并从 Pod 中使用适用于 GKE 和 Google Cloud 客户端库的工作负载身份联合更安全地访问这些数据。本教程适用于要将敏感数据移出集群内存储的安全管理员。

将敏感数据存储在集群存储之外可以降低在发生攻击时未经授权访问数据的风险。通过使用适用于 GKE 的工作负载身份联合访问数据,您可以避免与管理长期服务账号密钥相关的风险,并且您可以使用 Identity and Access Management (IAM)(而不是集群内 RBAC 规则)控制对 Secret 的访问权限。您可以使用任何外部 Secret 存储空间服务,例如 Secret Manager 或 HashiCorp Vault。

本教程使用 GKE Autopilot 集群。如需使用 GKE Standard 执行这些步骤,您必须手动启用适用于 GKE 的工作负载身份联合

您可以使用适用于 GKE 的工作负载身份联合从 GKE 工作负载访问任何 Google Cloud API,而无需使用静态服务账号密钥文件等安全性较低的方法。本教程以 Secret Manager 为示例,但您可以使用相同的步骤访问其他 Google Cloud API。如需了解详情,请参阅 Workload Identity Federation for GKE

目标

  • 在 Google Cloud Secret Manager 中创建 Secret。
  • 创建 GKE Autopilot 集群、Kubernetes 命名空间和 Kubernetes 服务账号。
  • 创建 IAM 允许政策,以授予对 Secret 上的 Kubernetes 服务账号的访问权限。
  • 使用测试应用来验证服务账号访问权限。
  • 运行使用 Secret Manager API 访问 Secret 的示例应用。

费用

在本文档中,您将使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本文档中描述的任务后,您可以通过删除所创建的资源来避免继续计费。如需了解详情,请参阅清理

准备工作

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. Install the Google Cloud CLI.
  3. To initialize the gcloud CLI, run the following command:

    gcloud init
  4. Create or select a Google Cloud project.

    • Create a Google Cloud project:

      gcloud projects create PROJECT_ID

      Replace PROJECT_ID with a name for the Google Cloud project you are creating.

    • Select the Google Cloud project that you created:

      gcloud config set project PROJECT_ID

      Replace PROJECT_ID with your Google Cloud project name.

  5. Make sure that billing is enabled for your Google Cloud project.

  6. Enable the Kubernetes Engine and Secret Manager APIs:

    gcloud services enable container.googleapis.com secretmanager.googleapis.com
  7. Install the Google Cloud CLI.
  8. To initialize the gcloud CLI, run the following command:

    gcloud init
  9. Create or select a Google Cloud project.

    • Create a Google Cloud project:

      gcloud projects create PROJECT_ID

      Replace PROJECT_ID with a name for the Google Cloud project you are creating.

    • Select the Google Cloud project that you created:

      gcloud config set project PROJECT_ID

      Replace PROJECT_ID with your Google Cloud project name.

  10. Make sure that billing is enabled for your Google Cloud project.

  11. Enable the Kubernetes Engine and Secret Manager APIs:

    gcloud services enable container.googleapis.com secretmanager.googleapis.com
  12. Grant roles to your user account. Run the following command once for each of the following IAM roles: roles/secretmanager.admin, roles/container.clusterAdmin

    gcloud projects add-iam-policy-binding PROJECT_ID --member="user:USER_IDENTIFIER" --role=ROLE
    • Replace PROJECT_ID with your project ID.
    • Replace USER_IDENTIFIER with the identifier for your user account. For example, user:myemail@example.com.

    • Replace ROLE with each individual role.

准备环境

克隆包含本教程的示例文件的 GitHub 代码库:

git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples
cd ~/kubernetes-engine-samples/security/wi-secrets

在 Secret Manager 中创建 Secret

  1. 以下示例展示了将用于创建 Secret 的数据:

    key=my-api-key
  2. 创建 Secret 来存储示例数据:

    gcloud secrets create bq-readonly-key \
        --data-file=manifests/bq-readonly-key \
        --ttl=3600s
    

    此命令执行以下操作:

    • us-central1 Google Cloud 区域中使用示例密钥创建新的 Secret Manager Secret。
    • 将 Secret 设置为在您运行命令一小时后过期。

创建集群和 Kubernetes 资源

创建 GKE 集群、Kubernetes 命名空间和 Kubernetes 服务账号。您将创建两个命名空间,一个用于对 Secret 的只读访问权限,另一个用于对 Secret 的读写访问权限。您还可以在每个命名空间中创建 Kubernetes 服务账号,以便与适用于 GKE 的工作负载身份联合搭配使用。

  1. 创建 GKE Autopilot 集群:

    gcloud container clusters create-auto secret-cluster \
        --region=us-central1
    

    集群部署大约需要 5 分钟。Autopilot 集群始终都会启用适用于 GKE 的工作负载身份联合。如果您要改用 GKE Standard 集群,则必须手动启用适用于 GKE 的工作负载身份联合,然后才能继续。

  2. 创建 readonly-ns 命名空间和 admin-ns 命名空间:

    kubectl create namespace readonly-ns
    kubectl create namespace admin-ns
    
  3. 创建 readonly-sa Kubernetes 服务账号和 admin-sa Kubernetes 服务账号:

    kubectl create serviceaccount readonly-sa --namespace=readonly-ns
    kubectl create serviceaccount admin-sa --namespace=admin-ns
    

创建 IAM 允许政策

  1. readonly-sa 服务账号授予对 Secret 的只读权限:

    gcloud secrets add-iam-policy-binding bq-readonly-key \
        --member=principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/readonly-ns/sa/readonly-sa \
        --role='roles/secretmanager.secretAccessor' \
        --condition=None
    

    替换以下内容:

    • PROJECT_NUMBER:您的 Google Cloud 项目数字编号。
    • PROJECT_ID:您的 Google Cloud 项目 ID。
  2. admin-sa 服务账号授予对 Secret 的读写权限:

    gcloud secrets add-iam-policy-binding bq-readonly-key \
        --member=principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/admin-ns/sa/admin-sa \
        --role='roles/secretmanager.secretAccessor' \
        --condition=None
    gcloud secrets add-iam-policy-binding bq-readonly-key \
        --member=principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/admin-ns/sa/admin-sa \
        --role='roles/secretmanager.secretVersionAdder' \
        --condition=None
    

验证 Secret 访问权限

在每个命名空间中部署测试 Pod 以验证只读访问权限和读写访问权限。

  1. 查看只读 Pod 清单:

    # Copyright 2022 Google LLC
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    apiVersion: v1
    kind: Pod
    metadata:
      name: readonly-test
      namespace: readonly-ns
    spec:
      containers:
      - image: google/cloud-sdk:slim
        name: workload-identity-test
        command: ["sleep","infinity"]
        resources:
          requests:
            cpu: "150m"
            memory: "150Mi"
      serviceAccountName: readonly-sa

    此 Pod 在 readonly-ns 命名空间中使用 readonly-sa 服务账号。

  2. 查看读写 Pod 清单:

    # Copyright 2022 Google LLC
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    apiVersion: v1
    kind: Pod
    metadata:
      name: admin-test
      namespace: admin-ns
    spec:
      containers:
      - image: google/cloud-sdk:slim
        name: workload-identity-test
        command: ["sleep","infinity"]
        resources:
          requests:
            cpu: "150m"
            memory: "150Mi"
      serviceAccountName: admin-sa

    此 Pod 在 admin-ns 命名空间中使用 admin-sa 服务账号。

  3. 部署测试 Pod:

    kubectl apply -f manifests/admin-pod.yaml
    kubectl apply -f manifests/readonly-pod.yaml
    

    Pod 可能需要几分钟才能开始运行。如需监控进度,请运行以下命令:

    watch kubectl get pods -n readonly-ns
    

    当 Pod 状态更改为 RUNNING 时,按 Ctrl+C 以返回到命令行。

测试只读访问权限

  1. readonly-test Pod 中打开 shell:

    kubectl exec -it readonly-test --namespace=readonly-ns -- /bin/bash
    
  2. 尝试读取 Secret:

    gcloud secrets versions access 1 --secret=bq-readonly-key
    

    输出为 key=my-api-key

  3. 尝试将新数据写入 Secret:

    printf "my-second-api-key" | gcloud secrets versions add bq-readonly-key --data-file=-
    

    输出类似于以下内容:

    ERROR: (gcloud.secrets.versions.add) PERMISSION_DENIED: Permission 'secretmanager.versions.add' denied for resource 'projects/PROJECT_ID/secrets/bq-readonly-key' (or it may not exist).
    

    使用只读服务账号的 Pod 只能读取 Secret,无法写入新数据。

  4. 退出 pod:

    exit
    

测试读写访问权限

  1. admin-test Pod 中打开 shell:

    kubectl exec -it admin-test --namespace=admin-ns -- /bin/bash
    
  2. 尝试读取 Secret:

    gcloud secrets versions access 1 --secret=bq-readonly-key
    

    输出为 key=my-api-key

  3. 尝试将新数据写入 Secret:

    printf "my-second-api-key" | gcloud secrets versions add bq-readonly-key --data-file=-
    

    输出类似于以下内容:

    Created version [2] of the secret [bq-readonly-key].
    
  4. 读取新的 Secret 版本:

    gcloud secrets versions access 2 --secret=bq-readonly-key
    

    输出为 my-second-api-key

  5. 退出 pod:

    exit
    

Pod 只会获取您授予 Pod 清单中使用的 Kubernetes 服务账号的访问权限级别。在 admin-ns 命名空间中使用 admin-sa Kubernetes 账号的任何 Pod 都可以写入 Secret 的新版本,但在 readonly-ns 命名空间中使用 readonly-sa Kubernetes 服务账号的任何 Pod 只能读取 Secret。

从代码访问 Secret

在本部分中,您将执行以下操作:

  1. 部署一个示例应用,该应用使用客户端库在 Secret Manager 中读取您的 Secret。

  2. 检查该应用是否可以访问您的 Secret。

您应该尽可能使用 Secret Manager API 从应用代码访问 Secret Manager Secret。

  1. 查看示例应用源代码:

    // Copyright 2022 Google LLC
    //
    // Licensed under the Apache License, Version 2.0 (the "License");
    // you may not use this file except in compliance with the License.
    // You may obtain a copy of the License at
    //
    //     http://www.apache.org/licenses/LICENSE-2.0
    //
    // Unless required by applicable law or agreed to in writing, software
    // distributed under the License is distributed on an "AS IS" BASIS,
    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    // See the License for the specific language governing permissions and
    // limitations under the License.
    
    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"os"
    
    	secretmanager "cloud.google.com/go/secretmanager/apiv1"
    	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
    )
    
    func main() {
    
            // Get environment variables from Pod spec.
            projectID := os.Getenv("PROJECT_ID")
            secretId := os.Getenv("SECRET_ID")
            secretVersion := os.Getenv("SECRET_VERSION")
    
            // Create the Secret Manager client.
            ctx := context.Background()
            client, err := secretmanager.NewClient(ctx)
            if err != nil {
                    log.Fatalf("failed to setup client: %v", err)
            }
            defer client.Close()
    
            // Create the request to access the secret.
            accessSecretReq := &secretmanagerpb.AccessSecretVersionRequest{
                    Name: fmt.Sprintf("projects/%s/secrets/%s/versions/%s", projectID, secretId, secretVersion),
            }
    
            secret, err := client.AccessSecretVersion(ctx, accessSecretReq)
            if err != nil {
                    log.Fatalf("failed to access secret: %v", err)
            }
    
            // Print the secret payload.
            //
            // WARNING: Do not print the secret in a production environment - this
            // snippet is showing how to access the secret material.
            log.Printf("Welcome to the key store, here's your key:\nKey: %s", secret.Payload.Data)
    }
    

    此应用会调用 Secret Manager API 来尝试和读取 Secret。

  2. 查看示例应用 Pod 清单:

    # Copyright 2022 Google LLC
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    apiVersion: v1
    kind: Pod
    metadata:
      name: readonly-secret-test
      namespace: readonly-ns
    spec:
      containers:
      - image: us-docker.pkg.dev/google-samples/containers/gke/wi-secret-store:latest
        name: secret-app
        env:
          - name: PROJECT_ID
            value: "YOUR_PROJECT_ID"
          - name: SECRET_ID
            value: "bq-readonly-key"
          - name: SECRET_VERSION
            value: "latest"
        resources:
          requests:
            cpu: "125m"
            memory: "64Mi"
      serviceAccountName: readonly-sa

    此清单执行以下操作:

    • readonly-ns 命名空间中创建一个使用 readonly-sa 服务账号的 Pod。
    • 从 Google 映像注册表中拉取示例应用。此应用使用 Google Cloud 客户端库调用 Secret Manager API。您可以在代码库的 /main.go 中查看应用代码。
    • 设置要使用的示例应用的环境变量。
  3. 替换示例应用中的环境变量:

    sed -i "s/YOUR_PROJECT_ID/PROJECT_ID/g" "manifests/secret-app.yaml"
    
  4. 部署示例应用:

    kubectl apply -f manifests/secret-app.yaml
    

    Pod 可能需要几分钟才能开始工作。如果 Pod 需要集群中的新节点,则您可能会在 GKE 预配节点时发现 CrashLoopBackOff 类型的事件。节点成功预配后,崩溃会停止。

  5. 验证 Secret 访问权限:

    kubectl logs readonly-secret-test -n readonly-ns
    

    输出为 my-second-api-key。如果输出为空,则 Pod 可能尚未运行。请在几分钟后重试。

替代方法

如果您需要将敏感数据装载到 Pod,请使用适用于 GKE 的 Secret Manager 插件(预览版)。此插件可在 GKE 集群中部署和管理 Google Cloud Secret Manager 提供程序,以用于 Kubernetes Secret Store CSI 驱动程序。有关说明,请参阅将 Secret Manager 插件与 GKE 配合使用

提供 Secret 作为装载的卷存在以下风险:

  1. 装载的卷容易受到目录遍历攻击。
  2. 环境变量可能会因配置错误(例如打开调试端点)而遭破解。

我们建议您尽可能通过 Secret Manager API 以编程方式访问 Secret。如需了解相关说明,请使用本教程中的示例应用,或参阅 Secret Manager 客户端库

清理

为避免因本教程中使用的资源导致您的 Google Cloud 账号产生费用,请删除包含这些资源的项目,或者保留项目但删除各个资源。

删除各个资源

  1. 删除集群:

    gcloud container clusters delete secret-cluster \
        --region=us-central1
    
  2. 可选:删除 Secret Manager 中的 Secret:

    gcloud secrets delete bq-readonly-key
    

    如果您不执行此步骤,则 Secret 会自动过期,因为您在创建期间设置了 --ttl 标志。

删除项目

    Delete a Google Cloud project:

    gcloud projects delete PROJECT_ID

后续步骤