在本文中,您将学习如何设计并构建一个安全、可扩展的混合云平台,该平台能够将您的本地Kubernetes基础设施与Google Cloud Platform连接起来。这样一来,本地应用程序就可以使用云服务(尤其是GPU),而无需依赖寿命较长的密钥、进行手动凭证管理,也不必采用存在风险的网络架构。

**适用对象:**
– 需要管理混合环境中的本地及云上Kubernetes资源的平台工程师、运维人员以及专注于安全性的云架构师;
– 希望实现从本地工作负载到GCP资源(尤其是GPU实例)的可扩展、可审计的访问机制,同时尽量降低运营开销和风险的企业团队。

**您将从本指南中获得什么:**
– 了解采用混合架构背后的动机及经济考量因素(为什么GPU通常会促使人们将相关工作负载迁移至云端);
– 了解服务账户密钥使用中常见的陷阱,以及在实际环境中如何出现“意外的安全漏洞”;
– 学习一种实用的全端解决方案:通过Workload Identity Federation机制,让本地Kubernetes集群中的Pods能够以短寿命、可审计的方式访问GCP资源,而无需在系统中嵌入密钥。

**包含内容:**
– 概念性解释、安全方面的权衡考量以及最佳运营实践;
– 具体的示例代码及Kubernetes/Terraform配置文件(这些资源链接在本文末尾的GitHub仓库中),您可以根据这些资料在自己的环境中重现相关设置。

**阅读建议:**
先了解相关的理论基础,然后按照实战指南中的步骤来配置GCP资源、建立身份认证联盟、使用CEL和Kyverno等工具实施策略控制,最后验证您的本地Kubernetes集群是否能够安全、高效地访问云上的GPU资源。

**注意:** Kubernetes及Terraform相关的配置文件链接在本文末尾的GitHub仓库中。

## 目录
– **先决条件**
– **为什么混合云如此重要**
– **混合架构中的GPU技术如何改变了一切**
– **为什么服务账户密钥在大规模应用环境中会失效**
– **“意外的安全漏洞”是如何产生的**
– **Workload Identity Federation如何填补这些安全缺口**
– **Kubernetes的身份认证机制是如何工作的**
– **如何准备Google Cloud Platform的资源**
– **如何使用CEL实现细粒度的访问控制**
– **如何利用Kyverno自动注入凭证信息**
– **如何为联合身份授予IAM权限**
– **如何验证整个配置是否正确**
– **如何将本地应用程序与云上的GPU资源连接起来**
– **如何利用CEL条件来实现GPU访问能力的动态扩展**
– **各种安全特性的对比分析**
– **完整的“基础设施即代码”架构设计**
– **如何使用vCluster进行概念验证**
– **常见问题及其解决方法**
– **结论**

先决条件

在开始操作之前,您需要准备以下条件:

  • 一个GKE架构的Kubernetes集群(可以是本地部署的、裸机服务器组成的集群,也可以是虚拟集群)。

  • 一个已启用以下API的Google Cloud项目:IAM、Security Token Service (STS)以及Workload Identity。

  • 已安装并配置好Terraform工具。

  • 已在您的集群中安装了Kyverno平台。

  • 需要安装Python 3,并确保系统中包含了google-cloud-secret-managergoogle-cloud-aiplatform库(这些库在验证步骤中会用到,相关代码可从GitHub仓库获取)。

  • 需要具备通过kubectl访问集群的权限。

为什么混合云如此重要

如果一切顺利,混合云平台能够让您的本地环境中的工作负载与云端的工作负载像处于同一网络中一样进行交互。

采用混合云架构有很多实际意义:

  • 将数据分析任务交给BigQuery处理:您可以将数据分析应用保留在本地环境中,以确保数据主权,但可以将大规模数据集传输到BigQuery中进行处理——这样无需额外购买服务器。

  • 通过Cloud Interconnect构建统一的网络环境:利用Cloud Interconnect或Cloud VPN,您的本地数据中心就可以成为Google Cloud Platform (GCP)虚拟私有云(VPC)的延伸。这样一来,您的本地应用就能以较低的延迟与云端服务进行交互,同时也不会暴露在公共互联网环境中。

  • 通过Cloud Storage实现经济高效的可扩展性:您可以将云存储作为本地应用的后端,用于存储日志、备份数据及历史信息,而且只需按实际使用量付费即可。

  • 利用Pub/Sub实现事件驱动的同步:当本地环境中发生某些事件时,Cloud Pub/Sub会立即发送消息,从而让云端服务能够即时做出反应——完全无需进行手动轮询操作。

