SOPS: asegurando credenciales en repositorios

La seguridad informática está generando una preocupación nunca vista. En pocos años, la complejidad de los ataques se ha disparado, pasando de chantajear compañías para recuperar ciertos datos a atacar las cadenas de suministro digital en una escala nunca antes vista.

Aunque a nivel personal es difícil ser objetivo de ataques tan complejos, siempre es recomendable seguir una serie de buenas prácticas para ser menos vulnerables o para no ser utilizados como un vector para atacar a terceros. Personalmente, recomiendo lo siguiente:

  • Asegurarnos que nuestras contraseñas tengan una complejidad y longitud adecuada.
  • Que sean únicas. Es una forma de evitar que muchos servicios sean expuestos de golpe (algo posible si un servicio que utilizamos ha sido expuesto).
  • Que dichas contraseñas sean rotadas cada X tiempo para evitar que pudiesen ser vulneradas a través de ataques de fuerza bruta.

Todas estas prácticas son muy útiles para usuarios finales de un servicio, pero también aplican de una forma o de otra a los desarrolladores: nuestra aplicación utiliza credenciales de alguna forma para conectarse a una base de datos o acceder a servicios de terceros, etc. Y ahí la cosa se complica puesto que gran parte de las intrusiones o fugas de datos ocurren debido a descuidos: credenciales o contraseñas almacenadas en sitios públicamente accesibles (como repositorios de código o buckets de objetos abiertos a todo el mundo en AWS o GCP), que agravan los problemas de seguridad.

En este post vamos a ver que medidas podemos tomar y que herramientas podemos usar para evitar que se produzcan este tipo de incidentes.

SOPS: Secret Operations

Como desarrolladores, existe un abanico enorme de servicios que nos permiten separar las credenciales de nuestro código y almacenarlas en lugar seguro. Los más conocidos son servicios gestionados por proveedores de nube pública (Google Secret Manager, Amazon Secret Manager o Azure Key Vault) o servicios instalables en nuestros propios servidores on-premise como Hashicorp Vault.

Todos estos servicios separan las credenciales del código, pero ¿Y si lo que queremos es almacenarlas junto a éste de forma segura?

Imaginemos que tenemos una aplicación desplegada on-premises y que necesita acceder a algunos servicios de AWS. La manera más sencilla de darle permisos sería a través del uso de una pareja de Access Keys y referenciarlas desde la aplicación. Pero si queremos que esas credenciales se almacenen en el código de forma segura, nuestra mejor baza es utilizar SOPS.

SOPS o Secrets OPerationS es una herramienta escrita en Go y desarrollada por Mozilla que nos permite cifrar las variables de ficheros estructurados (YAML, JSON, ENV, INI, etc) y binarios, permitiendo la gestión de secretos dentro de un repositorio de código. Soporta todos los sistemas de claves de los proveedores de nube (GCP KMS, AWS KMS o Azure Key Vault), junto a PGP y age por lo que puede ser utilizado casi en cualquier lugar.

Para comenzar a utilizar SOPS, tan sólo tenemos que descargarlo, meterlo en el PATH y crear un fichero en la raíz de la carpeta donde vayamos a trabajar llamado .sops.yaml:

## Download and installof Sops
export SOPS_VERSION="3.7.1"

wget "https://github.com/mozilla/sops/releases/download/v${SOPS_VERSION}/sops_${SOPS_VERSION}_amd64.deb"
sudo apt install "./sops_${SOPS_VERSION}_amd64.deb" -y
rm "sops_${SOPS_VERSION}_amd64.deb"

# Reloading files from bash
source ~/.bashrc
source ~/.profile

Previamente a configurar nada, debemos tener disponible algún sistema de cifrado compatible. Si por ejemplo, queremos utilizar GPG, necesitamos obtener el identificador de nuestra key con el siguiente comando y luego crear completar el fichero .sops.yaml con el siguiente contenido:

# Obtenemos el ID de nuestra clave GPG
gpg --list-keys

