Salut à toi camarade, ça baboule du devops ?

Je viens partager ma frustration avec toi au sujet de mon grand pote Ansible vault !

Alors si tu débarques et que tu ne connais pas Ansible, c’est un outil super rigolo pour faire de l’installation et de la configuration de serveur.

Le bon pote Ansible sait aussi chiffrer et déchiffrer des fichiers, c’est très pratique pour protéger et versionner des secrets.

Par contre comme il transforme tout en bouillies, à chaque action de chiffrement, créant des révisions sur les fichiers de secrets même si le contenu du fichier n’a pas changé.

Moi ça m’énerve ! mais tu sais bien que je suis un rageux constructif, je vais te partager ici mes idées pour éviter de trop en chier :)

Tu as oublié de relancer ansible-vault encrypt Harry !

Harry Potter et Ginnie Weasley dans la chambre des secrets

Le fond du problème

Voici un fichier de secret Ansible “en clair”

1
2
3
4
5
6
---
env_mongodb_coucou: yo
env_mongodb_host: localhost
env_mongodb_user: "user"
env_mongo_password: "misspell variable"
env_mongodb_coucou: "hello"

voici le même fichier chiffré par Ansible vault

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ANSIBLE_VAULT;1.1;AES256
35386231306631393166313430353962633938643533346139333532383934666565326563363635
3539316465623738383265646465663665323630626565620a383663313134323239633365653865
63353932623662313031646236393334303635663262623237613761646635376631366334333537
3937373534326465390a363536663931666162363564313036656463366463333236303066316233
35303731326137386432653836343866663561316230366564393765333537663135646436386336
34306335316630663262363263386333386236343438616332386561653963383663326531383962
37336334343730613031623430373732303739333133313331656131653937376666353262376638
66646430363461346664333531343131613139356261373865393338646238336436663962613362
63643631356436393765336664376438633638323138326163386539353930636631326666663333
30643531303331633236353265623365333732316637383838376463323138376534373035363336
65623664623062306631633964646363316639653965343034613833366364626535343530613336
30393138653965353837

voici le même fichier chiffré, déchiffré, rechiffré par Ansible vault

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ANSIBLE_VAULT;1.1;AES256
35393062313662303863303132643339636236333532613233663532383864623935353135386337
3538303231613362663133303136393462306632653234380a633264653136386263306136323361
63636439346231353361323063386461306234333566623535393639383263613136663030326336
3132383936666638660a613065643332353331323362373866363435653063616166383165633235
37336337316535303530376236666465623838393463373334633539363230386336306338343461
65313561376435646339333763643538616264656566633133653739363362623733633066663930
34323062656264663532393730643632353665633461643931303566356162323630376661663665
32333461396163323266353966623133363964333231353062656239666662663134663932366534
39333439393039383134353064646163646334613663343031353266613563376462326662343832
39383230383335353339383830353236303334373938643336356139643761633938616137343832
63353063363435643362356564336361316636333139353461393338386137393530383538316264
66326561626661393861

Voici un diff Gitlab, à vous de me dire si le fichier chiffré en question a changé de contenu en clair ou pas :) Bonne chance avec ça !

capture diff gitlab

le Schtroumpf grognon

Le même contenu, si si, je te jure, au caractère près, aucun changement, mais un nouveau blob complètement différent du précédent. La seule façon de s’en assurer c’est de prendre les deux fichiers, de les déchiffrer et de faire un diff manuellement. Autant dire que dans git c’est très loin d’être pratique et que tu vas y passer un temps de dingues avec des opérations manuelles reloues et un risque non négligeable de te planter !

Ansible vault c’est bien, mais c’est une merde sans nom à debugger et à auditer.

Il te fait de gros fichiers bien opaques, et pour savoir si une variable est présente ou pas, il faut déchiffrer le fichier à chaque fois.

Ce que je vous propose c’est de rendre le système un peu moins sécurisé, mais un peu plus facile à auditer 😁

Après tout, sécurité contre praticité est un débat très ancien, mais qui continue de faire parler 🙈

Sortez-moi vos meilleurs arguments pour me dire si c’est une bonne idée ou si je devrai être purement et simplement interdit de clavier à vie !

grosse chaine en métal sur un clavier

Cette astuce incroyable de grand-mère qui énerve les experts en sécurité

Ce qu’on va mettre en place c’est un truc qui j’avais déjà un peu fait avec le cron d’audit des secrets Gitlab.

On va faire une lecture de nos fichiers de secrets et faire un dictionnaire de secrets hashé en sha256.

C’est important de bien hacher sinon la soupe n’est pas bonne !

soupe à l’oignon, Philippe Etchebest

Ce qui va nous permettre de lister l’ensemble des secrets et de savoir à chaque moment si un secret a changé ou pas, mais aussi d’avoir une idée des secrets qui sont identiques entre nos envs ou qui sont un peu pétés (genre le mot de passe est le nom de mon utilisateur)

