Saltar al contenido principal

Ansible Vault 🔐

Gestión profesional de secretos: contraseñas, claves API y certificados cifrados.

Video pendiente de grabación

El Problema: Secretos en Texto Plano

Imagina que subes tu playbook a Git con las contraseñas de producción en texto claro. Cualquiera que tenga acceso al repositorio (o que hackee tu cuenta) tiene las llaves de tu infraestructura.

💣 La Analogía: La Llave bajo el Felpudo

Dejar contraseñas en texto plano es como dejar la llave de tu casa bajo el felpudo. Todo el mundo sabe que está ahí.

# ❌ NUNCA hagas esto (secretos visibles en Git)
db_password: "SuperSecreto123"
api_key: "sk-1234567890abcdef"
ssl_private_key: "-----BEGIN RSA PRIVATE KEY-----..."

Ansible Vault es la caja fuerte donde guardas esas llaves. Solo quien tiene la combinación puede abrirla.

Conceptos Básicos

Ansible Vault cifra archivos o variables usando AES-256, uno de los estándares de cifrado más robustos que existen.

¿Qué puedes cifrar?

  • Archivos completos: group_vars/all/vault.yml
  • Variables individuales: Una sola variable dentro de un archivo YAML
  • Archivos estáticos: Certificados SSL, claves privadas, configuraciones sensibles

Flujo de trabajo

Comandos Esenciales

Crear un archivo cifrado desde cero

ansible-vault create group_vars/all/vault.yml

Se abre tu editor ($EDITOR) y al guardar, el archivo queda cifrado automáticamente.

Cifrar un archivo existente

# Cifrar un archivo que ya existe
ansible-vault encrypt group_vars/production/secrets.yml

# Cifrar múltiples archivos a la vez
ansible-vault encrypt group_vars/*/vault.yml

Ver un archivo cifrado (sin modificar)

ansible-vault view group_vars/all/vault.yml

Editar un archivo cifrado

ansible-vault edit group_vars/all/vault.yml

Se descifra temporalmente en memoria, abre tu editor, y al guardar lo vuelve a cifrar.

Descifrar un archivo

# Descifrar (vuelve a texto plano)
ansible-vault decrypt group_vars/all/vault.yml

# ⚠️ Cuidado: ¡No hagas commit después de descifrar!

Cambiar la contraseña (Rekey)

# Cambiar la contraseña del Vault
ansible-vault rekey group_vars/all/vault.yml

# Cambiar en múltiples archivos
ansible-vault rekey group_vars/*/vault.yml

El Patrón Profesional: Variables Separadas

Esta es la práctica recomendada por Red Hat y la que verás en entornos profesionales.

🏗️ La Analogía: El Directorio Telefónico y la Caja Fuerte

Imagina que tienes un directorio telefónico (variables públicas) donde cada entrada tiene un nombre y dice "su clave está en la caja fuerte, cajón 42". La caja fuerte (vault) solo contiene las claves, sin contexto.

Estructura de Archivos

proyecto/
├── group_vars/
│ └── all/
│ ├── vars.yml # Variables PÚBLICAS (visible en Git)
│ └── vault.yml # Variables SECRETAS (cifrado con Vault)

Paso 1: Archivo público (vars.yml)

# group_vars/all/vars.yml
# Visible en Git, fácil de auditar

# Base de datos
db_host: "db.ejemplo.com"
db_port: 5432
db_name: "mi_aplicacion"
db_user: "app_user"
db_password: "{{ vault_db_password }}" # Referencia al secreto

# API externa
api_endpoint: "https://api.ejemplo.com/v2"
api_key: "{{ vault_api_key }}" # Referencia al secreto

# SSL
ssl_cert_path: /etc/ssl/certs/app.crt
ssl_key_path: /etc/ssl/private/app.key
ssl_key_content: "{{ vault_ssl_key }}" # Referencia al secreto

Paso 2: Archivo cifrado (vault.yml)

