En corto

1
2
3
4
5
6
7
8
9
CLUSTER=my-cluster; REGION=us-central1; NETWORK=my-vpc
MASTER_CIDR=$(gcloud container clusters describe $CLUSTER --region $REGION \
  --format='value(privateClusterConfig.masterIpv4CidrBlock)')
NODE_TAG=$(gcloud compute firewall-rules list --filter="name~^gke-$CLUSTER" \
  --format='value(targetTags.list())' | head -1)

gcloud compute firewall-rules create gke-$CLUSTER-master-to-vault-injector \
  --network=$NETWORK --direction=INGRESS --action=ALLOW \
  --source-ranges=$MASTER_CIDR --target-tags=$NODE_TAG --rules=tcp:8080

Borra un pod anotado y comprueba que el injector recibe la llamada:

1
2
3
4
5
kubectl -n app delete pod -l app=api
kubectl -n vault logs deploy/vault-agent-injector | grep mutate
# [INFO]  handler: Request received: Method=POST URL=/mutate?timeout=30s
kubectl -n app get pod -l app=api -o jsonpath='{.items[0].spec.initContainers[*].name} {.items[0].spec.containers[*].name}{"\n"}'
# vault-agent-init api vault-agent

El síntoma

  • El pod lleva vault.hashicorp.com/agent-inject: "true" y el resto de anotaciones, pero arranca solo con el contenedor de la aplicación: ni vault-agent-init ni vault-agent.
  • Crear el pod tarda unos 30 segundos más de lo normal.
  • kubectl get events no dice nada del webhook. En los logs del injector no hay ni una línea de Request received.
  • En otros clústeres (zonales públicos, kind, k3s) el mismo chart y las mismas anotaciones funcionan.

Por qué

En un clúster privado de GKE el control plane vive en una VPC de Google y llega a tus nodos a través de la regla de firewall que GKE crea sola (gke-<cluster>-<hash>-master). Esa regla solo permite TCP 443 y 10250 desde el rango del control plane hacia los nodos. Todo lo demás está cerrado.

Un webhook de admisión no pasa por el Service como lo haría un cliente normal: el API server resuelve el Service a sus endpoints y conecta directamente con la IP del pod en el targetPort. El Service del injector (vault-agent-injector-svc) publica el 443, pero el pod escucha en el 8080 (injector.port del chart), así que la conexión real es al 8080 y el firewall la descarta.

Lo que pasa después lo explican dos valores por defecto del chart de Vault:

ValorPor defectoEfecto
injector.webhook.timeoutSeconds30El API server espera 30 s la respuesta del webhook
injector.webhook.failurePolicyIgnoreSi el webhook no contesta, el pod se crea sin mutar, sin error

De ahí los 30 segundos de retraso y el pod sin sidecar y sin ningún mensaje. Con failurePolicy: Fail habrías visto failed calling webhook "vault.hashicorp.com": … context deadline exceeded en los eventos del ReplicaSet, que al menos apunta al sitio correcto.

No es solo Vault

Cualquier webhook de admisión, conversión de CRDs o API agregada que escuche en un puerto que no sea 443 o 10250 falla igual en GKE privado: el operador de OpenTelemetry (9443), Kyverno (9443), Gatekeeper (8443)… cert-manager escucha en el 10250 precisamente para no necesitar esta regla. La documentación de GKE lo lista como requisito de “admission webhooks, aggregated API servers, webhook conversion”: una regla por puerto, desde masterIpv4CidrBlock hacia los tags de los nodos.

Si el clúster está en Terraform, deja la regla junto al clúster para que no se pierda al recrearlo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
resource "google_compute_firewall" "master_to_vault_injector" {
  name          = "gke-${google_container_cluster.main.name}-master-to-vault-injector"
  network       = google_compute_network.main.name
  direction     = "INGRESS"
  source_ranges = [google_container_cluster.main.private_cluster_config[0].master_ipv4_cidr_block]
  target_tags   = ["gke-${google_container_cluster.main.name}"]   # el tag que usan tus node pools
  allow {
    protocol = "tcp"
    ports    = ["8080"]
  }
}

Trampas

  • El tag equivocado: los nodos llevan gke-<cluster>-<hash>-node; cópialo de las reglas gke-<cluster>-* existentes en vez de escribirlo a mano. Con varios node pools, comprueba que todos tengan el tag.
  • El rango del control plane cambia si recreas el clúster; una regla escrita a mano con el CIDR viejo deja de servir sin avisar.
  • failurePolicy: Fail hace visible el fallo, pero mientras el webhook esté caído no se crea ningún pod en los namespaces que casen con el namespaceSelector; el chart excluye al propio injector, pero no a Vault si comparten namespace y etiquetas. Úsalo con replicas: 2.
  • El injector sí recibe la petición y sigue sin sidecar: entonces ya no es red. Mira los logs de vault-agent-init: permission denied es el rol de Kubernetes auth (bound_service_account_names, bound_service_account_namespaces) o la política; connection refused es vault.hashicorp.com/service o la dirección de Vault.
  • Un solo puerto por regla no escala: si acumulas webhooks, una regla con --rules=tcp:8080,8443,9443 desde el control plane es más fácil de mantener que cinco.

Referencias: vault-k8s #46, vault-k8s #32 y reglas de firewall en clústeres privados en la documentación de GKE.

Ver también