La gestion de secrets dans la CI/CD peut être compliquée : il est parfois difficile de trouver le bon compromis entre sécurité et facilité d’utilisation. Ansible Vault est un outil qui propose une mitigation de ce problème.

Étape 1 : Les bases

Je vais commencer avec un playbook simple, qui effectue une seule tâche : il affiche le mot de passe de notre service dans la console. Par souci de modularité (et plus tard de sécurité), le nom du service ainsi que le mot de passe sont stockés dans un fichier de variables, en dehors du playbook.

# playbook.yml
---
- hosts: localhost
  connection: local
  gather_facts: no

  tasks:
    - name: display password
      debug:
        msg: "The password for {{ service_name }} is {{ password }}!"

Les valeurs des variables service_name et password sont sauvegardées dans un dossier host_vars, suivant les bonnes pratiques Ansible :

# host_vars/localhost/vars.yml
---
service_name: "my_service"
password: "unencrypted_password"

Quand j’exécute ce playbook, j’obtiens bien le message attendu : les variables sont bien remplacées par leurs valeurs. J’ai une base, mais avoir le mot de passe en clair me chiffonne.

Étape 2 : Mon premier coffre-fort

Création du coffre

Les plus anglophones parmi vous auront déjà fait le lien : Ansible propose de répondre à ce besoin de sécurité avec sa feature Vault. Elle permet de créer des fichiers chiffrés qui contiendront les variables sensibles.

Je crée un fichier vault depuis la ligne de commande avec l’outil ansible-vault :

$ ansible-vault create host_vars/localhost/vault.yml
New Vault password: 
Confirm New Vault password:

Une fois que j’ai renseigné mon mot de passe (qui servira à déchiffrer le contenu du fichier), un éditeur s’ouvre. Je peux y mettre mon mot de passe comme je l’avais fait dans mon fichier vars.yml. Après avoir validé et quitté l’éditeur, je vois qu’un fichier vault.yml a été créé. Si je regarde son contenu, je m’aperçois qu’il est inintelligible (normal, il est chiffré) :

# host_vars/localhost/vault.yml
$ANSIBLE_VAULT;1.1;AES256
32373336613330333765613136646165633130373236613332393764343538336461323334346561
3062376133366337383439613834373765373439646663340a643331656637653865656331333239
66313536333965363133396464666438346163613538646533313130643438636334636133633039
3430386338383165610a333633643938393330663566666232666364386631613539636634663761
39386166396462613033623736613465313434633563393932353066633030653636

Bien sûr je peux utiliser la commande ansible-vault pour lire ou modifier mon fichier, il faut juste que je renseigne le mot de passe :

$ ansible-vault view host_vars/localhost/vault.yml 
Vault password: 

vault_password: secret_password
(END)

Utilisation du coffre chiffré dans mon playbook

Je modifie mon fichier de variables en suivant encore une fois les bonnes pratiques.

Ansible préconise de conserver les variables stockées dans le fichier vars.yml, et de les faire pointer vers les variables chiffrées stockées dans le Vault. C’est plus facile de retrouver la source des valeurs en cas de debug. Je conserve donc ma variable password, mais je vais stocker sa valeur dans mon coffre chiffré (mon fichier host_vars/localhost/vault.yml).

# host_vars/localhost/vars.yml
---
service_name: "my_service"
password: "{{ vault_password }}"

Quand j’essaye de lancer mon playbook sans rien changer de plus, c’est la déception : plus rien ne marche. La raison est simple, Ansible n’a pas accès au contenu du Vault parce que je ne lui ai pas fourni le mot de passe pour le déchiffrer.

$ ansible-playbook playbook.yml 
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] *********************************************************************************************************************************************************************************************
ERROR! Attempting to decrypt but no vault secrets found

La manière la plus simple de passer le mot de passe à Ansible est d’utiliser l’option –ask-vault-pass à l’exécution du playbook. Je renseigne le mot de passe, et Ansible affiche notre mot de passe secret, c’est qu’il a réussi à déchiffrer le contenu du Vault.

$ ansible-playbook playbook.yml --ask-vault-pass             
Vault password: 
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] *********************************************************************************************************************************************************************************************

TASK [display password] **************************************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "The password for my_service is secret_password!"
}

PLAY RECAP ***************************************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Dans un contexte de production on utilise vault pour cacher la valeur de nos variables sensibles : n’allez pas les afficher dans la sortie du playbook.

Récapitulons : j’ai créé un playbook qui manipule des données sensibles. Pour les protéger, je les ai stockées dans un fichier chiffré avec ansible-vault. J’ai pu rejouer mon playbook avec la clé pour déchiffrer le coffre.

Ce scénario fonctionne très bien dans un environnement personnel ou de développement, mais il demande une entrée utilisateur qui le rend inutilisable dans un pipeline entièrement automatisé. Heureusement Ansible fournit les outils pour ce cas d’utilisation.

Automatisation du déblocage

Pour automatiser le déverrouillage, Ansible propose de fournir le mot de passe non pas en ligne de commande, mais en exécutant un script. Ce script peut-être par exemple en python ou en bash, et peut aller chercher le mot de passe du Vault dans une base de données, le porte-clés du système, etc. La seule contrainte est qu’à l’exécution, le script doit écrire le mot de passe sur la sortie standard. Pour le reste libre à vous d’inventer les solutions les plus tarabiscotées.

Je crée donc un script pour écrire mon mot de passe :

#!/bin/bash
# vault_secret.sh
echo "my_password"

La documentation d’Ansible a des exemples plus complexes.

Je me sers cette fois de l’option –vault-password-file pour déverrouiller mon coffre.

$ ansible-playbook playbook.yml --vault-password-file vault_secret.sh 
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] *********************************************************************************************************************************************************************************************