Les conséquences de ça, c’est que si on s’empare du repo, on pourra pas forcément déchiffrer le contenu des secrets, mais on pourra deviner certains trucs si vous avez fait des mauvaises pratiques 😜

Mais je te connais, tu ne laisses pas les mots de passe par défaut et ta base de données en accès public sur internet hein 😉

Pour se décharger mentalement, on va faire le versionning du fichier de hash dans gitlab-ci. Tout le toutim sera autocommis avec le tag [SKIP CI], pour éviter d’avoir une shitloop de CI/CD. ( Une CI à déclenchement récursif sans condition d’arrêt)

graphe cycle de vie des déchets organique

Également, comme on sait qu’à chaque chiffrement et déchiffrement le fichier chiffré est changé, on veut plutôt isoler le déchiffrement, rechiffrement et éviter de commiter des fichiers rechiffrées qui n’ont pas changés pour éviter de polluer inutilement l’historique.

hash_secrets.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env pipenv-shebang

import os
import yaml
import hashlib


def read_yaml_files():
    yaml_secrets = {}
    for root, dirs, files in os.walk("env_vars"):
        for file in files:
            if file.endswith(".yml") and "hash" not in file:
                file_path = os.path.join(root, file)
                with open(file_path, "r") as f:
                    yaml_secrets[file_path] = yaml.safe_load(f)
    return yaml_secrets


def hash_secrets(yaml_secrets):
    for secret_file in yaml_secrets:
        for secret in yaml_secrets[secret_file]:
            yaml_secrets[secret_file][secret] = hashlib.sha256(
                str(yaml_secrets[secret_file][secret]).encode()
            ).hexdigest()
    return yaml_secrets


def write_hash_files(hash_secrets):
    for file in hash_secrets:
        dump_file = file.strip(".yml") + "_hash.yml"
        with open(dump_file, "w") as hash_file:
            print(f"dumping file {dump_file}")
            yaml.safe_dump(yaml_secrets[file], hash_file)


yaml_secrets = read_yaml_files()
hash_secrets = hash_secrets(yaml_secrets)
write_hash_files(hash_secrets)

Alors que fait le code ?

On boucle sur le dossier env_vars Pour chaque dossier on récupère les fichiers yaml qui n’ont pas “hash” dans leur nom (pour éviter une récursivité de création de fichiers ensuite) pour chaque clef de nos fichiers yaml on fait un hash sha256 de la valeur du secret. et on dump nos petits amis dans un fichier du même nom avec le suffixe hash.yml.

On met tout ça dans un gitlab-ci un peu crados qu’on vas expliquer ensuite :)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
image: python:3

hash-secrets:
  script:
    - echo $ANSIBLE_SECRET > .vault_pass.txt
    - pip3 install pipenv pipenv-shebang
    - pipenv install
    - git config --global user.name "Gitlab bot"
    - git config --global user.email "[email protected]"
    - | 
        set -x
        envs=$(ls env_vars )
        for folder in $envs;
        do
            echo $folder
            for file in $( ls env_vars/$folder)
            do
                pipenv run ansible-vault decrypt env_vars/$folder/$file --vault-password-file .vault_pass.txt || true
                echo "check file env_vars/${folder}/${file} syntax"
                pipenv run python3 -c 'import yaml, sys; print(yaml.safe_load(sys.stdin))' < env_vars/$folder/$file
            done
        done
    - ./hash_secrets.py
    - |
        envs=$(ls env_vars )
        for folder in $envs;
        do
            echo $folder
            for file in $( ls env_vars/$folder|grep hash)
            do
                git add env_vars/$folder/$file
            done
        done        
    - git commit -m "[SKIP CI] autocommit hashed file"||true
    - git clean -f 
    - export authenticated_url=$(echo $CI_REPOSITORY_URL|sed "s,https://.*@,https://${GITLAB_AUTH_STRING}@,g") 
    - git push "$authenticated_url" "HEAD:${CI_COMMIT_REF_NAME}" -o skip-ci||true
  • on écrit le fichier secret de vault (qu’on a bien rangé au chaud dans une variable gitlab)
  • on installe Ansible et dépendances
  • on set les variables d’identification de git pour que notre “robot” puisse commiter
  • on déchiffre chaque fichier secret et on fait un check de la syntaxe yaml (c’est ce que fait le one liner python)
  • on lance le script de hash
  • on commit tous les fichiers de hash
  • la variable GITLAB_AUTH_STRING contient une authentification Gitlab sous cette forme nom_du_token:token_gitlab

Et donc à partir de maintenant, à chaque réel changement sur les fichiers secrets, on va avoir un nouveau autocommit et un changement sur les fichiers de hash avec un indice sur ce qui a changé.

On voit d’un coup d’œil dans l’historique des commits à quel moment un changement dans les secrets a été fait. capture d’écran gitlab commit gitlab, les commits du robot mettent en avant les changements sur les secrets

