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
latestpara obtener la más reciente.
Cómo llegan los secretos a un pod
Hay dos formas principales:
- 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.
- 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), noproject_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-sourcegcp)resourceName: la ruta completa del secreto en Secret Managerpath: 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.ioes el managed de GKE. El open-source essecrets-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-readerOutput 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 destroyLinks útiles
- Secret Manager CSI en GKE — doc oficial del CSI driver managed
- Workload Identity Federation — el mecanismo general de Federation
- Workload Identity Federation en GKE — Federation aplicado a GKE
- Secrets Store CSI Driver — el proyecto upstream
- PR del lab — todo el código de este post