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 kubectl configurado contra el clúster, Helm 3 o superior y jq (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

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 con kubectl logs <pod> -c vault-agent-init. Un error permission denied suele indicar que la ServiceAccount o el namespace del pod no coinciden con bound_service_account_names y bound_service_account_namespaces del 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 en spec.template.metadata.annotations, no en los metadatos del Deployment.
  • vault-0 vuelve a 0/1 tras un reinicio. Vault arranca sellado. Repite el vault operator unseal del paso 2.
  • El pod CSI falla con failed to mount secrets store objects. Ejecuta kubectl describe pod webapp-csi y revisa que secretPath incluye data/ y que secretKey existe 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.