SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

Ansible Vault: cifrar secretos en Git de forma segura

Aprenda a cifrar archivos vars o valores inline con Ansible Vault, separar staging y producción, y cambiar claves sin dejar secretos en texto plano en Git.

Qué protege Ansible Vault y qué no protege

Ansible Vault cifra los secretos dentro del repositorio del playbook. Así, git almacena texto cifrado en lugar de una contraseña en texto plano. El comando ansible-vault cifra un archivo completo o un valor individual dentro de un archivo mediante una clave simétrica derivada de la contraseña que elija. Ansible descifra ese contenido en memoria cuando se ejecuta el play, por lo que la variable se comporta como cualquier otra variable.

Este modelo tiene un límite claro. Vault protege un secreto en reposo dentro del repositorio y nada más. Cuando se ejecuta una tarea, el valor está en texto plano en la memoria, en la plantilla renderizada, en los argumentos del módulo y en la salida de la ejecución, salvo que lo impida. Todas las personas que pueden ejecutar el playbook tienen la contraseña de Vault. Por tanto, Vault protege los secretos frente a personas ajenas al equipo, pero no proporciona control de acceso individual dentro del equipo.

Si todavía no ha escrito un playbook, empiece con un primer playbook de Ansible contra un VPS y vuelva aquí cuando ese playbook necesite una contraseña.

¿Cifrar un archivo completo o una sola cadena?

ansible-vault encrypt reemplaza un archivo por texto cifrado. El archivo se convierte en un único bloque de texto en base64 bajo una línea de encabezado que comienza por $ANSIBLE_VAULT. Úselo cuando el archivo sólo contenga secretos.

ansible-vault encrypt_string cifra un valor y muestra un fragmento YAML que puede pegar en un archivo de variables normal. El nombre de la variable permanece legible y sólo el valor queda cifrado. Úselo cuando los secretos estén junto a opciones de configuración en texto plano.

La diferencia importante en el trabajo diario es el diff. Un archivo vault se vuelve a cifrar con un salt aleatorio nuevo cada vez que se guarda, por lo que cambian todos los bytes del texto cifrado. git diff muestra entonces un bloque ilegible reemplazado por otro bloque ilegible. El revisor no puede saber si se rotó una contraseña o si se reescribió el archivo. Con encrypt_string, cada secreto es su propio bloque dentro de un archivo de texto plano. Por eso, un diff muestra exactamente qué variable cambió y deja intacto el resto del archivo.

El formato inline tiene un coste que aparece al rotar secretos: ansible-vault rekey no modifica los bloques inline. Elija el formato de archivo cuando la lista de secretos sea larga y cambie pocas veces. Elija el formato inline cuando el archivo mezcle secretos con variables normales y quiera que la revisión de código sea útil.

La estructura de group_vars muestra qué está protegido

Ansible carga group_vars/<group>.yml y también carga todos los archivos que están dentro de un directorio group_vars/<group>/. La forma basada en directorios es la adecuada porque permite que un mismo grupo contenga un archivo de texto plano y otro cifrado.

inventory/
  hosts.ini
group_vars/
  all/
    vars.yml
    vault.yml
  web/
    vars.yml
    vault.yml
host_vars/
  db01/
    vars.yml
    vault.yml
playbooks/
  site.yml

Cada vault.yml está cifrado. Cada vars.yml está en texto plano. El lector puede saber qué valores están protegidos sin abrir ningún archivo, porque el nombre lo indica.

La segunda parte del patrón es la indirección. Dentro del archivo cifrado, anteponga vault_ a cada variable.

vault_db_password: "a real password"
vault_grafana_admin_token: "a real token"

Después, haga referencia a esos nombres desde el archivo de texto plano contiguo.

db_password: "{{ vault_db_password }}"
grafana_admin_token: "{{ vault_grafana_admin_token }}"