ansible-vault create group_vars/all/vault.yml
# group_vars/all/vault.yml (cifrado)
# Solo contiene los valores sensibles con prefijo vault_

vault_db_password: "P@ssw0rd_Pr0duct10n_2025!"
vault_api_key: "sk-abc123def456ghi789"
vault_ssl_key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA7...
-----END RSA PRIVATE KEY-----

¿Por qué este patrón?

AspectoTodo cifradoPatrón separado
Git diffsInútiles (todo cifrado)Claros para variables públicas
AuditoríaNecesitas descifrar para verVes qué variables existen sin descifrar
Búsquedagrep no funcionagrep db_password vars.yml funciona
Revisión de códigoImposible sin contraseñaSe revisa normalmente

Variables Cifradas Inline

Puedes cifrar una sola variable dentro de un archivo YAML sin cifrar el archivo completo.

# Cifrar un valor individual
ansible-vault encrypt_string 'SuperSecreto123' --name 'db_password'

Salida:

db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
63326634633135663663353566633134613133383865316234616330613066363865
3666346564363532636537393366656465383438643262640a3831333233323131
...

Puedes copiar esa salida directamente en tu archivo de variables:

# group_vars/all/vars.yml
db_host: "db.ejemplo.com"
db_port: 5432
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
63326634633135663663353566633134613133383865316234616330613066363865
...

¿Cuándo usar inline vs archivo completo?

  • Inline: Cuando solo tienes 1-2 secretos en un archivo con muchas variables públicas
  • Archivo completo: Cuando tienes muchos secretos (patrón vault.yml separado)

Vault IDs: Múltiples Contraseñas

En proyectos grandes, necesitas diferentes contraseñas para diferentes entornos. No quieres que el equipo de desarrollo pueda descifrar los secretos de producción.

🏢 La Analogía: Llaves de la Oficina

El becario tiene llave de la sala de reuniones (dev), el ingeniero tiene la del laboratorio (staging), y solo el director tiene la de la caja fuerte (production).

Configuración

# Cifrar con un Vault ID específico
ansible-vault create --vault-id dev@prompt group_vars/development/vault.yml
ansible-vault create --vault-id prod@prompt group_vars/production/vault.yml

# Ejecutar playbook con múltiples Vault IDs
ansible-playbook site.yml \
--vault-id dev@~/.vault_pass_dev \
--vault-id prod@~/.vault_pass_prod

Archivos de contraseña por entorno

# Crear archivos de contraseña (uno por entorno)
echo "contraseña_dev_2025" > ~/.vault_pass_dev
echo "contraseña_prod_2025" > ~/.vault_pass_prod

# Permisos estrictos (IMPORTANTE)
chmod 600 ~/.vault_pass_dev ~/.vault_pass_prod

Configurar en ansible.cfg

# ansible.cfg
[defaults]
vault_identity_list = dev@~/.vault_pass_dev, prod@~/.vault_pass_prod

Con esto ya no necesitas pasar --vault-id en cada ejecución.

Integración con Sistemas Externos

En entornos enterprise, los secretos suelen estar en sistemas dedicados como HashiCorp Vault, AWS Secrets Manager o Azure Key Vault. Ansible Vault puede trabajar con ellos.

Script de Contraseña Personalizado

En lugar de un archivo de texto con la contraseña, puedes usar un script que la obtenga de donde sea.

#!/bin/bash
# vault-password-client.sh
# Obtener la contraseña del Vault desde un gestor de secretos externo

# Ejemplo con HashiCorp Vault
vault kv get -field=ansible_vault_password secret/ansible/vault-pass

# Ejemplo con AWS Secrets Manager
# aws secretsmanager get-secret-value --secret-id ansible-vault-pass --query SecretString --output text

# Ejemplo con macOS Keychain
# security find-generic-password -a ansible -s vault-pass -w
# Hacer el script ejecutable
chmod +x vault-password-client.sh