TASK [display password] **************************************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "The password for my_service is secret_password!"
}

PLAY RECAP ***************************************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Cette fois pas besoin de renseigner le mot de passe manuellement, le fichier est déverrouillé à partir du fichier.

Prochaine étape : intégrer ça à ma CI.

Étape 3 : Petit vault deviendra grand

Pour cet article, je vais utiliser Gitlab CI. La première chose que je vais faire est de modifier mon script pour ne pas avoir le mot de passe en clair. J’affiche à la place la valeur d’une variable d’environnement.

#!/bin/bash
# vault_secret.sh
echo $VAULT_PASSWORD

Je renseigne mon mot de passe dans les variables Gitlab CI. Elles sont accessibles sous l’onglet Settings > CI / CD > Variables.

Je crée un pipeline simple, avec un unique job qui lance mon playbook. Je vais aussi afficher le contenu de la variable d’environnement VAULT_PASSWORD.

# gitlab-ci.yml
image: williamyeh/ansible:ubuntu18.04

stages:
  - deploy

vault:
  stage: deploy
  script:
    - echo $VAULT_PASSWORD
    - ansible-playbook playbook.yml --vault-password-file vault_secret.sh

À l’exécution de ce pipeline, j’ai la sortie suivante. Notez que la variable d’environnement est masquée, mais que notre playbook a accès au coffre déchiffré !

Executing "step_script" stage of the job script
$ echo $VAULT_PASSWORD
[MASKED]
$ ansible-playbook playbook.yml --vault-password-file vault_secret.sh
PLAY [localhost] ***************************************************************
TASK [display password] ********************************************************
ok: [localhost] => {
    "msg": "The password for my_service is secret_password!"
}
PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0   
Job succeeded

Étape 4 : Un coffre c’est bien, deux c’est mieux

Maintenant que j’ai automatisé le déverrouillage de mon coffre depuis mes pipelines, j’aimerais une chose : pouvoir gérer plusieurs environnements. Plusieurs environnements, donc plusieurs secrets, donc plusieurs coffres.

Création des coffres

Je vais commencer par créer des coffres avec des mots de passe différents, en précisant leur id avec –vault_id :

$ ansible-vault create --vault-id dev@prompt group_vars/dev/vault.yml
New vault password (dev): 
Confirm new vault password (dev): 
$ ansible-vault create --vault-id prod@prompt group_vars/prod/vault.yml
New vault password (prod): 
Confirm new vault password (prod): 

Une fois déchiffrés, ces fichiers contiennent les mots de passe de mes différents environnements :

# group_vars/dev/vault.yml
vault_password: my_dev_password
# group_vars/prod/vault.yml
vault_password: my_prod_password

Utilisation dans ma CI/CD

J’ajoute tout d’abord un inventaire pour répertorier mes serveurs :

# inventory.cfg
[dev]
dev-server

[prod]
prod-server

Je crée deux nouveaux pipelines, un par environnement. Pour choisir le coffre à déverrouiller en fonction de mon environnement, je vais utiliser les possibilités de filtrage de ansible-playbook. J’utilise aussi l’option –vault-id pour préciser le coffre à déverrouiller ainsi que le fichier à utiliser pour le déverrouillage :

# gitlab-ci.yml
image: williamyeh/ansible:ubuntu18.04

stages:
  - deploy_dev
  - deploy_prod

vault_dev:
  stage: deploy_dev
  script:
    - ansible-playbook playbook.yml --vault-id dev@vault_dev_secret.sh -i inventory.cfg -l dev

vault_prod:
  stage: deploy_prod
  script:
    - ansible-playbook playbook.yml --vault-id prod@vault_prod_secret.sh -i inventory.cfg -l prod

À l’exécution je peux vérifier que les jobs accèdent bien au coffre qui concerne leur environnement :

Executing "step_script" stage of the job script
00:02
$ ansible-playbook playbook.yml --vault-id dev@vault_dev_secret.sh -i inventory.cfg -l dev
PLAY [all] *********************************************************************
TASK [display password] ********************************************************
ok: [dev-server] => {
    "msg": "The password for my_dev_service is my_dev_password!"
}
PLAY RECAP *********************************************************************
dev-server                 : ok=1    changed=0    unreachable=0    failed=0   
Job succeeded
Executing "step_script" stage of the job script
00:02
$ ansible-playbook playbook.yml --vault-id prod@vault_prod_secret.sh -i inventory.cfg -l prod
PLAY [all] *********************************************************************
TASK [display password] ********************************************************
ok: [prod-server] => {
    "msg": "The password for my_prod_service is my_prod_password!"
}
PLAY RECAP *********************************************************************
prod-server                : ok=1    changed=0    unreachable=0    failed=0   
Job succeeded

Aller plus loin

On peut passer tous ses environnements en une seule fois à l’exécution de mon playbook :

$ ansible-playbook playbook.yml --vault-id dev@vault_dev_secret.sh --vault-id prod@vault_prod_secret.sh -i inventory.cfg

PLAY [all] ***************************************************************************************************************************************************************************************************

TASK [display password] **************************************************************************************************************************************************************************************
ok: [dev-server] => {
    "msg": "The password for my_dev_service is my_dev_password!"
}
ok: [prod-server] => {
    "msg": "The password for my_prod_service is my_prod_password!"
}

PLAY RECAP ***************************************************************************************************************************************************************************************************
dev-server                 : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
prod-server                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

On peut aussi ajouter l’option no_log dans les playbook pour éviter d’afficher nos secrets dans les logs. Après tout ce serait dommage d’avoir fait tous ces efforts pour afficher nos secrets en clair, n’est-ce pas ?