Los roles y las plantillas usan db_password y nunca necesitan saber de dónde procede el valor, lo que mantiene limpia la separación entre un playbook y un rol. El archivo de texto plano vars.yml también funciona como un índice consultable: grep -r vault_ group_vars/ enumera todos los secretos que espera el repositorio, sin descifrar nada. El coste es usar un nombre adicional por secreto. Además, un error tipográfico en un nombre vault_ aparece en tiempo de ejecución como una variable no definida, no como un error de sintaxis.

Cifre una variable con encrypt_string

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  --stdin-name 'vault_db_password'

Escriba el secreto y pulse Ctrl-D. --stdin-name lee el valor desde la entrada estándar, por lo que no queda registrado en el archivo del historial del shell. La otra forma incluye el valor en la línea de comandos, donde el shell lo registra:

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  'a real password' --name 'vault_db_password'

En ambos casos, el comando muestra un bloque YAML. Péguelo en el archivo vars exactamente como se muestra, porque la indentación bajo la etiqueta !vault forma parte del valor.

vault_db_password: !vault |
          $ANSIBLE_VAULT;1.2;AES256;prod
          6638643965323633646262656665306333616466396630323136393465356136396436383331
          3131303163306665326539353837343663313762616561306534373963383531613664393332

La etiqueta !vault indica al cargador YAML que el escalar es texto cifrado y no texto sin cifrar. La cabecera contiene la versión del formato, el cifrado y la etiqueta del ID del vault que lo cifró. Un valor cifrado sin ID de vault contiene una cabecera 1.1 sin etiqueta. También funciona, pero proporciona menos información sobre el origen de la contraseña.

¿Dónde se guarda la contraseña del vault?

Fuera del repositorio. Esta es la única regla sin excepciones.

--ask-vault-pass solicita la contraseña una vez por ejecución y no almacena nada. Es adecuado para un portátil, pero no para una tarea de cron ni para un runner de CI.

Un archivo de contraseñas es un archivo de texto plano cuya primera línea contiene la contraseña. Créelo vacío con permisos restrictivos y, después, rellénelo con un editor para que la contraseña no llegue al historial del shell:

mkdir -p ~/.ansible
install -m 600 /dev/null ~/.ansible/vault-prod.txt
$EDITOR ~/.ansible/vault-prod.txt

Indique ese archivo a cualquier comando con --vault-password-file:

ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
  --vault-password-file ~/.ansible/vault-prod.txt

Repetir esa opción en cada comando es fácil de olvidar, así que establézcala una vez en ansible.cfg, en la raíz del repositorio.

[defaults]
inventory = inventory/hosts.ini
vault_password_file = ~/.ansible/vault-prod.txt

La misma configuración lee la variable de entorno ANSIBLE_VAULT_PASSWORD_FILE, que es la forma habitual en que un trabajo de CI proporciona la contraseña. El trabajo escribe la contraseña desde su propio almacén de credenciales en un archivo de un directorio temporal, exporta la variable y elimina el archivo cuando termina la ejecución. Añada también el patrón de nombre de archivo a .gitignore, porque la ruta de ansible.cfg se confirma en el repositorio y, tarde o temprano, alguien creará el archivo real dentro de la copia de trabajo.

Si el archivo de contraseñas es ejecutable, Ansible lo ejecuta y lee la contraseña de su salida estándar en lugar de leer el archivo como texto. Así puede obtener la contraseña del vault desde un llavero del sistema o desde un gestor de secretos en la nube sin escribirla en el disco. Un script utilizado mediante --vault-id tiene requisitos adicionales: su nombre debe terminar en -client o en -client seguido de una extensión, debe ser ejecutable, debe aceptar una opción --vault-id y debe imprimir la contraseña en la salida estándar.

Dos identificadores de vault: staging y production

Un identificador de vault es una etiqueta asociada a una contraseña de vault, escrita como label@source. El origen es prompt: la ruta a un archivo de contraseñas o la ruta a un script cliente. Las etiquetas permiten que un repositorio almacene secretos protegidos con más de una contraseña, de modo que la contraseña de staging no abra el archivo de production.