混合云的经济性:GPU彻底改变了这一切

在深入探讨技术层面的问题之前,首先有必要了解为什么如今混合云比以往任何时候都更加重要。

像大多数企业一样,您的组织也在本地数据中心投入了大量资金:购买了服务器,搭建了机架,还购置了网络基础设施。因此,额外运行一个工作负载所带来的边际成本基本上为零。

然而,人工智能技术的兴起改变了这一状况。

突然间,每个团队都急需图形处理单元(GPU)。需要的数量不是一两颗,而是几十颗A100 GPU用于训练任务,还需要大量的推理终端以及需要靠近模型运行的向量数据库。由于GPU资源非常稀缺,本地购买GPU所需的等待时间往往长达数月,而云服务提供商却能在几分钟内提供这些资源。

从经济角度来看,最合理的架构应该是这样的:……

  • 本地数据中心负责处理大部分计算任务——包括Web服务器、业务逻辑处理、数据库操作以及批处理作业。这些都是你已经付费购买的常规计算资源。

  • 而云服务则用于处理那些稀缺的资源——比如需要GPU加速的推理任务、模型训练工作,以及人工智能/机器学习相关应用。你可以根据需求按请求付费来使用这些服务,而且无需等待数月才能获得所需的硬件设备。

云服务并不是一个适合进行全面迁移的目标环境——它更像是为那些在本地环境中难以实现的功能提供的一种扩展手段。

不过,那些在本地运行的应用程序仍然需要与云服务进行交互。从数据中心发出的每一个API请求、对GPU加速服务的每一次调用、以及所有写入Cloud Storage的模型数据文件,都需要使用相应的认证凭据才能完成操作。而这篇文章正是要解决这个问题。

为什么服务账户密钥在大规模应用时会出现问题

以下这种情况每天都在成千上万家企业中发生:

一个开发团队需要让他们的本地应用程序能够写入Google Cloud Storage。那么,“显而易见”的解决方案是什么呢?生成一个GCP服务账户密钥,将其进行Base64编码,存储在Kubernetes的Secret资源中,然后再将该Secret挂载到相应的Pod中即可:

apiVersion: v1
kind: Secret
metadata:
  name: gcp-credentials
type: Opaque
data:
  key.json: eyJ0eXBlIjoic2VydmljZV9hY2NvdW50IiwicHJvamVjdF9pZCI6…

这种做法确实可行,但也带来了一些严重的问题:

  • 这种密钥永远不会过期。只要没有人记得去更新它,或者它的安全性没有受到威胁,那么这个密钥就会一直有效下去。

  • 这种密钥很容易被窃取。

    任何拥有对该命名空间读取权限的人,都可以通过执行`kubectl get secret -o yaml`命令来获取永久性的GCP访问权限。

  • 这种机制无法为实际的业务操作提供审计追踪功能。

    GCP只会记录“service-account-xyz访问了这个存储桶”,而不会显示是“production命名空间中的pod frontend-abc-123进行了访问”。

  • 这种方案在规模扩展时会出现严重的问题。

    如果有50个团队,每个团队使用3种环境,且每个环境都涉及4个GCP项目,那么就需要管理600个密钥,这些密钥还需要定期更新或备份,同时还要防止它们被误放到Git仓库中。

安全团队很清楚这些问题的存在。因此,许多组织采取了唯一合理的措施:他们完全禁用了服务账户密钥的生成功能。

这种“意外的安全隔离”是如何发生的

当你禁止密钥的生成时,你其实并没有真正解决混合云平台所面临的问题——你只是把这个问题转移给了其他人。而通常来说,这个“别人”就是那些负责维护平台系统的团队。他们会收到这样的Jira工单:“无法从本地环境访问GCP,这是一个优先级为P1的问题,正在阻碍项目的发布。”

结果是什么呢?你的“混合云平台”实际上已经变成了两个完全相互隔离的系统。

