Secret Manager en GKE con CSI Driver Secrets Store

Cómo montar secretos de GCP Secret Manager en pods de GKE usando el CSI driver managed, Workload Identity Federation y OpenTofu.
Kubernetes
GCP
OpenTofu
IaC
Security
Published

March 28, 2026

En un post anterior armamos un cluster de GKE de bajo costo con OpenTofu. Ahora vamos a agregarle la capacidad de consumir secretos de Secret Manager directamente desde los pods, sin hardcodear credenciales ni usar SDKs.

La idea: tu app lee un archivo, y ese archivo es un secreto que viene de Secret Manager. La app no sabe que Secret Manager existe. Todo el código está en emimartin26/gke-lab#1.

Qué es Secret Manager

Secret Manager es el servicio de GCP para guardar datos sensibles: API keys, passwords, certificados, connection strings, etc. En vez de tener esos valores en variables de entorno o archivos de config, los guardás en Secret Manager y tu aplicación los lee en runtime.

Un secreto tiene dos conceptos:

  • Secret: el “contenedor”. Tiene un nombre (ej: demo-api-key) y configuración de replicación.
  • Secret Version: el valor en sí. Un secret puede tener múltiples versiones (v1, v2, …). Siempre podés pedir latest para obtener la más reciente.

Cómo llegan los secretos a un pod

Hay dos formas principales:

  1. SDK/API directa: tu app llama a la API de Secret Manager con el SDK de GCP. Funciona, pero acopla tu código a GCP.
  2. Secrets Store CSI Driver (lo que usamos acá): GKE monta el secreto como un archivo dentro del pod. Tu app solo lee un archivo.

Qué es el CSI Driver

CSI = Container Storage Interface. Es un estándar de Kubernetes para conectar distintos sistemas de almacenamiento. El Secrets Store CSI Driver es un plugin que “monta” secretos de un vault externo como si fueran archivos en un volumen.

GKE tiene una versión managed de este driver. Cuando lo habilitás, GKE instala automáticamente:

  • El CSI driver (secrets-store-gke.csi.k8s.io): el componente genérico que sabe montar secretos como volúmenes
  • El GCP provider: el plugin específico que sabe hablar con Secret Manager

El flujo completo

Pod spec
  │
  ▼
Volume type: CSI (driver: secrets-store-gke.csi.k8s.io)
  │
  ▼
SecretProviderClass → "andá a buscar este secreto de Secret Manager"
  │
  ▼
CSI Driver → "necesito autenticarme como este pod"
  │
  ▼
Workload Identity Federation → el KSA del pod se convierte en un principal de IAM
  │
  ▼
Secret Manager → "este principal tiene roles/secretmanager.secretAccessor, OK"
  │
  ▼
El secreto se monta como archivo en /var/secrets/api-key
  │
  ▼
Tu app lee el archivo normalmente (cat, open(), etc.)

Workload Identity Federation

Para que el CSI driver pueda autenticarse con Secret Manager, necesitamos que GCP reconozca al pod como una identidad válida. Esto lo resuelve Workload Identity Federation.

El problema

Cuando un pod corre en GKE, corre bajo un Kubernetes ServiceAccount (KSA). Pero GCP no sabe qué es un KSA — Secret Manager necesita un principal de IAM para dar permisos.

La solución

Workload Identity Federation convierte al KSA en un principal de IAM directamente, sin intermediarios. El KSA no impersona a nadie — es la identidad.

Y este mecanismo no es exclusivo de GKE. Federation soporta muchos proveedores de identidad:

Proveedor Caso de uso
GKE / Kubernetes Pods accediendo a GCP (nuestro caso)
GitHub Actions CI/CD pipelines desplegando a GCP
GitLab CI/CD pipelines
AWS Apps en AWS accediendo a GCP (multi-cloud)
Azure / AD Apps en Azure o Active Directory on-premise
Cualquier OIDC/SAML 2.0 Cualquier sistema que emita tokens estándar

El flujo general sigue el estándar OAuth 2.0 Token Exchange:

Tu workload (GKE pod, GitHub Action, app en AWS, etc.)
  │
  ▼
Identity Provider → emite un token diciendo "soy X"
  │
  ▼