ansible-vault encrypt --vault-id staging@~/.ansible/vault-staging.txt \
  group_vars/staging/vault.yml
ansible-vault encrypt --vault-id prod@~/.ansible/vault-prod.txt \
  group_vars/prod/vault.yml

Pase todos los identificadores que pueda necesitar la ejecución:

ansible-playbook playbooks/site.yml \
  --vault-id staging@~/.ansible/vault-staging.txt \
  --vault-id prod@~/.ansible/vault-prod.txt

O enumérelos una sola vez en ansible.cfg:

[defaults]
vault_identity_list = staging@~/.ansible/vault-staging.txt, prod@~/.ansible/vault-prod.txt

Hay un comportamiento que sorprende a muchas personas. De forma predeterminada, la etiqueta es una indicación, no una restricción. Ansible prueba todos los secretos que tiene disponibles contra el archivo hasta que uno lo descifra. Por tanto, un archivo etiquetado como staging también se abre si la contraseña de production resulta ser la clave correcta. Establezca vault_id_match = True en [defaults], o la variable de entorno ANSIBLE_VAULT_ID_MATCH, para que Ansible use sólo el secreto cuya etiqueta coincida con la cabecera del archivo. Esta comprobación necesita la cabecera 1.2, por lo que sólo se aplica al contenido cifrado originalmente con un identificador de vault.

Cuando hay más de un identificador cargado, ansible-vault encrypt ya no sabe con qué contraseña debe cifrar. Indíquelo con --encrypt-vault-id prod, o establezca vault_encrypt_identity en ansible.cfg para que el repositorio tenga un valor predeterminado.

La ventaja está en el alcance del despliegue. Un trabajo de CI que despliega staging recibe únicamente la contraseña de staging, por lo que un runner comprometido no puede leer las credenciales de production. Cuando ejecute playbooks en un conjunto de servidores Linux desde una sola máquina de control, esta separación marca la diferencia entre un incidente limitado y uno muy grave.

Cambiar la clave del vault cuando alguien deja el equipo

Cambiar la clave modifica la contraseña del vault y vuelve a cifrar su contenido con la nueva contraseña. No revierte nada. Cualquiera que haya tenido la contraseña anterior todavía puede descifrar cualquier copia del repositorio que conserve, incluidos todos los commits antiguos de esa copia. Por tanto, considere que la contraseña del vault está comprometida desde el momento en que su titular deja el equipo y haga la rotación en este orden.

  1. Cambie las credenciales reales en los servidores y en los servicios de terceros. Este paso es el que revoca el acceso.
  2. Escriba los nuevos valores en los archivos del vault con ansible-vault edit.
  3. Cambie la clave de cada archivo cifrado con una nueva contraseña del vault.
  4. Entregue la nueva contraseña del vault a las personas que todavía la necesiten, mediante un canal que no sea el repositorio.
ansible-vault rekey --vault-id prod@~/.ansible/vault-prod-old.txt \
  --new-vault-id prod@prompt \
  group_vars/prod/vault.yml host_vars/db01/vault.yml

rekey acepta varios archivos en un solo comando, y --new-vault-id prod@prompt solicita la nueva contraseña una vez en lugar de leerla del disco. Mantenga la misma etiqueta salvo que tenga un motivo para cambiarla, porque la etiqueta se escribe en la cabecera de cada archivo que el comando vuelve a escribir.

Aquí es donde el formato en línea tiene un coste. ansible-vault rekey funciona con archivos completamente cifrados, por lo que un bloque !vault dentro de un archivo de variables en texto plano no se modifica. Localícelos primero y, después, vuelva a generar cada uno con encrypt_string usando la nueva contraseña:

grep -rl '!vault' group_vars/ host_vars/

Ese es el coste completo. Los bloques en línea permiten revisar diferencias legibles, pero requieren una pasada manual durante la rotación. Los archivos completamente cifrados se rotan con un solo comando y no ofrecen información útil durante la revisión.