# Usarlo con Ansible
ansible-playbook site.yml --vault-password-file ./vault-password-client.sh

Buenas Prácticas de Vault

1. Usa el prefijo vault_ para secretos

# ✅ BIEN: prefijo claro
vault_db_password: "secreto"
vault_api_key: "clave"

# ❌ MAL: sin prefijo, confuso
db_password: "secreto" # ¿Es la variable real o la cifrada?

2. Nunca descifres en producción

# ❌ MAL: Descifrar archivo (queda en texto plano)
ansible-vault decrypt secrets.yml
git add . && git commit -m "fix" # ¡Acabas de subir los secretos!

# ✅ BIEN: Solo editar o ver
ansible-vault edit secrets.yml
ansible-vault view secrets.yml

3. Protege el archivo de contraseña

# Permisos restrictivos
chmod 600 ~/.vault_pass

# Añadir a .gitignore
echo ".vault_pass" >> .gitignore
echo "*.vault_pass" >> .gitignore

4. Usa no_log para tareas con secretos

- name: Configurar contraseña de base de datos
mysql_user:
name: app_user
password: "{{ db_password }}"
state: present
no_log: yes # Evita que la contraseña aparezca en los logs

5. Rota secretos periódicamente

# Script de rotación de secretos
#!/bin/bash
# 1. Generar nueva contraseña
NEW_PASS=$(openssl rand -base64 32)

# 2. Actualizar en Vault
ansible-vault edit group_vars/production/vault.yml
# Cambiar vault_db_password por $NEW_PASS

# 3. Desplegar el cambio
ansible-playbook -i inventory/production.ini playbooks/rotate-credentials.yml

# 4. Cambiar contraseña maestra del Vault (cada trimestre)
ansible-vault rekey group_vars/production/vault.yml

Práctica: Proyecto Completo con Vault 🔒

Vamos a crear un proyecto desde cero con gestión profesional de secretos.

Estructura del proyecto

vault-demo/
├── ansible.cfg
├── inventory/
│ ├── dev.ini
│ └── prod.ini
├── group_vars/
│ ├── all/
│ │ ├── vars.yml
│ │ └── vault.yml # Cifrado
│ ├── dev/
│ │ └── vars.yml
│ └── prod/
│ ├── vars.yml
│ └── vault.yml # Cifrado (contraseña diferente)
├── playbooks/
│ └── deploy-app.yml
└── .gitignore

ansible.cfg

[defaults]
inventory = inventory/dev.ini
vault_identity_list = dev@~/.vault_pass_dev, prod@~/.vault_pass_prod

group_vars/all/vars.yml

app_name: mi-aplicacion
app_port: 8080
db_host: "{{ vault_db_host }}"
db_password: "{{ vault_db_password }}"

Crear el Vault

# Crear secretos para desarrollo
ansible-vault create --vault-id dev@prompt group_vars/all/vault.yml

# Contenido:
# vault_db_host: "localhost"
# vault_db_password: "dev_password_123"

# Crear secretos para producción
ansible-vault create --vault-id prod@prompt group_vars/prod/vault.yml

# Contenido:
# vault_db_host: "db.produccion.empresa.com"
# vault_db_password: "Pr0d_S3cur3_P@ss!"

playbooks/deploy-app.yml

- name: Desplegar aplicación con secretos
hosts: all
become: yes

tasks:
- name: Crear archivo de configuración con secretos
template:
src: templates/app.conf.j2
dest: "/etc/{{ app_name }}/config.yml"
mode: '0600'
owner: root
no_log: yes

- name: Mostrar configuración (sin secretos)
debug:
msg: |
App: {{ app_name }}
Puerto: {{ app_port }}
DB Host: {{ db_host }}
DB Password: ******** (oculto por seguridad)

.gitignore

.vault_pass*
*.retry
*.pyc
__pycache__/

Ejecución