pub   rsa4096 2021-09-10 [SC]
      FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4
uid        [  absoluta ] Tangelov <correo@chungo.com>
sub   rsa4096 2021-09-10 [E]


# Contenido del fichero .sops.yaml
creation_rules:
  - pgp: 'FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4'

Si no sabemos ni cómo crear una llave GPG, podemos seguir esta guía de la Wiki de Ubuntu que a mi parecer es bastante completa (aunque yo prefiero cambiar el keysize a 4096).

El alcance y las funcionalidades de SOPS exceden con mucho lo que vamos a tratar en este post, pero me gustaría reseñar algunas de ellas:

  • Podemos usar múltiples sistemas de cifrado a la vez para acceder a los secretos usando cualquiera de dichas claves, o por el contrario agrupar diferentes claves y necesitar acceso a todas ellas para poder descifrar un secreto.
  • Puede funcionar como un sistema “cliente-servidor” o enviar la información de los secretos a otros procesos si lo necesitamos.
  • Permite generar informes de auditoría que podemos almacenar en ficheros o en una base de datos en PostgreSQL.

Recomiendo encarecidamente echarle un vistazo a su documentación, puesto que no voy a profundizar mucho más en SOPS en este post y creo que puede ser muy útil a cualquier desarrollador o sysadmin.

La difícil tarea de eliminar secretos de repositorios

Git es una herramienta fantástica para desarrollar software. Nos permite trabajar de forma descentralizada y volver a cualquier punto anterior del código gracias al seguimiento que hace de los cambios realizados.

Sin embargo, este seguimiento puede ser un gran inconveniente a la hora de tratar con secretos. Si por error un secreto fuese pusheado a un repositorio, va a ser muy difícil eliminarlo del histórico. Nuestra mejor opción es considerar dicho secreto como expuesto y reemplazarlo (sin volver a subirlo al repositorio :) ). Si en el punto anterior hemos visto una herramienta para gestionar secretos en repositorios, ahora vamos a ver métodos para evitar que un secreto quede almacenado de forma insegura por un error o despiste.

Lo primero que me gustaría comentar es que si trabajamos en un repositorio en local este tipo de errores es menos grave. El secreto solo está almacenado en nuestra copia local y podemos enmendar el error fácilmente:

  • Si acabamos de empezar, podemos borrar la rama local y recrearla ya libre de secretos.
  • Podemos dar marcha atrás en nuestra propia rama e irnos al commit previo.

Para dar marcha atrás en nuestra rama podemos utilizar los siguientes comandos:

# Utilizamos git log para ver el ID de nuestros commits
git log

# Buscamos cuando hemos cometido el error (es más fácil usando un IDE)
git show --pretty="" --name-only $ID_COMMIT

# Forzamos la vuelta hacia atrás con el comando git reset (manteniendo todos los cambios en disco)
git reset --soft $ID_COMMIT

# Si queremos eliminar los cambios también de disco podemos reemplazar --soft por --hard

En cualquier caso, lo ideal es que esta situación no se produzca y crear algún flujo de trabajo que compruebe si hay secretos en nuestro código ANTES de que acaben en el repositorio. La mejor forma de hacerlo es mediante el uso de los hooks de git.

Pre-commit, un framework para validarlos a todos

Los git hooks o ganchos permiten la ejecución de scripts antes o después de utilizar ciertos comandos de git de forma automática. Es algo tremendamente útil para realizar validaciones y rechazar los cambios realizados en el código si éste no sigue alguna guía de estilo o si no tiene la calidad o el formato deseado.

Estos scripts se encuentran almacenados en la carpeta .git/hooks de cualquier repositorio y el nombre de cada uno de ellos referencia al momento en el que se va a ejecutar:

ls .git/hooks 
applypatch-msg.sample      pre-applypatch.sample    prepare-commit-msg.sample  pre-receive.sample
commit-msg.sample          pre-commit               pre-push                   update.sample
fsmonitor-watchman.sample  pre-commit.sample        pre-push.sample
post-update.sample         pre-merge-commit.sample  pre-rebase.sample