Por qué el secreto sigue apareciendo en la salida

Vault deja de proteger el valor en cuanto se descifra. Ansible informa del resultado de una tarea, y un módulo que repite sus argumentos incluye la credencial en ese informe. Una ejecución detallada, un --diff en una tarea de plantilla, una tarea fallida que muestre sus argumentos o un plugin de callback que escriba la salida en un archivo conservarán el texto sin cifrar. Cifrar el archivo no evita ninguno de estos casos.

no_log: true es la opción que debe activar. Configúrela en cualquier tarea que reciba una credencial.

- name: Write the application environment file
  ansible.builtin.template:
    src: app.env.j2
    dest: /etc/myapp/app.env
    owner: myapp
    group: myapp
    mode: "0600"
  no_log: true

Ansible omite el resultado de esa tarea en la salida. El registro indica que la tarea se ejecutó, pero no qué datos procesó. Configúrela especialmente en los bucles, porque un bucle informa de un resultado por cada elemento, y un bucle sobre una lista de credenciales informa de la lista completa.

Hay otros cuatro lugares por los que puede escaparse un secreto descifrado. no_log no cubre ninguno de ellos:

  • Un archivo generado a partir de una plantilla hereda los mode y owner que se le asignaron. Configure mode: "0600" y un propietario específico para cualquier archivo que contenga una credencial. De lo contrario, el secreto puede quedar legible para todos los usuarios del host de destino.
  • Un secreto pasado a ansible.builtin.command o ansible.builtin.shell aparece en la lista de procesos del host de destino mientras se ejecuta el comando. Cualquier usuario local puede leerlo. Páselo mediante un archivo o una variable de entorno.
  • El almacenamiento en caché de facts escribe en disco los facts recopilados en la máquina de control. Por tanto, una variable registrada que contenga un secreto puede terminar en un archivo de caché que nadie considere sensible.
  • El mismo secreto suele existir en un segundo lugar, como un archivo de entorno que lee un contenedor. Las reglas son distintas en ese caso, y mantener las credenciales fuera de los archivos env de Compose explica esa parte.

no_log dificulta la depuración. Esa es precisamente su función. Quítelo temporalmente en un host de prueba cuando una tarea falle y vuelva a configurarlo antes de aplicar el cambio en producción.

Leer y editar archivos cifrados sin dejar texto sin cifrar

ansible-vault view group_vars/prod/vault.yml descifra el contenido en un paginador y no escribe nada en disco. ansible-vault edit descifra el contenido en un archivo temporal, abre el archivo con $EDITOR y lo vuelve a cifrar al cerrarlo. Prefiera ambos métodos a ansible-vault decrypt, que deja un archivo sin cifrar en el árbol de trabajo. Añadir por accidente un archivo de un almacén descifrado al área de preparación es la forma más habitual de que una credencial real llegue a un repositorio público.

Git puede mostrar un diff legible de archivos completamente cifrados mientras los descifra:

git config --local diff.ansible-vault.textconv "ansible-vault view --vault-password-file ~/.ansible/vault-prod.txt"
printf '%s\n' 'group_vars/**/vault.yml diff=ansible-vault' >> .gitattributes

Comprenda lo que hace antes de habilitarlo. git diff imprimirá secretos de producción en su terminal, por lo que quedarán en el historial de desplazamiento y en cualquier pantalla compartida. Es una comodidad local para una persona en un equipo, así que mantenga git config local y tenga en cuenta que los clones de otras personas se comportarán de otra forma si no configuran lo mismo.

Cuando Vault deja de ser la herramienta adecuada