# Desarrollo (usa contraseña de dev automáticamente)
ansible-playbook -i inventory/dev.ini playbooks/deploy-app.yml

# Producción (usa contraseña de prod)
ansible-playbook -i inventory/prod.ini playbooks/deploy-app.yml

Gestión Segura de Claves SSH

Ansible se conecta a los hosts por SSH. La forma en que gestiones esas claves es el primer eslabón de tu cadena de seguridad: si un atacante consigue tu clave privada, todo lo demás (Vault incluido) deja de servir de mucho.

Reglas básicas

  1. Nunca contraseñas SSH en texto plano. Usa autenticación por clave pública.
  2. Una clave por contexto: humano, CI/CD, jump host. No reutilices.
  3. Usa ed25519 salvo que tengas un motivo concreto para RSA.
  4. Protege con passphrase las claves de humanos. Para CI puede no tener passphrase, pero entonces rota y limita el blast radius.
  5. No comitees claves privadas al repositorio. Jamás. Ni cifradas con Vault.

Generar y distribuir claves desde Ansible

- name: Generar clave SSH dedicada al deploy
community.crypto.openssh_keypair:
path: ~/.ssh/deploy_ed25519
type: ed25519
comment: "deploy@{{ ansible_date_time.iso8601 }}"
delegate_to: localhost
run_once: true

- name: Asegurar usuario "deploy" con su clave pública
ansible.builtin.user:
name: deploy
shell: /bin/bash
groups: sudo

- name: Autorizar la clave pública en cada host
ansible.posix.authorized_key:
user: deploy
state: present
key: "{{ lookup('file', '~/.ssh/deploy_ed25519.pub') }}"
exclusive: false

Pasar la clave privada a Ansible

Tres opciones, ordenadas de menos a más seguras:

MecanismoCuándo usarlo
--private-key=ruta/clavePruebas locales rápidas
ansible_ssh_private_key_file en group_varsSetup multi-entorno controlado por inventario
ssh-agentLo recomendado: la clave nunca toca disco en claro durante la ejecución
# Opción ssh-agent (lo más seguro)
eval $(ssh-agent)
ssh-add ~/.ssh/deploy_ed25519 # te pide la passphrase una vez
ansible-playbook -i inventories/prod playbooks/deploy.yml

Bastion / jump host

Cuando los hosts de producción no tienen IP pública, usas un bastion. Configúralo en ~/.ssh/config y Ansible lo respetará automáticamente:

# ~/.ssh/config
Host bastion
HostName bastion.example.com
User jump
IdentityFile ~/.ssh/deploy_ed25519

Host 10.0.*.*
User deploy
ProxyJump bastion
IdentityFile ~/.ssh/deploy_ed25519

O bien explícito en el inventario:

all:
vars:
ansible_ssh_common_args: '-o ProxyJump=jump@bastion.example.com'

Rotación de claves

Una clave que nunca rota es una clave comprometida que aún no lo sabes. Patrón típico:

- name: Añadir clave nueva
ansible.posix.authorized_key:
user: deploy
key: "{{ lookup('file', 'keys/deploy_2026.pub') }}"
state: present

- name: (Tras validar) eliminar clave antigua
ansible.posix.authorized_key:
user: deploy
key: "{{ lookup('file', 'keys/deploy_2025.pub') }}"
state: absent

🎯 Buena práctica: rota claves de CI cada 90 días y cualquier vez que cambie un miembro del equipo con acceso.

Variables Sensibles y no_log

Vault cifra los datos en reposo (en el repositorio). Pero si tu playbook imprime una contraseña en pantalla durante la ejecución, ese secreto acaba en los logs del CI, en el historial de la terminal y posiblemente en un sistema de observabilidad. Aquí entra no_log.

El problema

- name: Crear usuario con contraseña
ansible.builtin.user:
name: app
password: "{{ vault_app_password | password_hash('sha512') }}"

Por defecto Ansible imprime los parámetros de cada tarea en modo verbose (-v). Esa contraseña acabará en los logs aunque venga de Vault.