Bajo mi punto de vista, los dos más interesantes son pre-commit y pre-push:

  • pre-commit se ejecuta antes de realizar un commit. Esto lo hace tremendamente útil para ejecutar un lint sobre nuestro código o usarlo para verificar si contiene algún secreto.
  • pre-push se ejecuta antes de realizar un push sobre un repositorio. En mi caso, lo utilizo para garantizar que las ramas creadas en el repositorio siguen una convención de nombres acordada por todo el equipo.

El uso de validaciones de este tipo es común entre los desarrolladores y dependiendo de nuestro propósito, la creación de un hook de git puede ser muy compleja. Por ello, la comunidad se apoya desde hace años en un formato común para su gestión: el framework de Pre-Commit.

Desarrollado en Python, Pre-Commit proporciona un sistema estandarizado y con esteroides para la gestión de hooks de git. Añade soporte a otros lenguajes de programación (Javascript, Go, Python, etc.) más allá de bash y mantiene una lista de hooks que cubren los casos de uso más habituales. Para instalarlo, tan sólo debemos ejecutar los siguientes comandos:

# Para instalar pre-commit a usando Pip
pip install pre-commit

Una vez instalado, debemos inicializarlo en algún repositorio. Para ello, nos situamos en la raíz de dicho repositorio y ejecutamos el comando pre-commit install . Esta acción modificará el fichero ubicado en .git/hooks/pre-commit para asegurar que antes de cada commit se ejecute cualquier validación que configuremos.

Tras inicializar el framework, ahora podemos ir a la lista de hooks soportados y seleccionar los que más nos interesen. Para nuestro caso de uso, tenemos múltiples opciones pero yo me voy a decantar por gitleaks.

Gitleaks es una pequeña aplicación desarrollada en Go que verifica que no hemos añadido ningún secreto a nuestro repositorio. A través de una serie de reglas, permite detectar contraseñas, API keys o tokens en nuestro código ayudando a evitar que alguno se filtre por un descuido. Como cualquier aplicación escrita en Go, tan sólo tenemos que descargarla y añadir su ubicación a nuestro path para comenzar a utilizarla:

# Descargando gitleaks de su página oficial de Github
export GITLEAKS_VERSION="7.6.1"
curl -L "https://github.com/zricethezav/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks-linux-amd64" -o gitleaks

# Le damos permisos de ejecución y lo movemos a cualquier carpeta que esté en el PATH (~/.local/bin es un ejemplo)
chmod +x gitleaks
mv gitleaks /home/${USERNAME}/.local/bin

Gitleaks posee algunas reglas por defecto y si ahora ejecutamos gitleaks dentro de algún repositorio, veremos algo parecido a esto:

gitleaks

INFO[0000] opening .                                    
INFO[0000] scan time: 20 milliseconds 331 microseconds  
INFO[0000] No leaks found

Si, por el contrario, tenemos algún tipo de secreto en el código, recibiremos un error donde se nos indica la línea donde se encuentra el error, en que commit se ha añadido y que regla está incumpliendo entre otras cosas:

gitleaks --path=. -v --unstaged
INFO[0000] opening .                                    
{
	"line": "AWSACCESSKEY",
	"lineNumber": 2,
	"offender": "AWSACCESSKEY",
	"offenderEntropy": -1,
	"commit": "0000000000000000000000000000000000000000",
	"repo": ".",
	"repoURL": "",
	"leakURL": "",
	"rule": "AWS Access Key",
	"commitMessage": "",
	"author": "",
	"email": "",
	"file": "example",
	"date": "1970-01-01T00:00:00Z",
	"tags": "key, AWS"
}
INFO[0000] scan time: 12 milliseconds 286 microseconds  
WARN[0000] leaks found: 1           

Gitleaks posee integración nativa con pre-commit así que empezar a usarlo es muy sencillo. Tan sólo tenemos que crear un fichero en la raíz de nuestro repositorio con el nombre de pre-commit-config.yaml con el siguiente contenido y ya estará listo:

repos:
-   repo: https://github.com/zricethezav/gitleaks
    rev: v7.6.1
    hooks:
    -   id: gitleaks

Si ahora añadiésemos por error un secreto, pre-commit impediría que este acabase en nuestro repositorio de git:

git commit -S -m "Test leaks"
[INFO] Stashing unstaged files to /home/testing/.cache/pre-commit/patch1635789429-156940.
Detect hardcoded secrets.................................................Failed
- hook id: gitleaks
- exit code: 1

Ansible y Terraform

Gracias a SOPS y al resto de herramientas de este post, ya podemos asegurar de una forma mínimamente fiable que nuestros repositorios no almacenan secretos o que al menos éstos se encuentran cifrados. Sin embargo, de nada sirve todo lo anterior si no tenemos una forma sencilla de acceder a ellos cuando es necesario. SOPS cuenta con una gran comunidad y ha sido extendida para poder integrarse con dos viejos conocidos de este blog: Ansible y Terraform.

Ansible tiene un tutorial muy completo donde nos enseñan, paso a paso, cómo integrar ambas herramientas. Decidí utilizar SOPS para gestionar y rotar los secretos que utilizo en mis servidores personales y estoy muy contento con el resultado. Aunque utilizaba Ansible Vault, los ficheros funcionaban como una especie de caja negra que necesitaba abrir para auditar y depurar errores. El sistema actual es más transparente en la gestión y proporciona un único punto de acceso como es una clave GPG. Si alguien está pensando en utilizar SOPS junto con Ansible, debe que seguir los siguientes pasos:

  • Primero descargamos SOPS y ciframos los ficheros de variables utilizados por Ansible tal y como hemos visto en la primera parte de este post.
  • Después necesitamos instalar el plugin de la comunidad que facilita la integración de SOPS en Ansible. Tan sólo debemos añadir su collection a nuestro fichero requirements.yml.
  • El último paso es indicarle a Ansible que debe descifrar las variables utilizando SOPS. Ansible soporta dos “modos” de funcionamiento: podemos descifrar los ficheros dentro de una tarea o hacerlo directamente a través de un plugin de inventario, facilitando todo el proceso. Este segundo método ha sido el elegido por mi.

Para dar un poco de contexto, así se quedaría el arbol de directorios de nuestro repositorio de Ansible. Los comentarios añadidos muestran que ficheros han sido modificados durante el proceso:

.
├── ansible.cfg # Fichero de configuración de Ansible
├── host_vars
│   ├── raspi.sops.yml # Fichero de variables para mi Raspberry Pi cifrado con SOPS
│   ├── localhost.sops.yml # Fichero de variables para mi PC local cifrado con SOPS
│   └── wordpress.sops.yml # Fichero de variables para mi instancia de Wordpress cifrado con SOPS
├── inventory
├── launcher.sh
├── main.yml
├── playbooks
│   ├── raspi.yml
│   ├── localhost.yml
│   ├── pre-configuration.yml
│   ├── tasks
│   ├── vaults
│   └── wordpress.yml
├── README.md
├── requirements.txt
├── requirements.yml # Añadimos la nueva collection para dar soporte a SOPS dentro de Ansible
└── vars.yml

Y por último así quedarían las modificaciones realizadas en ansible.cfg y requirements.yml:

# Primero instalamos la collection de SOPS que habilita la integración entre ambas herramientas
# requirements.yml
---
collections:
  - name: community.sops
    version: 1.1.0


# Después activamos el plugin en nuestro fichero de configuración de Ansible
# ansible.cfg
[defaults]
interpreter_python=/usr/bin/python3
vars_plugins_enabled = host_group_vars,community.sops.sops

Los ficheros creados dentro de _host_vars se crearían ejecutando el siguiente comando desde la carpeta raíz del proyecto:

sops host_vars/raspi.sops.yml
sops host_vars/localhost.sops.yml
sops host_vars/wordpress.sops.yml

Y ya podemos seguir utilizando Ansible como si nada estuviera cifrado, dejando que SOPS gestione las variables por nosotros: ansible-playbook -i inventory main.yml --ask-become-pass --extra-vars "my_ansible_host=raspi"

El caso de Terraform es un poco distinto. Al igual que Ansible, Terraform también ha recibido por parte de la comunidad soporte para acceder a secretos cifrados con SOPS. Sin embargo, existen algunas diferencias que debemos tener en cuenta: Terraform almacena los valores de las variables o secretos dentro de su estado en texto plano. Esto es así para poder compararlos y validarlos entre ejecuciones, pero puede comprometer su integridad si nuestro estado no está almacenado en un lugar seguro y cifrado. Hay un artículo muy bueno de Gruntwork que adjunto en la documentación sobre las diferentes opciones que tenemos y las ventajas e inconvenientes de cada una de ellas.

Con esto en mente, vamos a ver cómo cifrar un secreto con SOPS y utilizarlo en Terraform.

  • Primero inicializamos el proveedor de sops, debe tener acceso al sistema de descifrado
  • Después configuramos el fichero de variables cifrado
  • Por último referenciamos los datos usando un data source

Imaginemos que tenemos este secreto y queremos almacenarlo en el repositorio. Son un par de claves que serán utilizados por un servicio para conectarse a un servicio de AWS que tenemos en nuestro CPD:

# Generamos con SOPS un fichero con el siguiente contenido
sops my-creds.enc.yml

# Pegamos este contenido a modo de ejemplo
access_key: AKIAIOSFODNN7EXAMPLE
secret_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Recordad que esto es un ejemplo y siempre que sea posible no se deben utilizar Key pairs para acceder a servicios de AWS ;)

Una vez hemos creado el fichero cifrado, vamos a crear el código de terraform necesario para poder acceder a él:

# Definimos el proveedor
terraform {
  required_providers {
    sops = {
      source  = "carlpett/sops"
      version = "~> 0.6"
    }
  }
}

# Cargamos los datos en el data source de Terraform
data "sops_file" "aws_secrets" {
  source_file = "my-creds.enc.yml"
}

# Creamos un output desde el que podemos ver que el valor es el esperado
output "access_key" {
  # Access the access_key variable stored in SOPS
  value     = data.sops_file.aws_secrets.data.access_key
  sensitive = false
}

output "secret_key" {
  # Access the secret_key variable stored in SOPS
  value     = data.sops_file.aws_secrets.data.secret_key
  sensitive = true
}

En las últimas versiones de Terraform, éste no nos permite mostrar por pantalla secretos, pero si que podremos ver el contenido en el tfstate y ver que todo ha funcionado correctamente:

{
  "version": 4,
  "terraform_version": "1.0.11",
  "serial": 1,
  "lineage": "894e88f5-2b46-7330-6818-813d003784af",
  "outputs": {
    "access_key": {
      "value": "AKIAIOSFODNN7EXAMPLE",
      "type": "string",
      "sensitive": true
    },
    "secret_key": {
      "value": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
      "type": "string",
      "sensitive": true
    }
  },
  "resources": [
    {
      "mode": "data",
      "type": "sops_file",
      "name": "aws_secrets",
      "provider": "provider[\"registry.terraform.io/carlpett/sops\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "data": {
              "access_key": "AKIAIOSFODNN7EXAMPLE",
              "secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
            },
            "id": "-",
            "input_type": null,
            "raw": "access_key: AKIAIOSFODNN7EXAMPLE\nsecret_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n",
            "source_file": "my-creds.enc.yml"
          },
          "sensitive_attributes": []
        }
      ]
    }
  ]
}

Y con esto ya podríamos guardar secretos en los repositorios de forma segura y utilizarlos desde Terraform o Ansible. Espero que sea útil y nos vemos en el siguiente post.

Documentación

Revisado a 01/05/2023