Google Security Token Service (STS)
  → verifica el token con el Identity Provider
  → lo intercambia por un token federado de GCP
  │
  ▼
El workload usa el token federado para acceder a recursos GCP

Dos conceptos clave

  • Workload Identity Pool: una agrupación de identidades externas. En GKE, el pool se crea automáticamente al habilitar Workload Identity: mi-proyecto.svc.id.goog
  • Provider: describe la relación entre GCP y tu proveedor de identidad. En GKE, el provider es el API server del cluster.

Acceso directo vs impersonación

Una vez que Federation reconoce la identidad, hay dos caminos para dar permisos:

Acceso directo — la identidad federada recibe roles directamente sobre el recurso:

resource "google_secret_manager_secret_iam_member" "accessor" {
  secret_id = google_secret_manager_secret.this.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "principal://iam.googleapis.com/projects/${var.project_number}/..."
}

Impersonación de Service Account — la identidad federada actúa a través de una GSA:

# Primero: la identidad federada puede impersonar una GSA
resource "google_service_account_iam_member" "wi_binding" {
  service_account_id = google_service_account.my_gsa.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principal://iam.googleapis.com/projects/${var.project_number}/..."
}

# Después: la GSA tiene los permisos reales
resource "google_secret_manager_secret_iam_member" "accessor" {
  secret_id = google_secret_manager_secret.this.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.my_gsa.email}"
}

Google recomienda acceso directo como primera opción. La impersonación queda como alternativa para servicios que todavía no soportan principal://.

El formato del principal

principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/NAMESPACE/sa/KSA_NAME

Desglose:

Parte Qué significa
principal://iam.googleapis.com Prefijo fijo — es una identidad federada
projects/617397463558 El proyecto (usa el número, no el ID string)
locations/global Fijo — el pool es global
workloadIdentityPools/...svc.id.goog El pool de Workload Identity del cluster
subject/ns/default El namespace de Kubernetes
sa/default El nombre del Kubernetes ServiceAccount

Para que Federation funcione, el cluster necesita tres cosas:

1. Workload Identity habilitado en el cluster:

workload_identity_config {
  workload_pool = "${var.project_id}.svc.id.goog"
}

2. GKE metadata server en los nodos:

workload_metadata_config {
  mode = "GKE_METADATA"
}

3. nodeSelector en el pod para asegurar que corra en nodos con el metadata server:

nodeSelector:
  iam.gke.io/gke-metadata-server-enabled: "true"

Estructura del proyecto

Organizamos el proyecto con módulos de Terraform. Cada feature nueva es un módulo en modules/ invocado desde un archivo en la raíz:

iac/
├── main.tf                         # cluster con secret_manager_config enabled
├── data.tf                         # data source para obtener project_number
├── iam.tf                          # node SA + WI Federation (observabilidad)
├── secret-manager.tf               # invoca el módulo
├── modules/secret-manager/
│   ├── main.tf                     # API + secret + version
│   ├── iam.tf                      # secretAccessor via WI Federation
│   ├── variables.tf
│   └── outputs.tf
├── k8s/secret-manager/
│   ├── secret-provider-class.yaml  # CRD que mapea secretos a archivos
│   └── demo-pod.yaml               # pod de prueba

Terraform paso a paso

Habilitar el CSI driver (main.tf)

secret_manager_config {
  enabled = true
}

Un detalle importante: en el provider de Terraform, secret_manager_config es un bloque top-level del recurso google_container_cluster. No va dentro de addons_config:

resource "google_container_cluster" "primary" {
  # ...

  addons_config {
    horizontal_pod_autoscaling { disabled = false }
    http_load_balancing { disabled = false }
    gcp_filestore_csi_driver_config { enabled = false }
    gcs_fuse_csi_driver_config { enabled = false }
  }

  # Top-level, NO dentro de addons_config
  secret_manager_config {
    enabled = true
  }

  # ...
}

Obtener el project number (data.tf)

Workload Identity Federation usa el project_number (numérico), no el project_id (string). Para obtenerlo automáticamente:

data "google_project" "this" {
  project_id = var.project_id
}

Después lo usamos como data.google_project.this.number.

Crear el secreto (modules/secret-manager/main.tf)