et notre audit est facilité juste en regardant le diff

1
2
3
4
5
6
  env_vars/dev/mongodb_hash.yml 
  env_mongo_password: 1866239520cf6e707470fbb3520341e44cf912b51847c990dc4f8982c232526a
- env_mongodb_coucou: e9058ab198f6908f702111b0c0fb5b36f99d00554521886c40e2891b349dc7a1
+ env_mongodb_coucou: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
  env_mongodb_host: 49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763
  env_mongodb_user: 04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb

On voit d’un coup d’œil rapide que le secret env_mongodb_coucou a changé de valeur et avec un coup de grep on retrouve très facilement ou il est utilisé dans le code. Bonus on sait avec précision le nom des secrets vaultés, ça nous permet de contrôler le nom des secrets et éventuellement des erreurs.

Mets-moi ton plus beau crochet du droit

crochet du droit Mike Tyson

Dernier petite astuce pour avoir des casseroles qui brille et les yeux qui pétilles, on peut mettre en place un git Hook pour re chiffrer le vault avant le commit.

On s’enlève encore un peu de charge mentale, on évite de commiter les fichiers de secrets en clair.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.git/hooks/pre-commit

#!/bin/bash

function cdgit () {
  while ! [ -d .git ];
   do cd ..
  done
}


cdgit

set -x

envs=$(ls env_vars )
for folder in $envs;
do 
    echo $folder
    for file in $( ls env_vars/$folder)
    do
        ansible-vault encrypt env_vars/$folder/$file || true
    done
done

ce que fait le script:

  • peu importe ou je suis dans l’arborescence du repo, je cd jusqu’à la racine de repo.
  • boucles sur les dossiers dans env_vars
  • pour chacun des fichiers dans les dossiers env_vars, je lance ansible-vault encrypt et je veux un résultat d’exécution .
  • le script s’exécute avant le push et permet de faire un commit amend si on à oublié de rechiffrer les secrets.
  • ce qui nous donne une exécution comme celle-ci.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
++ ls env_vars
+ envs='dev
local
prod'
+ for folder in $envs
+ echo dev
dev
++ ls env_vars/dev
+ for file in $( ls env_vars/$folder)
+ ansible-vault encrypt env_vars/dev/mongodb.yml
ERROR! input is already encrypted
+ true
+ for folder in $envs
+ echo local
local
++ ls env_vars/local
+ for file in $( ls env_vars/$folder)
+ ansible-vault encrypt env_vars/local/mongodb.yml
ERROR! input is already encrypted
+ true
+ for folder in $envs
+ echo prod
prod
++ ls env_vars/prod
+ for file in $( ls env_vars/$folder)
+ ansible-vault encrypt env_vars/prod/memcached.yml
ERROR! input is already encrypted
+ true
Énumération des objets: 41, fait.
Décompte des objets: 100% (41/41), fait.
Compression par delta en utilisant jusqu'à 8 fils d'exécution
Compression des objets: 100% (25/25), fait.
Écriture des objets: 100% (40/40), 12.00 Kio | 4.00 Mio/s, fait.
Total 40 (delta 0), réutilisés 0 (delta 0), réutilisés du pack 0
To gitlab.com:cyberpunkachien/ansible-vault-hash.git
   30ec6b4..a1565bb  main -> main

mon arborescence de fichier ressemble à ça

1
2
3
4
5
6
7
├── env_vars
│   ├── dev
│   │   └── mongodb.yml
│   ├── local
│   │   └── mongodb.yml
│   └── prod
│       └── memcached.yml

Conspiration

Cette astuce est probablement à éviter dans un contexte de repo sensible, la dernière chose qu’on veut, c’est de fragiliser nos secrets face à une attaque extérieure.

femmes complotiste, chapeau en aluminium pour manifester contre les ondes

Mais quand on jardine du Ansible, et qu’on essaye de bosser un peu proprement et de manière sécurisée, ansible vault est un incontournable, C’est dommage de s’en passer, parce qu’il est plutôt simple à mettre en place. Le fait d’exposer un peu plus le contenu de notre vault réduit la sécurité, mais permet aussi d’enforcer des bonnes pratiques.

  • on veut éviter d’avoir les mêmes mots de passe entre nos environnements
  • on veut éviter d’oublier des variables et d’être obligé d’exposer des tâches sensibles pour débugguer
  • on veut éviter de perdre du temps et mettre de la confusion en chiffrant déchiffrant les fichiers vaultés sans changement.
  • on veut savoir quand un secret a été changé sans en exposer le contenu et savoir quel commit a provoqué le changement.

Solution imparfaite, parce que on peut encore committer des fichiers dévaulté en clair, si ont fait pas gaffe, et ça ne fonctionne que dans le contexte d'Ansible.

Si vous avez d’autres trucs à chiffrer en dehors d’Ansible, pensez à git-crypt :)

Bisous

Anna Kendrick, bisous sarcastique