Blog

ansible-vault rekey

Todos conocemos (o deberíamos conocer) la recomendación de cambiar nuestras contraseñas cada cierto tiempo.

Sobre todo en entornos empresariales, esta recomendación se extiende no sólo al cambio de contraseña de usuario, sino también de claves RSA para accesos SSH, cifrados, etc.

En este caso, venimos a hablaros de una necesidad de cambio de claves distinta, pero igualmente importante para nosotros. 

En STR Sistemas, intentamos automatizar el 100% de las infraestructuras (terraform) y configuración de sistemas (ansible). En dichas automatizaciones, en ocasiones, es necesario incluir información sensible (contraseñas, certificados SSL, RSA's de usuarios, etc), la cual siempre ciframos para evitar tener en código/repositorio contraseñas en texto plano.

Dicho esto, en STR nos encontramos habitualmente con esta necesidad de rotar claves y una vez más, nos llevó a pensar en la automatización o en una solución programada. Una de estas situaciones habituales es la de cambiar la contraseña de ansible-vault utilizada para cifrar toda información sensible de una automatización.

Descifrar un contenido cifrado con una clave y cifrarlo con una nueva se denonima "rekey" y esa precisamente es nuestra necesidad. Necesitamos realizar un cambio de la contraseña de ansible-vault utilizada en las automatizaciones de varios clientes.

Como hemos mencionado, esta información sensible cifrada, estaba presente no sólo en ficheros completos (el hecho de cifrar un fichero completo en sí, como un certificado SSL), si no que también, teníamos ficheros de variables de grupo de host (group_vars y host_vars) en los que existían variables cifradas. ansibe-vault nos facilita la posibilidad de hacer un rekey (con el comando ansibe-vault rekey), no obstante, éste funciona únicamente para ficheros cifrados completos, no siendo válido para realizar un rekey de variables cifradas dentro de ficheros.

Sin embargo, en la mayoría de nuestros clientes, tenemos no sólo ficheros cifrados, sino decenas o cientos de variables encriptadas (cifradas previamente con el comando "ansible-vault encrypt_string" como ya os contamos).

Como era de esperar, no nos planteábamos realizar esta labor de una forma no automatizada/programada poniendo a un técnico días y días a descifrar manualmente cientos de variables, a volver a cifrar el comando "ansible-vault encrypt_string", a realizar el cambio en el fichero pertinente del repositorio, etc, etc.

No nos gusta reinventar la rueda, y en muchos casos, antes de empezar a desarrollar una solución propia, siempre surge el comentario ¿A alguien más le habrá pasado? o ¿Alguien más habrá necesitado hacer algo igual o similar no?

Nos pusimos a investigar, y estando manos a la obra, vimos varias soluciones que podrían ser válidas para nuestro requisito, todas ellas haciendo uso de las librerías de ansible para Python.

Aunque algunas de estas soluciones nos podrían haber servido, una de ellas nos pareció la más completa, solución con la que a partir de ahí trabajaríamos y realizaríamos cambios en código en caso de ser necesario para adaptarlo a nuestros requisitos, ahorrando un considerable tiempo de base en la programación de la herramienta completa.

El script utilizado, es el siguiente:

#!/usr/bin/env python3

# derived from https://stackoverflow.com/a/67161907/277767
# Changes to the StackOverflow version:
# * delete temporary files that contain vaults!
# * prompt for passwords instead of passing them as program argument
# * more precise vault replacement
# * a bit nicer error messages that points at the line where re-keying failed

from typing import Optional

import sys
import re
import os

from os.path import join as join_path
from tempfile import gettempdir
from ansible.parsing.vault import VaultEditor, VaultLib, VaultSecret
from ansible.constants import DEFAULT_VAULT_IDENTITY # type: ignore
from ansible.errors import AnsibleError
from getpass import getpass

VAULT_REGEX = re.compile(r'(?P<vault>^(?P<indent>\s*)\$ANSIBLE_VAULT\S*\n(?:\s*\w+\n)*)', re.MULTILINE)

temp_count = 0

class ReKeyError(Exception):
    __slots__ = 'lineno', 'cause'

    lineno: int
    cause: Optional[Exception]

    def __init__(self, lineno: int, cause: Optional[Exception]=None) -> None:
        super().__init__()
        self.lineno = lineno
        self.cause = cause

    def __str__(self) -> str:
        return f'at line {self.lineno}: {self.cause if self.cause is not None else "an error occured"}'

def rekey(content: str, old_secret: VaultSecret, new_secret: VaultSecret) -> str:
    global temp_count

    temp_name = join_path(gettempdir(), f'ansible-rekey-{os.getpid()}-{temp_count}.tmp')
    temp_count += 1

    prev_index = 0
    new_content: list[str] = []
    while True:
        match = VAULT_REGEX.search(content, prev_index)
        if match is None:
            new_content.append(content[prev_index:])
            break

        indentation = match.group('indent')
        old_vault = match.group('vault')

        index = match.start()
        if index > prev_index:
            new_content.append(content[prev_index:index])
        new_content.append(indentation)

        string_content = old_vault.replace(indentation, '')

        try:
            with open(temp_name, 'w') as fout:
                fout.write(string_content)

            editor = VaultEditor(VaultLib([ (DEFAULT_VAULT_IDENTITY, old_secret) ]))
            editor.rekey_file(temp_name, new_secret)

            with open(temp_name) as fin:
                lines = fin.readlines()
        except Exception as exc:
            lineno = content.count('\n', 0, index) + 1
            if isinstance(exc, AnsibleError):
                exc.message = exc.message.replace(temp_name, f'line {lineno}')
            raise ReKeyError(lineno, exc)
        finally:
            os.unlink(temp_name)

        new_content.append(indentation.join(lines))
        prev_index = match.end()

    return ''.join(new_content)

def rekey_files(old_password: str, new_password: str, files: list[str]) -> None:
    for file_name in files:
        with open(file_name) as f:
            content = f.read()

        try:
            new_content = rekey(content, VaultSecret(old_password.encode()), VaultSecret(new_password.encode()))
        except ReKeyError as exc:
            print(f'{file_name}:{exc.lineno}: {exc.cause if exc.cause is not None else "an error occured"}', file=sys.stderr)
        else:
            with open(file_name, 'w') as f:
                f.write(new_content)

            print('rekeyed', file_name)

def main() -> None:
    if len(sys.argv) < 2:
        print("Usage: rekey.py <file...>", file=sys.stderr)
        sys.exit(1)

    old_password = getpass('Vault password: ')
    new_password = getpass('New Vault password: ')
    new_password_confirmation = getpass('Confirm New Vault password: ')

    if new_password != new_password_confirmation:
        print('ERROR! Passwords do not match', file=sys.stderr)
        sys.exit(1)

    rekey_files(old_password, new_password, sys.argv[1:])

if __name__ == '__main__':
    main()

Código original de Mathias Panzenböck

Resumiendo todo lo visto, el script recibe "N" ficheros, los recorre todos buscando las expresiones regulares que machean con la expresión regular de las contraseñas de ansible-vault '(?P<vault>^(?P<indent>\s*)\$ANSIBLE_VAULT\S*\n(?:\s*\w+\n)*)'.

Por cada coincidencia con la expresión regular, el script copia la cadena de texto cifrada (ya sea un fichero completo o una variable de un fichero), la escribe en un fichero en /tmp/ y la descifra. Una vez desencriptada, vuelve a cifrar la variable o fichero con la nueva contraseña de ansible-vault y realiza un replace de la vieja cadena de texto cifrada por la nueva. Todo ello respetando el formato del fichero origen, lo cual es vital en los ficheros YAML.

De esta manera ahorramos mucho tiempo que podemos dedicar a cosas más divertidas como por ejemplo matar pods con Kubeinvaders.

 

Newsletter de STR Sistemas

Suscríbete a nuestra newsletter para recibir contenido interesante del mundo DevOps y artículos escritos por nuestros técnicos

¡Usamos cookies propias y de terceros para mejorar tu experiencia en esta web! Si sigues navegando, consientes y aceptas estas cookies en tu ordenador, móvil o tablet.

Más información sobre las cookies y cómo cambiar su configuración en tu navegador aquí.

x