La solución

- name: Crear usuario con contraseña
ansible.builtin.user:
name: app
password: "{{ vault_app_password | password_hash('sha512') }}"
no_log: true # 👈 silencia esta tarea

Cuando no_log: true:

  • Los parámetros y el resultado de la tarea no aparecen en stdout ni en los logs.
  • El usuario sólo ve [censored due to no_log].
  • Si la tarea falla, el mensaje de error también se censura → diagnóstico más complicado, úsalo con criterio.

Patrón: no_log selectivo en bucles

Si tienes un bucle que procesa una lista con secretos mezclados con datos públicos, usa no_log: "{{ item.secret | bool }}":

- name: Configurar credenciales mixtas
ansible.builtin.lineinfile:
path: /etc/app/creds
line: "{{ item.key }}={{ item.value }}"
loop:
- { key: APP_NAME, value: notastack, secret: false }
- { key: APP_TOKEN, value: "{{ vault_app_token }}", secret: true }
no_log: "{{ item.secret }}"

Variables sensibles desde el entorno (sin Vault)

Para secretos efímeros (token de un PR, contraseña de un test concreto) puedes evitar tocar Vault y leerlos de variables de entorno:

- name: Login en registry usando GH_TOKEN del entorno
community.docker.docker_login:
registry_url: ghcr.io
username: "{{ lookup('env', 'GH_USER') }}"
password: "{{ lookup('env', 'GH_TOKEN') }}"
no_log: true

En CI, esas variables vienen de los secretos del runner (GitHub Secrets, Jenkins Credentials), nunca del repositorio.

Anti-patterns que conviene evitar

Anti-patternPor qué es maloAlternativa
debug: var=vault_password para depurarImprime el secreto en pantalla y logsUsa --check o un assert que no exponga el valor
Pasar secretos como extra-vars (-e password=...)Quedan en el historial de la shell y en logs del CIVariables de entorno + Vault
command: "mysql -p{{ vault_pass }} ..."El secreto va en la línea de comandos del proceso, visible con psUsa el módulo nativo (community.mysql.mysql_user) o un fichero de credenciales temporal
Secretos en group_vars/all.yml sin cifrarAcaban en Git tarde o tempranogroup_vars/all/vault.yml cifrado con ansible-vault
Reutilizar la misma clave SSH para humanos y CIComprometer al CI compromete a todos los humanosUna clave por identidad

Checklist rápido antes de hacer push

  • No hay claves privadas (*.pem, id_*, *.key) en el árbol de Git.
  • Todos los vault_* viven en */vault.yml y están cifrados (ansible-vault view falla con contraseña incorrecta).
  • Las tareas que manejan secretos tienen no_log: true.
  • La pipeline no expone variables con set -x ni echo $SECRET.
  • Los register: de tareas con secretos también se filtran (no_log aplica al register).
  • git diff antes de cada commit (¡un grep rápido nunca está de más!).

📝 Resumen del Capítulo

En este capítulo has aprendido:

Cifrado AES-256: Proteger archivos y variables con Ansible Vault ✅ Comandos esenciales: create, encrypt, decrypt, edit, view, rekey ✅ Patrón profesional: Separar variables públicas y secretas con prefijo vault_Variables inline: Cifrar valores individuales con encrypt_stringVault IDs: Usar diferentes contraseñas por entorno ✅ Integración externa: Conectar con HashiCorp Vault, AWS Secrets Manager ✅ Claves SSH: Generación, distribución, ssh-agent, bastion y rotación ✅ no_log: Evitar fugas de secretos en logs y stdout ✅ Anti-patterns: Errores típicos al manejar credenciales en Ansible ✅ no_log: Evitar que los secretos aparezcan en los logs ✅ Buenas prácticas: Rotación, permisos, .gitignore

Próximo paso: Reutilizar y organizar código con Include, Import y control avanzado de tareas 📦