Vault es un formato de archivo con una contraseña por etiqueta, y esa estructura determina dónde deja de ser suficiente. Cambie a un almacén de secretos real cuando se cumpla cualquiera de las siguientes condiciones.

  • Necesita acceso por persona. Todas las personas que ejecutan el playbook tienen la misma contraseña, y los vault IDs separan el acceso por entorno, nunca por persona.
  • Necesita un registro de auditoría. Vault no registra quién descifró qué ni cuándo.
  • Necesita rotación programada. Vault no tiene caducidad ni versionado, por lo que nada indica que una credencial no haya cambiado en dos años.
  • La propia aplicación necesita el secreto durante la ejecución. Un servicio que lee la contraseña de su base de datos al arrancar no debería leerla del repositorio de despliegue.

El patrón se invierte. Ansible deja de almacenar secretos y empieza a obtenerlos durante la ejecución mediante un plugin de lookup, desde HashiCorp Vault (otro producto con un nombre confusamente similar), el gestor de secretos de un proveedor cloud o un keyring del equipo de control. El repositorio contiene una ruta, el almacén contiene el valor y el almacén conserva el registro de acceso. Para un equipo pequeño, un gestor de contraseñas autoalojado con una API, como un servidor Vaultwarden, cubre la misma función con una escala menor.

Una credencial queda fuera de todo esto. La clave SSH que usa el equipo de control para acceder a los servidores no es un problema de vault, porque Ansible la necesita antes de poder ejecutar cualquier play. Gestione esta clave con un agente y una passphrase, siguiendo conceptos básicos como los fundamentos de la gestión de claves SSH.

FAQ

¿Debo cifrar todo el archivo vars o sólo la cadena secreta?

Cifre todo el archivo cuando sólo contenga secretos, porque un solo comando los rota todos y el diseño se mantiene sencillo. Use ansible-vault encrypt_string cuando los secretos estén junto a variables normales, porque así sólo cambia el valor cifrado en la diferencia y el revisor puede ver qué variable se modificó. La contrapartida es la rotación. ansible-vault rekey cubre los archivos completos y deja intactos los bloques !vault insertados, por lo que debe regenerarlos manualmente con la nueva contraseña.

¿Dónde debo almacenar el archivo de contraseña de Ansible Vault?

Fuera del repositorio, con el modo 0600, en una ruta como ~/.ansible/vault-prod.txt. Indique esta ruta con --vault-password-file, establezca vault_password_file en [defaults] dentro de ansible.cfg o establezca ANSIBLE_VAULT_PASSWORD_FILE en el entorno. En CI, haga que el trabajo escriba la contraseña de su propio almacén de credenciales en un archivo temporal, exporte la variable y elimine el archivo cuando termine el trabajo. Si el archivo es ejecutable, Ansible lo ejecuta y lee la contraseña de la salida estándar, lo que permite obtenerla de un llavero de claves en lugar de almacenarla en disco.

¿Cómo uso contraseñas de Vault diferentes para staging y producción?

Asigne una etiqueta a cada contraseña con --vault-id staging@/path/to/file y --vault-id prod@/path/to/file, y cifre los archivos de cada entorno con su propia etiqueta. Pase ambos identificadores en tiempo de ejecución o enumérelos en vault_identity_list bajo [defaults]. De forma predeterminada, Ansible prueba todos los secretos disponibles hasta que uno descifra el archivo. Establezca vault_id_match = True si quiere que pruebe sólo el secreto cuya etiqueta coincida con la cabecera del archivo. Si hay varios identificadores cargados, seleccione el que se usará para cifrar con --encrypt-vault-id.

¿Ansible Vault impide que una contraseña aparezca en la salida de la ejecución?

No. Vault protege el secreto en reposo dentro del repositorio, pero nada más. Cuando se ejecuta una tarea, el valor está en texto plano, y una ejecución detallada o una tarea fallida puede incluirlo en el registro. Añada no_log: true a todas las tareas que gestionen una credencial, establezca mode y owner restrictivos en cualquier archivo que genere mediante plantillas y evite pasar secretos como argumentos de comandos, porque son visibles en la lista de procesos del host de destino mientras se ejecuta el comando.