Protegiendo Secretos con Mozilla Sops y Age

Advertencia
Este artículo se actualizó por última vez el 2022-11-01, el contenido puede estar desactualizado.

Antes de comenzar, quería contarles que hay una promoción en DigitalOcean donde te dan un crédito de USD 100.00 durante 60 días para que puedas probar los servicios que este Proveedor Cloud ofrece. Lo único que tienes que hacer es suscribirte a DigitalOcean con el siguiente botón:

DigitalOcean Referral Badge

O a través del siguiente enlace: https://bit.ly/digitalocean-itsm


Si bien muchos piensan que es una buena idea de guardar información como usuarios de base de datos, contraseñas, claves API u cualquier otro tipo de información sensible en texto plano y enviarlos a repositorios Git públicos como GitHub, GitLab o BitBucket o inclusive privados, te tengo una mala noticia: NO, lo es. Si aun asi quieres hacerlo, debes encriptarlo primero usando Mozilla SOPS (Secret Operations) y AGE.

SOPS es un editor de secretos que soporta formatos de archivo como: YAML, JSON, ENV, INI y BINARY y podemos encriptarlos con AWS KMS, GCP KMS, Azure Key Vault, age, y PGP (este ultimo descontinuado).

SOPS tiene varias opciones para instalarse, con paquetes construidos para distribuciones basadas en Debian/RedHat.

Desde el repositorio oficial de GitHub de SOPS, vamos a la parte de Releases

Si lo queremos instalar en Debian:

1
2
wget https://github.com/mozilla/sops/releases/download/v3.7.3/sops_3.7.3_amd64.deb
sudo dpkg -i ./sops_3.7.3_amd64.deb

o Redhat/AlmaLinux/Rocky/Fedora:

1
2
wget https://github.com/mozilla/sops/releases/download/v3.7.3/sops-3.7.3-1.x86_64.rpm
sudo dnf localinstall sops-3.7.3-1.x86_64.rpm

Otra opción podría ser descargarnos el binario de SOPS, instalarlo en /usr/local/bin y darle permisos de ejecución:

1
2
3
wget https://github.com/mozilla/sops/releases/download/v3.7.3/sops-v3.7.3.linux.amd64
sudo mv sops-v3.7.3.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops

Comprobemos la version instalada: queremos

1
sops -v

Y deberíamos ver la siguiente salida:

1
sops 3.7.3 (latest)

age es una herramienta escrita en Go, moderna, simple y segura de encriptación. Podemos encriptar y desencriptar nuestros archivos haciendo posible su almacenamiento en repositorios Git.

También cuenta con multiples opciones para instalarlo:

Debian:

1
sudo apt-get install age

RedHat:

1
sudo dnf install age

O manual desde la sección Releases del proyecto age:

1
wget -O age.tar.gz https://github.com/FiloSottile/age/releases/download/v1.0.0/age-v1.0.0-linux-amd64.tar.gz

Luego descomprimimos y movemos los ejecutables a /usr/local/bin:

1
2
3
tar xf age.tar.gz
sudo mv age/age /usr/local/bin
sudo mv age/age-keygen /usr/local/bin

Y les damos permisos de ejecución:

1
sudo chmod +x /usr/local/bin/age /usr/local/bin/age-keygen

Comprobamos las versiones:

De age:

1
age -v

Y de age-keygen:

1
age-keygen -version

En ambos caso deberíamos ver la versión:

1
v1.0.0

Ya que tenemos age instalado en nuestro equipo, vamos a generar nuestras llaves publica y privada:

1
age-keygen -o key.txt

Y nos mostraria la siguiente salida:

1
2
age-keygen: warning: writing secret key to a world-readable file
Public key: age16emn5m6lkccns748kz0cyms2jwdykhz4e4pquu50eqt9qlnz8dvquwqcs5

Y si vemos el contenido del archivo key.txt:

1
2
3
# created: 2022-10-03T13:37:37-03:00
# public key: age16emn5m6lkccns748kz0cyms2jwdykhz4e4pquu50eqt9qlnz8dvquwqcs5
AGE-SECRET-KEY-1MZMVXNDA0V4JMYPDAD9V49VQA0KH456AWKSK6S5D22SR0FGYYVGS0E860T
Advertencia
Recuerda que la llave privada no debemos guardarla en ningún repositorio Git.

Para facilitar un poco las cosas, vamos a crear una carpeta en nuestro directorio HOME donde guardaremos nuestra llave privada:

1
2
mkdir ~/.sops
mv ./key.txt ~/.sops

Y exportaremos una variable de entorno hacia nuestro .zshrc o .bashrc

Zsh:

1
echo "export SOPS_AGE_KEY_FILE=$HOME/.sops/key.txt" >> ~/.zshrc

Bash:

1
echo "export SOPS_AGE_KEY_FILE=$HOME/.sops/key.txt" >> ~/.bashrc

Una vez configurado nuestro sistema, procederemos a encriptar nuestros secretos. Hay muchas formas de abordar esto dependiendo del tipo de archivo.

Pueden ser secretos de Kubernetes, valores de Helm o solo yaml plano.

Creamos un archivo con el siguiente contenido:

secretos.yml

1
2
3
4
5
6
7
8
9
---
apiVersion: v1
kind: Secret
metadata:
    name: mysql-secreto
    namespace: default
stringData:
    MYSQL_USER: root
    MYSQL_PASSWORD: Password_superSecret@

Para encriptar el archivo:

1
sops --encrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") --encrypted-regex '^(data|stringData)$' --in-place ./secretos.yaml

Si prestamos atención a la parte del comando que dice --encrypted-regex '^(data|stringData)$' le estamos indicando a sops que busque dentro de ese archivo la clave stringData y que encripte su contenido.

Ahora, para desencriptarlo:

1
sops --decrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") --encrypted-regex '^(data|stringData)$' --in-place ./secretos.yaml

Es bastante util que apliquemos los cambios a Kubernetes sin tener que desencriptar el archivo sino que lo hacemos al vuelo:

Primero lo encriptamos:

1
sops --encrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") --encrypted-regex '^(data|stringData)$' --in-place ./secretos.yaml

Y luego lo desencriptamos y aplicamos los cambios al cluster al vuelo, haciéndole pipe a kubectl:

1
sops --decrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") --encrypted-regex '^(data|stringData)$' ./secretos.yaml | kubectl apply -f -

Comprobemos los cambios:

1
kubectl describe secrets mysql-secret
1
kubectl get secret mysql-secret -o jsonpath='{.data}'
1
kubectl get secret mysql-secret -o jsonpath='{.data.MYSQL_PASSWORD}'  | base64 --decode

Creamos un archivo con el siguiente contenido:

secretos.env

1
2
MYSQL_USER=superroot
MYSQL_PASSWORD="Password_superSecret@!!!!############"

Encriptamos:

1
sops --encrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") -i .env

Desencriptamos:

1
sops --decrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") -i .env

No olvidemos agregar el archivo .decrypted~secretos.env al .gitignore para que no lo suba a Git.

Creamos un archivo con el siguiente contenido:

secretos.json

1
2
3
4
{
    "mySqlUser": "superroot",
    "password": "Password_superSecret@!!!!#######"
}

Encriptamos:

1
sops --encrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") -i secretos.json

Desencriptamos:

1
sops --decrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") -i secret.json

No olvidemos agregar el archivo .decrypted~secretos.json al .gitignore para que no lo suba a Git.

secretos.ini

1
2
3
[database]
user     = superroot
password = Password_superSecret@!!!!1223

Encriptamos:

1
sops --encrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") -i secretos.json

Desencriptamos:

1
sops --decrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") -i secret.json

No olvidemos agregar el archivo .decrypted~secretos.ini al .gitignore para que no lo suba a Git.

secretos.sql

1
2
3
--- https://xkcd.com/327/
--- DO NOT USE
INSERT INTO Students VALUES ( 'Robert' );  DROP TABLE STUDENTS; --' )

Encriptamos:

1
sops --encrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") --in-place ./secretos.sql

Desencriptamos:

1
sops --decrypt --age $(cat $SOPS_AGE_KEY_FILE |grep -oP "public key: \K(.*)") --in-place ./secret.sql

Si tienes un flujo GitOps con Flux, para desencriptar secretos en el cluster te recomiendo leer la documentación de Flux: https://fluxcd.io/flux/guides/mozilla-sops/#configure-in-cluster-secrets-decryption

Encriptar y desencriptar secretos nunca había sido tal fácil. Con SOPS y age puedes realizar tal tarea sin necesidad de complicarnos mucho y ahorrarnos grandes dolores de cabeza con malas practicas. El siguiente objetivo seria el de juntar todas las piezas y automatizar el flujo de CI/CD para que reconozcan esos valores, cosa que muy seguramente sera en un proximo articulo.

Espero les haya gustado y nos vemos en la próxima!