为了应对这个问题,各团队往往会尝试构建中间服务或API网关来代理请求,或者想出各种变通方法来获取所需的密钥。但这些措施根本不能算是一种真正的平台解决方案,它们更像是一些临时性的权宜之计罢了。

工作负载身份联盟如何弥补这一差距

每个Kubernetes集群都会为每个Pod生成经过加密签名的身份令牌,而Google Cloud也提供了专门用于识别这些令牌的服务。

这项服务被称为工作负载身份联盟——当它与OpenID Connect结合使用时,就能让混合平台真正发挥出应有的作用。

这个服务的名称非常贴切,因为“联盟”这个词意味着Google Cloud并不会存储你的身份信息,而是会信任其他系统颁发的身份凭证,只要这些凭证能够通过加密方式被验证即可。整个流程是按照以下步骤有序进行的:

  1. Pod将其由Kubernetes发行的JWT令牌提交给Google Cloud的STS端点。

  2. STS会使用你的集群所公开的JWKS密钥来验证该令牌的签名。

  3. STS还会根据工作负载身份联盟的规定,检查JWT令牌中包含的信息是否合法。

  4. 最后,STS会返回一个有效期较短的Google访问令牌(通常为1小时),供Pod用于API调用。

需要特别说明的是,工作负载身份联盟并不局限于Kubernetes平台,它同样可以与AWS IAM、Azure AD、GitHub Actions的OIDC功能以及任何符合OIDC标准的身份认证服务配合使用。

Kubernetes的身份验证机制是如何工作的

每个拥有ServiceAccount的Pod都会自动获得一个JSON Web Token(JWT),该令牌会被存储在/run/secrets/kubernetes.io/serviceaccount/token路径下。这个JWT不仅仅是一段无法理解的二进制数据,而是一种经过加密签名的身份证明:

{
  "iss": "https://kubernetes.default.svc.cluster.local",
  "sub": "system:serviceaccount:production:backend-api",
  "aud": ["https://iam.googleapis.com/..."],
  "kubernetes.io": {
    "namespace": "production",
    "serviceaccount": {
      "name": "backend-api"
    }
  },
  "exp": 1735689600
}

在JWT中,各种信息实际上都是以键值对的形式存在的——每一项都代表了发行方对于该身份凭证所做出的声明。可以把这些声明看作是令牌所证明的事实,而这些事实都是通过加密方式被验证的,因此验证者可以信任它们。

关键在于:这种令牌是由一组JSON Web Key Set(JWKS)生成的,任何拥有你的集群公开密钥的人都可以通过JSON Web Key Set端点来验证这些令牌的有效性:

kubectl get --raw /openid/v1/jwks

Google Cloud的安全令牌服务(STS)能够对这些令牌进行验证。在整个过程中,没有任何密钥会被交换,也没有任何敏感信息会被存储,只需要利用加密技术来证明身份的合法性即可。

如何准备Google Cloud Platform的资源

工作负载身份联盟实际上是一种信任边界机制——它表示“我愿意接受来自外部来源的身份凭证”。而OIDC提供者则负责配置如何对这些身份凭证进行验证。

resource "google_iam_workload_identity_pool" "pool" {
  workload.identity_pool_id = "hybrid-platform-pool"
  project                   = "my-project"
}

resource "google_iam_workload_identity_pool_provider" "k8s-provider" {
  project                            = "my-project"
  workloadidentity_pool_id          = google_iam_workload_identity_pool.pool.workload_identity_pool_id
  workload.identity_pool_provider_id = "on-prem-cluster"

  attribute_mapping = {
    "google.subject"      = "assertion.sub"
    "attribute.namespace" = "assertion['kubernetes.io']['namespace']"
  }

  attribute_condition = "attribute.namespace in [\"production\", \"staging\"]"

  oidc {
    issuer_uri = "https://kubernetes.default.svc.cluster.local"
    jwks_json  = file("jwks.json")  # 你的集群的公开密钥
  }
}

这里有两点需要注意:

  1. attribute_mapping 会从 Kubernetes JWT 中提取相关信息,并将这些信息作为 GCP 的属性提供出来。通过使用 `assertion[‘kubernetes.io’][‘namespace’]`,可以获取命名空间信息,从而将其用于访问控制。

  2. attribute_condition 是安全策略的具体实现方式。关于这一点的更多内容将在下一节中介绍。

