Los Secret nativos de Kubernetes se guardan en etcd codificados en base64, que no es cifrado, y cualquiera con permiso de lectura sobre el namespace puede verlos. HashiCorp Vault centraliza los secretos, los cifra, controla quién accede a cada uno mediante políticas y registra cada acceso. En este tutorial instalarás Vault en un clúster de Kubernetes con Helm, guardarás un secreto en el motor KV, permitirás que los pods se autentiquen con su ServiceAccount y entregarás ese secreto a una aplicación de dos formas: con el Vault Agent Injector y con el driver CSI de Secrets Store.
Requisitos previos
Para seguir esta guía necesitas:
- Un clúster de Kubernetes 1.29 o superior con al menos un nodo worker con 2 GB de RAM libres, por ejemplo uno desplegado sobre VPS de CubePath.
- Una StorageClass por defecto, ya que Vault guarda sus datos en un volumen persistente. Compruébalo con
kubectl get storageclass. - Una estación de trabajo (tu portátil o un servidor Ubuntu 24.04) con
kubectlconfigurado contra el clúster, Helm 3 o superior yjq(sudo apt install jq).
Todos los comandos se ejecutan desde esa estación de trabajo.
Paso 1: Instalar Vault con Helm
HashiCorp publica un chart oficial que despliega el servidor de Vault, el Agent Injector (un webhook que añade un contenedor de Vault a los pods que lo piden) y, opcionalmente, el proveedor CSI. Añade el repositorio:
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
Instala Vault en su propio namespace. Por defecto el chart crea un único servidor en modo standalone con almacenamiento en disco, que es suficiente para este tutorial. Activa también el proveedor CSI, que usarás en el paso 7:
helm install vault hashicorp/vault \
--namespace vault --create-namespace \
--set csi.enabled=true
Comprueba los pods:
kubectl get pods -n vault
NAME READY STATUS RESTARTS AGE
vault-0 0/1 Running 0 45s
vault-agent-injector-7d8b5c9f4d-x2kqp 1/1 Running 0 45s
vault-csi-provider-8h4tw 2/2 Running 0 45s
vault-0 aparece como 0/1: es normal, la sonda de readiness falla mientras Vault no esté inicializado y desbloqueado.
Paso 2: Inicializar y desbloquear Vault
Al inicializar Vault se genera la clave maestra, dividida en varias claves de desbloqueo (unseal keys), y un token root. Para este tutorial usarás una sola clave:
kubectl exec -n vault vault-0 -- vault operator init \
-key-shares=1 -key-threshold=1 -format=json > vault-keys.json
Advertencia
vault-keys.jsoncontiene la única forma de desbloquear Vault y un token con acceso total. Guárdalo en un gestor de contraseñas y bórralo del disco al terminar. En producción usa varias claves (por ejemplo-key-shares=5 -key-threshold=3) repartidas entre personas distintas, o el auto-unseal con un KMS.
Desbloquea Vault con la clave generada:
VAULT_UNSEAL_KEY=$(jq -r '.unseal_keys_b64[0]' vault-keys.json)
kubectl exec -n vault vault-0 -- vault operator unseal "$VAULT_UNSEAL_KEY"
En la salida, Sealed debe valer false:
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
...
Pasados unos segundos, kubectl get pods -n vault mostrará vault-0 como 1/1. Cada vez que el pod se reinicie volverá a arrancar sellado y tendrás que repetir el unseal.
Paso 3: Guardar un secreto en el motor KV
Muestra el token root para iniciar sesión:
jq -r '.root_token' vault-keys.json
Abre una shell dentro del pod de Vault. Los pasos 3, 4 y 5 se ejecutan en esta shell:
kubectl exec -it -n vault vault-0 -- /bin/sh
Inicia sesión pegando el token root cuando te lo pida:
vault login
Activa el motor de secretos KV versión 2 en la ruta secret/ y guarda las credenciales de ejemplo de una aplicación llamada webapp. Sustituye los valores por los tuyos:
vault secrets enable -path=secret kv-v2
vault kv put secret/webapp/config username="webapp" password="your_strong_password"
Léelo para confirmar que se ha guardado:
vault kv get secret/webapp/config
====== Secret Path ======
secret/data/webapp/config
...
====== Data ======
Key Value
--- -----
password your_strong_password
username webapp
Fíjate en la ruta real, secret/data/webapp/config: en KV v2 las políticas y las anotaciones usan siempre el segmento data/.
Paso 4: Activar la autenticación de Kubernetes
El método de autenticación kubernetes permite que un pod presente el token de su ServiceAccount y reciba a cambio un token de Vault. Vault valida ese token contra la API de Kubernetes. Como Vault corre dentro del propio clúster, basta con indicarle la dirección de la API; usará su propia ServiceAccount y el certificado de la CA montados en el pod:
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
Success! Enabled kubernetes auth method at: kubernetes/
Success! Data written to: auth/kubernetes/config
Paso 5: Crear una política y un rol
Una política define qué rutas puede leer quien la tenga. Crea una de solo lectura para el secreto de webapp:
vault policy write webapp - <<EOF
path "secret/data/webapp/config" {
capabilities = ["read"]
}
EOF
Un rol une la política con una identidad de Kubernetes concreta. Este rol solo acepta pods que usen la ServiceAccount webapp en el namespace default:
vault write auth/kubernetes/role/webapp \
bound_service_account_names=webapp \
bound_service_account_namespaces=default \
policies=webapp \
ttl=1h
Verifica el rol y sal de la shell del pod:
vault read auth/kubernetes/role/webapp
exit
Paso 6: Inyectar el secreto con el Vault Agent Injector
El Agent Injector vigila los pods nuevos. Si llevan la anotación vault.hashicorp.com/agent-inject: "true", les añade un contenedor init que se autentica en Vault y escribe los secretos en un volumen compartido en /vault/secrets/, y un contenedor sidecar que los renueva.
Crea el manifiesto de la aplicación:
nano webapp.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: webapp
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "webapp"
vault.hashicorp.com/agent-inject-secret-database.env: "secret/data/webapp/config"
vault.hashicorp.com/agent-inject-template-database.env: |
{{- with secret "secret/data/webapp/config" -}}
DB_USER={{ .Data.data.username }}
DB_PASSWORD={{ .Data.data.password }}
{{- end }}
spec:
serviceAccountName: webapp
containers:
- name: app
image: nginx:1.27
ports:
- containerPort: 80
La anotación agent-inject-secret-database.env indica qué secreto leer y el nombre del fichero que se creará. La anotación agent-inject-template-database.env define su formato; sin ella el agente escribiría el secreto en un formato interno poco práctico.
Aplica el manifiesto y espera a que el pod arranque:
kubectl apply -f webapp.yaml
kubectl get pods -l app=webapp
NAME READY STATUS RESTARTS AGE
webapp-6b9f7c8d5b-lq2wz 2/2 Running 0 20s
El pod tiene 2/2 contenedores: tu aplicación y el sidecar vault-agent. Comprueba el fichero que ve la aplicación:
kubectl exec deploy/webapp -c app -- cat /vault/secrets/database.env
DB_USER=webapp
DB_PASSWORD=your_strong_password
La aplicación puede cargar ese fichero al arrancar. El secreto nunca pasa por un objeto Secret de Kubernetes ni por etcd.
Paso 7: Montar el secreto con el driver CSI
El driver CSI de Secrets Store es una alternativa sin sidecar: el kubelet monta los secretos como ficheros al crear el pod, a través del proveedor de Vault que activaste en el paso 1. Primero instala el driver, que es un proyecto de Kubernetes SIGs:
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm repo update
helm install csi secrets-store-csi-driver/secrets-store-csi-driver --namespace kube-system
Comprueba que el DaemonSet está listo en todos los nodos:
kubectl get pods -n kube-system -l app=secrets-store-csi-driver
Crea un SecretProviderClass que indique qué secreto leer y con qué rol:
nano webapp-csi.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-webapp
namespace: default
spec:
provider: vault
parameters:
vaultAddress: "http://vault.vault:8200"
roleName: "webapp"
objects: |
- objectName: "db-password"
secretPath: "secret/data/webapp/config"
secretKey: "password"
---
apiVersion: v1
kind: Pod
metadata:
name: webapp-csi
namespace: default
spec:
serviceAccountName: webapp
containers:
- name: app
image: nginx:1.27
volumeMounts:
- name: secrets
mountPath: /mnt/secrets
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: vault-webapp
El pod usa la misma ServiceAccount webapp, así que el rol del paso 5 le sirve. Aplica el manifiesto y lee el fichero montado:
kubectl apply -f webapp-csi.yaml
kubectl wait --for=condition=Ready pod/webapp-csi --timeout=60s
kubectl exec webapp-csi -- cat /mnt/secrets/db-password
your_strong_password
Cuál elegir: el Agent Injector permite plantillas y renueva los secretos durante la vida del pod; el driver CSI no añade contenedores extra y encaja mejor si solo necesitas ficheros sencillos.
Solución de problemas
- El pod se queda en
Init:0/1. Revisa el contenedor init del agente conkubectl logs <pod> -c vault-agent-init. Un errorpermission deniedsuele indicar que la ServiceAccount o el namespace del pod no coinciden conbound_service_account_namesybound_service_account_namespacesdel rol. - El pod no recibe el sidecar. Comprueba que el injector está en ejecución (
kubectl get pods -n vault) y que las anotaciones están enspec.template.metadata.annotations, no en los metadatos del Deployment. vault-0vuelve a0/1tras un reinicio. Vault arranca sellado. Repite elvault operator unsealdel paso 2.- El pod CSI falla con
failed to mount secrets store objects. Ejecutakubectl describe pod webapp-csiy revisa quesecretPathincluyedata/y quesecretKeyexiste en el secreto.
Conclusión
Tienes Vault funcionando en Kubernetes, con los secretos cifrados fuera de etcd y cada aplicación limitada a sus propias rutas mediante políticas y roles ligados a su ServiceAccount. Los pods reciben las credenciales como ficheros, ya sea por el Agent Injector o por el driver CSI. Como siguientes pasos, puedes desplegar Vault en alta disponibilidad con almacenamiento Raft integrado (server.ha.enabled=true y server.ha.raft.enabled=true en el chart), configurar el auto-unseal y activar el motor database para generar credenciales de base de datos dinámicas que caducan solas.