# Habilita la API (sin esto no podés crear secretos)
resource "google_project_service" "secretmanager" {
  service            = "secretmanager.googleapis.com"
  disable_on_destroy = false
}

# El contenedor del secreto
resource "google_secret_manager_secret" "this" {
  secret_id = var.secret_id

  replication {
    auto {}  # GCP elige dónde replicar automáticamente
  }

  depends_on = [google_project_service.secretmanager]
}

# El valor del secreto (una "versión")
resource "google_secret_manager_secret_version" "this" {
  secret      = google_secret_manager_secret.this.id
  secret_data = var.secret_data
}

Dar permisos al KSA (modules/secret-manager/iam.tf)

resource "google_secret_manager_secret_iam_member" "accessor" {
  secret_id = google_secret_manager_secret.this.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "principal://iam.googleapis.com/projects/${var.project_number}/locations/global/workloadIdentityPools/${var.project_id}.svc.id.goog/subject/ns/${var.ksa_namespace}/sa/${var.ksa_name}"
}

Notas:

  • El permiso es sobre el secreto específico, no a nivel proyecto. Mínimo privilegio.
  • Usa project_number (numérico), no project_id (string).
  • Si quisieras que otro namespace/KSA acceda, necesitás otro binding.

Invocar el módulo (secret-manager.tf)

module "secret_manager" {
  source = "./modules/secret-manager"

  project_id     = var.project_id
  project_number = data.google_project.this.number
  secret_id      = "demo-api-key"
  secret_data    = "change-me-super-secret-value"
  ksa_name       = "default"
  ksa_namespace  = "default"
}

Kubernetes: los manifests

SecretProviderClass

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: demo-secret
spec:
  provider: gke
  parameters:
    secrets: |
      - resourceName: "projects/mi-proyecto/secrets/demo-api-key/versions/latest"
        path: "api-key"

Es un CRD (Custom Resource Definition) que instala el CSI driver. Define:

  • provider: gke: usa el provider managed de GKE (no el open-source gcp)
  • resourceName: la ruta completa del secreto en Secret Manager
  • path: nombre del archivo donde se monta el secreto dentro del volumen

Podés listar múltiples secretos agregando más entradas bajo secrets:.

Pod de demo

apiVersion: v1
kind: Pod
metadata:
  name: demo-secret-reader
spec:
  serviceAccountName: default          # (A)
  containers:
    - name: reader
      image: busybox:1.36
      command: ["sh", "-c", "cat /var/secrets/api-key"]
      volumeMounts:
        - name: secrets
          mountPath: /var/secrets       # (B)
          readOnly: true
  volumes:
    - name: secrets
      csi:
        driver: secrets-store-gke.csi.k8s.io  # (C)
        readOnly: true
        volumeAttributes:
          secretProviderClass: demo-secret      # (D)
  nodeSelector:
    iam.gke.io/gke-metadata-server-enabled: "true"  # (E)
  restartPolicy: Never
  • (A) serviceAccountName: debe coincidir con el KSA al que le diste permisos en Terraform
  • (B) mountPath: el directorio donde el CSI driver monta los secretos como archivos
  • (C) driver: secrets-store-gke.csi.k8s.io es el managed de GKE. El open-source es secrets-store.csi.k8s.io — distinto, no usar
  • (D) secretProviderClass: nombre del SecretProviderClass que creaste
  • (E) nodeSelector: asegura que el pod corra en un nodo con el GKE metadata server habilitado

Probarlo

# 1. Levantar la infra
tofu apply

# 2. Conectarse al cluster
gcloud container clusters get-credentials gke-lab \
  --location us-central1-c \
  --project mi-proyecto \
  --dns-endpoint

# 3. Aplicar los manifests
kubectl apply -f k8s/secret-manager/secret-provider-class.yaml
kubectl apply -f k8s/secret-manager/demo-pod.yaml

# 4. Ver el secreto
kubectl logs demo-secret-reader

Output esperado:

--- Secret from CSI volume mount ---
change-me-super-secret-value
--- Done ---

Para limpiar:

kubectl delete pod demo-secret-reader
kubectl delete secretproviderclass demo-secret
tofu destroy