如何使用 CEL 实现细粒度的访问控制

attribute_condition 字段使用了通用表达式语言(CEL)。这样一行配置代码就可以替代数十条身份与访问管理规则:

attribute.namespace in ["production", "staging"]

根据这个条件,位于 kube-system 命名空间中的 Pod 将无法登录 GCP —— 在请求身份与访问管理服务之前,令牌交换就会被直接拒绝。

你还可以设置更复杂的规则:

// 仅允许 production 命名空间中的特定服务账户进行登录
attribute.namespace == "production" &&;
  attribute.service_account in ["payment-processor", "order-service"]

// 允许 staging 命名空间中的登录请求,但仅在工作时间
attribute.namespace == "staging" &&;
  request.time(hours("America/New_York") >= 9 &&;
  request.time.hours("America/New_York") < 17

这种设计属于深度防御策略。即使有人创建了伪造的服务账户,或者获得了对 kubectl 的访问权限,只要 CEL 条件不满足,他们仍然无法登录 GCP。安全边界是由 Google 的基础设施来强制执行的,而不是依赖开发人员是否遵守这些规则。

如何使用 Kyverno 自动注入凭证信息

仅仅建立了一个可用的身份联合机制还不够。你的客户和开发人员不应该需要了解 OIDC、STS 或凭证配置文件这些概念——他们应该能够直接部署应用程序并让它们正常运行。

在讨论自动化流程之前,有必要先弄清楚什么是“凭证配置文件”——因为这个名称可能会造成一些误解。

凭证配置文件(有时也被称为“外部账户配置文件”或“ADC 配置文件”)其实是一份简单的 JSON 文档,它告诉 Google 的客户端库在运行时如何获取凭证信息。但它本身并不是凭证本身。在本文的后面部分你会看到这种文件的示例——其中并不包含任何敏感信息,只包含一些元数据,比如工作负载身份池的目标用户群体、STS 令牌交换的端点地址、源令牌的类型,以及存储在 Pod 文件系统中的实际 Kubernetes 服务账户令牌的位置。

将其与传统的服务账户密钥进行对比:

服务账户密钥 (key.json) 凭证配置文件 (credential-configuration.json)
文件内容 一个 RSA 私钥,这个私钥本身就是凭证 用于交换外部令牌的说明
密钥的有效期限 永久有效,除非手动更新 源令牌会自动更新(有效期约为 1 小时)
如果文件泄露 会导致对 GCP 服务账户的长期访问权限被滥用 单独来看这个文件是没有用的——它只是指向一个只有 Pod 才能读取的令牌
身份认证机制 直接代表 GCP 服务账户进行身份验证 通过 STS 将外部身份信息整合到 GCP 系统中
谁来负责密钥的更新 人工操作(或者根本不需要人工干预) Kubernetes API 服务器会自动完成更新过程

这两个文件最终都会被GOOGLE_APPLICATION_CREDENTIALS引用,从应用程序的角度来看,它们似乎可以互换使用——但实际上,只有其中一个文件丢失才会带来危险。凭证配置文件之所以可以通过ConfigMap安全地传输,正是因为其中没有可供窃取的敏感信息。

将这个文件放入ConfigMap中只是解决问题的一半。实际上,它必须被放置在那些需要访问GCP服务的Pod中。这时Kyverno就派上了用场:一个简单的ClusterPolicy就可以自动为这些Pod提供所需的所有配置。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: workload-identity-federation
spec:
  rules:
    - name: inject-gcp-credentials
      match:
        any:
          - resources:
              kinds:
                - Deployment
              selector:
                matchLabels:
                  workload-identity-federation: "enabled"
      mutate:
        patchStrategicMerge:
          spec:
            template:
              spec:
                volumes:
                  - name: workload-identity-credential-configuration
                    configMap:
                      name: workload-identity-federation-config
                containers:
                  - (name): "*"
                    volumeMounts:
                      - name: workload-identity-credential-configuration
                        mountPath: /etc/workload-identity
                        readOnly: true
                    env:
                      - name: GOOGLE_APPLICATION_CREDENTIALS
                        value: "/etc/workload-identity/credential-configuration.json"

上述ClusterPolicy实现了以下三个功能:

  1. 将ConfigMap文件挂载到部署中的容器内,挂载路径为/etc/workload-identity

  2. 设置一个名为GOOGLE_APPLICATION_CREDENTIALS的环境变量,该变量的值指向凭证配置文件的绝对路径。

从开发者的角度来看,他们的整个集成过程就是这样的:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    workload-identity-federation: "enabled" # 只需要设置这个标签即可。
spec:
  # ... 其他的常规部署配置

凭证配置文件(由Terraform创建成ConfigMap格式)会告诉Google的客户端库如何进行令牌交换:

{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_IDproviders/PROVIDER_ID",
  "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "token_url": "https://sts.googleapis.com/v1/token",
  "credential_source": {
    "file": "/run/secrets/kubernetes.io/serviceaccount/token"
  }
}

这个JSON文件是用于配置Google Workload Identity Federation的凭证信息。它指示Google Cloud客户端库,通过使用Kubernetes ServiceAccount令牌(位于/run/secrets/kubernetes.io/serviceaccount/token路径下)与外部身份提供商进行交换,从而获取Google Cloud访问权限。

这样,那些运行在GCP之外的环境中的工作负载——比如本地部署的Kubernetes集群——也能无需管理长期有效的ServiceAccount密钥,就能成功登录并使用Google Cloud服务。

所有的 Google Cloud SDK 及客户端库都能理解这种格式。Python、Go、Java 和 Node.js 等编程语言都可以使用它。

如何为联合身份授予 IAM 权限

被 STS 服务认可的 service account token,也就是所谓的联合身份,需要具备访问资源的权限。你可以通过将 IAM 角色绑定到身份池的相应属性上来实现这一目标:

resource "google_project_iam_member" "secret_access" {
  for_each = toset(["production", "staging"])
  project  = "my-project"
  role     = "roles/secretmanager.secretAccessor"
  member   = "principalSet://iam.googleapis.com/projects/\({PROJECT_NUMBER}/locations/global/workloadIdentityPools/\){POOL_ID}/attribute.namespace/${each.value}"
}

这样,Secret Manager 就能够访问来自 productionstaging 命名空间的所有经过身份验证的资源。principalSet 这一语法允许根据特定属性来进行匹配。你也可以将权限限制在特定的 service account 上:

member = "principal://iam.googleapis.com/.../subject/system:serviceaccount:production:payment-processor"

如何验证设置是否正确

你可以通过一个简单的 Python 脚本来验证设置是否正确。这个脚本会在你的本地集群中的某个 pod 内运行,用于列出 Secret Manager 中保存的秘密信息:

# list_secrets.py - 在本地环境中运行,访问 GCP Secret Manager
from google.cloud import secretmanager

def list_secrets(project_id: str):
    """
    列出某个 GCP 项目中的所有秘密信息。

    不需要显式提供认证凭据。google-cloud-secret-manager 库会自动完成以下操作:
    1. 读取环境变量 GOOGLE_APPLICATION_CREDENTIALS(由 Kyverno 设置)
    2. 加载相应的凭证配置文件
    3. 从 /run/secrets/ 目录中获取 K8s ServiceAccount token
    4> 通过 STS 将该 token 转换为 GCP 访问令牌
    5> 使用该令牌调用 Secret Manager API

    """
    client = secretmanager.SecretManagerServiceClient()
    parent = f"projects/{project_id」

    print(f"项目 {project_id} 中的秘密信息:")
    print("-" * 40)

    for secret in client.list_secrets(request={"parent": parent}):
        secret_name = secret.name.split("/")[-1]
        print(f"  - {secret_name}")
    print("-" * 40)
    print("认证方式:工作负载身份联合认证")
    print("凭证信息:未存储任何凭据,令牌在运行时生成")

if __name__ == "__main__":
    list_secrets("my-project-id")

请在你的相应 pod 中运行这个脚本:

$ kubectl exec -it my-app-xyz -- python list_secrets.py

项目 my-project-id 中的秘密信息:
----------------------------------------
  - database-password
  - api-key-stripe
  - oauth-client-secret
  - ml-model-api-key
----------------------------------------
认证方式:工作负载身份联合认证
凭证信息:未存储任何凭据,令牌在运行时生成

没有服务账户密钥,也没有任何秘密配置被加载到系统中。在运行时,只是使用Kubernetes的服务账户令牌来换取GCP的认证凭据而已。

这种机制同样适用于任何GCP服务——无论是Secret Manager、Cloud Storage、BigQuery、Pub/Sub还是Vertex AI。

如何将本地应用程序连接到云GPU

以一个典型的场景为例:一个本地的订单处理服务需要调用Vertex AI的端点来进行欺诈检测。相关模型是在Google Cloud中的GPU上运行的(你可以几分钟内就启动A100 GPU,而不需要花费数月的时间)。而应用程序的核心逻辑仍然保留在本地环境中(因为你已经为这些计算资源支付了费用)。

一旦设置了适当的IAM权限配置,任何位于允许的命名空间内的Pod都可以调用Vertex AI的服务:

# fraud_detector.py – 在本地运行,但会调用云端的GPU
from google.cloud import aiplatform

def check_fraud(transaction: dict) -> float:
    """
    调用Vertex AI的端点来进行欺诈检测。
    相关模型是在Google Cloud中的A100 GPU上运行的,
    而这段代码则是在本地的数据中心中执行的。

    认证过程是自动完成的:
    1. Kyverno会自动注入GOOGLE_APPLICATION_CREDENTIALS环境变量;
    2. aiplatform SDK会读取这些认证配置信息;
    3. K8s的服务账户令牌会通过STS机制被兑换成GCP的认证凭据;
    4. 最后,请求会经过验证才能被发送到Vertex AI。
    """
    endpoint = aiplatform.Endpoint(
        endpoint_name="projects/my-project/locations/us-central1/endpoints/fraud-model"
    )
    prediction = endpoint.predict(instances=[transaction])
    return prediction.predictions[0]["fraud_score"]

def generate_embeddings(texts: list[str]) -> list[List[float]]:
    """
    使用云端部署的模型来生成文本嵌入向量。
    由于嵌入模型的计算需求较高,如果在本地环境中运行的话需要专门的硬件;
   而在云端,你只需要按照请求次数来付费即可。
    """
    from vertexai.language_models import TextEmbeddingModel

    model = TextEmbeddingModel.from_pretrained("text-embedding-004")
    embeddings = model.get_embeddings(texts)
    return [e.values for e in embeddings]

开发者完全不需要考虑认证相关的问题。他们只需要在部署配置中添加相应的标签,就可以让本地的Pod正常调用这些云服务:

  • Vertex AI的端点,用于在云端GPU上执行机器学习推理任务。

  • Cloud Storage,用于存储模型文件和训练数据。

  • BigQuery,用于存储特征数据和进行分析。

  • Pub/Sub

    , 用于在不同环境之间传输事件数据。

  • Secret Manager,用于管理API密钥和配置信息。

  • 这样的混合架构正是按照预期的方式运行的。

    如何利用CEL条件来灵活地控制GPU访问权限

    当你需要限制某些命名空间内的程序才能使用GPU时,CEL条件就显得非常有用了。例如,如果你只想让与机器学习相关的命名空间能够访问Vertex AI的服务,就可以这样配置:

    attribute.namespace in ["ml-inference", "ml-training", "data-science"] && attribute.service_account.startsWith("ml-")

    您还可以为不同的命名空间设置不同的访问权限:

    # 用于机器学习推理的命名空间具有预测数据访问权限
    resource "google_project_iam_member" "ml_inference" {
      project = "my-project"
      role    = "roles/aiplatform.user"
      member  = "principalSet://iam.googleapis.com/.../attribute.namespace/ml-inference"
    }
    
    # 用于数据科学的命名空间具有完整的Vertex AI访问权限(适用于实验用途)
    resource "google_project_iam_member" "data_science" {
      project = "my-project"
      role    = "roles/aiplatform.admin"
      member  = "principalSet://iam.googleapis.com/.../attribute.namespace/data-science"
    }
    

    企业内部的应用开发团队无需了解也不必关心GCP的IAM机制。他们只需将资源部署到相应的命名空间中,并添加相应的标签,其余的工作就由平台来处理了。

    两种安全机制的对比

    以下是这两种认证方式之间的对比:

    属性 服务账户密钥 工作负载身份联盟
    凭证的有效期限 直到手动更新为止(通常为数年) 有效期较短(GCP令牌的有效期为1小时)
    数据泄露风险 较高——静态密钥可以被复制到任何地方 较低——令牌会很快过期
    审计追踪记录 仅显示服务账户名称 显示命名空间和服务账户名称
    密钥管理成本 在大规模应用中需要管理600多个密钥 无需管理任何密钥
    安全策略的执行方式 手动操作或基于信任机制 由GCP基础设施通过CEL机制自动执行
    开发者的使用体验 需要复制密钥、创建秘密文件并挂载卷盘 只需为部署资源添加一个标签即可

    令牌的有效期限较短这一特点值得特别注意。即使在最坏的情况下,如果令牌被泄露,它也会很快过期。Kubernetes的服务账户令牌的有效期限是可以配置的,而STS颁发的GCP访问令牌的有效期仅为1小时;相比之下,服务账户密钥的有效期限会一直持续下去,直到有人手动将其更新——这个过程通常需要数年的时间。

    完整的“基础设施即代码”架构布局

    整个解决方案都是通过Terraform来实现的,它能够同时管理GCP和Kubernetes资源:

    workload-identity-federation/
    ├── providers.tf      # Google与Kubernetes相关的配置文件
    ├── locals.tf         # 包含命名空间、项目ID等配置信息
    ├── gcp.tf            # 身份管理相关设置、提供者配置及IAM权限绑定
    └── kubernetes.tf     # 包含凭证配置的ConfigMap文件

    只需执行一次`terraform apply`命令,即可完成以下操作:

    1. 在GCP中创建工作负载身份池

    2. 使用集群中的JWKS密钥来配置OIDC提供者

    3. 为允许访问的命名空间设置IAM权限绑定

    4. 在每个命名空间中创建包含凭证配置的ConfigMap文件

    结合Kyverno的政策,就能构建出一个完全自动化的流程:

    新的命名空间被添加到允许列表中
            │
            ▼
    Terraform会在该命名空间中创建ConfigMap
            │
            ▼
    开发人员使用标签进行部署
            │
            ▼
    Kyverno会自动注入认证凭据
            │
            ▼
    Pod通过OIDC与GCP进行身份验证
            │
            ▼
    应用程序便可访问GCP提供的服务

    无需处理任何工单,也无需管理任何密钥或机密信息。

    如何使用vCluster进行概念验证

    为了证明这一方案在GKE之外也能正常运行,你可以使用vCluster来搭建演示环境——这是一个运行在另一个Kubernetes集群内部的虚拟Kubernetes集群。这样就能证明该解决方案适用于任何类型的集群。你也可以使用vind在Docker中配置vCluster。

    # vcluster.yaml
    experimental:
      docker:
        nodes:
          - name: worker-1
          - name: worker-2
    deploy:
      cni:
        flannel:
          enabled: true
    controlPlane:
      distro:
        k8s:
          version: "v1.35.0"
    
    [root@localhost #] vcluster create hybrid --driver docker -f vcluster.yaml
    [root@localhost #] kubectl get nodes
    hybrid-control-plane   Ready    control-plane   14d   v1.34.0   192.168.107.2   <none>        Debian GNU/Linux 12 (bookworm)   7.0.5-orbstack-00330-ge3df4e19b0a0-dirty   containerd://2.1.3
    hybrid-worker          Ready    <none>          14d   v1.34.0   192.168.107.3   <none>        Debian GNU/Linux 12 (bookworm)   7.0.5-orbstack-00330-ge3df4e19b0a0-dirty   containerd://2.1.3
    hybrid-worker2         Ready    <none>          14d   v1.34.0   192.168.107.4   <none>        Debian GNU/Linux 12 (bookworm)   7.0.5-orbstack-00330-ge3df4e19b0a0-dirty   containerd://2.1.3
    

    在vCluster内部,部署一个简单的测试环境:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: gcp-test
      labels:
        workload-identity-federation: "enabled"
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: gcp-test
      template:
        metadata:
          labels:
            app: gcp-test
        spec:
          containers:
            - name: test
              image: google/cloud-sdk:slim
              command: ["sleep", "infinity"]
    

    进入该Pod并验证其功能:

    $ kubectl exec -it gcp-test-xxx -- bash
    
    # 在Pod内部:
    \( gcloud auth login --cred-file=\)GOOGLE_APPLICATION_CREDENTIALS
    已使用外部账户凭据完成登录:[principal://iam.googleapis.com/...)
    $ gcloud secrets list --project=my-project
    NAME                 CREATED
    database-password    2024-01-15T10:30:00Z
    api-key              2024-01-14T09:15:00Z
    

    无需管理任何密钥或机密信息,身份联合认证机制完全按照设计要求正常工作。

    常见问题及解决方法

    如何处理隔离式集群中的JWKS获取问题

    如果你的集群的OIDC发现端点无法被外部访问(大多数本地部署的集群都是如此),你需要手动导出JWKS文件并将其上传到GCP:

    kubectl get --raw /openid/v1/jwks > jwks.json
    

    如果集群的签名密钥发生了变更,就需要更新这个文件。可以设置一个定期任务来检查密钥是否发生变化,并相应地更新Terraform配置。

    如何解决发行者URL不匹配的问题

    Kubernetes令牌中的iss字段必须与OIDC提供商配置的发行者URL完全一致。对于使用内部DNS系统的集群来说,配置如下:

    issuer_uri = "https://kubernetes.default.svc.cluster.local"
    

    这个URL不需要能够从GCP访问——因为JWKS文件中已经包含了用于验证的密钥信息。但它的值必须与令牌中的内容完全匹配。

    如何排查令牌交换失败的问题

    当认证失败时,出现的错误信息可能会让人难以理解。以下是一些常见的原因及解决方法:

    >

    错误信息 可能的原因 解决办法
    invalid_grant 发行者URL不匹配 检查JWT中的iss字段是否与配置的issuer_uri一致
    audience mismatch 凭证配置中的audience值不正确 通过Terraform重新生成凭证配置文件
    CEL条件不满足 命名空间不在允许的列表中 将相应的命名空间添加到attribute_condition中,然后重新应用配置
    JWKS验证失败 签名密钥已经更新 重新导出JWKS文件并更新Terraform配置

    总结

    在完成了上述设置之后,本地部署的工作负载就可以像GKE工作负载一样进行身份验证了——而且根本不需要使用任何长期有效的凭证。安全团队对此非常满意(因为没有需要审核的密钥),开发人员也很高兴(只需添加一个标签即可完成配置),平台维护团队同样感到欣慰(再也不用处理与凭证管理相关的问题了)。

    通过本教程,你完成了以下任务:

    1. 了解了为什么服务账户密钥在大规模环境中会失效,以及它们所带来的安全风险

    2. 在GCP中创建了一个工作负载身份池和OIDC提供商,从而确认你的集群使用的令牌发行者是可信的

    3. 利用CEL条件实现了针对不同命名空间的细粒度访问控制策略

    4. 通过Kyverno ClusterPolicy自动将凭证信息注入到Pod中

    5. 将IAM角色与联合身份属性关联起来——这样就不再需要任何长期有效的密钥了

    6. 通过在本地Pod中调用GCP的API(如Secret Manager、Vertex AI)来验证配置是否正确

    7. 证明了该解决方案适用于任何使用vCluster的Kubernetes集群

    这里使用的技术并不新鲜。自Kubernetes 1.20版本以来,OIDC就已经被纳入其中了;Workload Identity Federation在GCP中也已经应用了很多年。Kyverno和Terraform也都是成熟的工具。本教程所提供的方案是一个端到端的解决方案,开发者可以毫不费力地将其采用。
    如果你的组织已经禁用了服务账户密钥(或者应该这样做),那么这条路径正是你应该选择的。这样一来,你的本地集群和云集群最终就可以发挥它们应有的作用:成为彼此的安全补充。
    完整的实现方案以Terraform模块的形式提供,并包含了Kyverno的政策配置: github.com/shkatara/hybrid-platform-gcp-workload-identity-federation
    如果这些内容对你有帮助,你可以通过以下链接关注我:https://www.linkedin.com/in/shubhamkatara/https://www.youtube.com/@kubesimplify以及https://www.linkedin.com/company/kubesimplify/

Comments are closed.