Saltar al contenido principal

Seguridad en Ansible: Vault, SSH y credenciales

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

Video pendiente de grabación

El problema: secretos en texto plano

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

💣 La analogía: la llave bajo el felpudo

Dejar contraseñas en texto plano es como dejar la llave de nuestra 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 guardamos 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é podemos 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 nuestro 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 nuestro 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 veremos en entornos profesionales.

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-----

Ahora ya podríamos ejecutar nuestro playbook normalmente, y Ansible se encargará de descifrar vault.yml en memoria para resolver las referencias en vars.yml.

Podríamos pasar la contraseña del Vault con --ask-vault-pass o usar un archivo de contraseña con --vault-password-file para automatizarlo en CI/CD.

Podemos crear el fichero con la contraseña:

chmod 600 ~/.vault_pass

¿Por qué este patrón?

AspectoTodo cifradoPatrón separado
Git diffsInútiles (todo cifrado)Claros para variables públicas
AuditoríaNecesitamos descifrar para verVemos 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

Podemos 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
...

Podemos copiar esa salida directamente en nuestro 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 tenemos 1-2 secretos en un archivo con muchas variables públicas
  • Archivo completo: Cuando tenemos muchos secretos (patrón vault.yml separado)

Vault IDs: múltiples contraseñas

En proyectos grandes, necesitamos diferentes contraseñas para diferentes entornos. No queremos 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 necesitamos 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, podemos 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

Usamos 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?

Nunca descifremos 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

Protegemos el archivo de contraseña

# Permisos restrictivos
chmod 600 ~/.vault_pass

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

Usamos 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

Rotamos 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.yml 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: quotes
app_image: pabpereza/quotes:latest
app_port: 8000
postgres_host: "{{ vault_db_host }}"
postgres_user: "{{ vault_db_user }}"
postgres_password: "{{ vault_db_password }}"
postgres_db: "{{ vault_db_name }}"

Crear el Vault

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

# Contenido:
# vault_db_host: "postgres"
# vault_db_user: "quotes"
# vault_db_password: "dev_password_123"
# vault_db_name: "quotes"

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

# Contenido:
# vault_db_host: "postgres.prod.internal"
# vault_db_user: "quotes"
# vault_db_password: "Pr0d_S3cur3_P@ss!"
# vault_db_name: "quotes"

playbooks/deploy-app.yml

- name: Desplegar aplicación como contenedor con secretos
hosts: all
become: true

tasks:
- name: Desplegar contenedor quotes con credenciales inyectadas como env vars
community.docker.docker_container:
name: "{{ app_name }}"
image: "{{ app_image }}"
state: started
restart_policy: unless-stopped
pull: true
ports:
- "{{ app_port }}:8000"
env:
POSTGRES_HOST: "{{ postgres_host }}"
POSTGRES_USER: "{{ postgres_user }}"
POSTGRES_PASSWORD: "{{ postgres_password }}"
POSTGRES_DB: "{{ postgres_db }}"
no_log: true

- name: Mostrar configuración (sin secretos)
debug:
msg: |
App: {{ app_name }}
Puerto: {{ app_port }}
Postgres Host: {{ postgres_host }}
Postgres 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

📝 Resumen del capítulo

En este capítulo hemos 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 📦