Test d’un role Ansible

Après avoir réorganisé l’ensemble de mes rôles Ansible, j’avais en tête de trouver un moyen de tester leur exécution dans un environnement vierge. L’idée n’est pas nouvelle. Le but : vérifier l’exécution du rôle plus facilement, c’est-à-dire, sans savoir à installer une nouvelle VM, ou à louer un serveur à cette occasion, même si cette dernière solution est, par expérience : pratique, assez rapide à mettre en œuvre et d’un faible coût en utilisant un serveur facturé à l’heure.

Ayant parcouru quelques dépôts de rôle Ansible présent sur Ansible Galaxy et ayant sauvegardé quelques articles évoquant le sujet, j’avais donc une idée des outils disponibles et des exemples fonctionnels. Il ne restait plus qu’à se retrousser les manches, et à tenter l’implémentation sur l’un de mes rôles les plus simples consistant à installer fail2ban.

Installation

Première étape avant configuration, l’installation des outils qui permettront de tester le rôle en exécutant une seule commande, à savoir : Molecule et virtualenv. Avec python3 installé, je commence donc par installer virtualenv via: pip install virtualenv. J’initialise ensuite un environnement virtuel python avec virtualenv dans le dossier contenant mon rôle ansible, soit les commandes :

$ pip3 install virtualenv
$ virtualenv -p python3 .venv
$ source .venv/bin/activate

Étape suivante, l’installation de molecule via pip.

$ pip3 install --upgrade setuptools
$ pip3 install 'molecule[ansible,docker,lint]'

Configuration basique

Maintenant que les outils sont installés, je génère la configuration de base avec la commande: molecule init scenario. Cette commande a pour effet de créer quatre fichiers.

  • INSTALL.rst : contient des instructions pour l’installation d’autres dépendances, ou des étapes de configuration à réaliser. Dans mon cas, installation de molecule[docker], que j’ai donc directement intégré dans la commande citée précédemment.
  • molecule.yml : fichier principal décrivant les outils utilisés pour le test.
  • converge.yml : le playbook ansible chargé de déployer le rôle testé.
  • verify.yml : un fichier ansible permettant de décrire les vérifications à effectuer après déploiement du rôle.

Comme j’utilise docker pour la gestion de l’instance de test, je suis les préconisations de la documentation pour vérifier que docker fonctionne correctement.

$ docker run hello-world

Une fois l’environnement prêt, je peux passer à l’exécution du test.

$ molecule test

En théorie, ce premier test devrait au minimum réussir jusqu’à l’étape de vérification, en fonction des modifications effectuées dans verify.yml. En cas d’erreur, je trouve très utile d’exécuter manuellement les différentes étapes de molecule test. A savoir:

  1. molecule create : Création de l’instance.
  2. molecule converge: Déploiement du rôle.
  3. molecule login : Connexion à l’instance pour aller explorer son état, très pratique en cas d’erreur.
  4. molecule verify : Exécuter les vérifications.
  5. molecule destroy : Nettoyage.

Il convient de noter que, si le rôle ansible utilise service pour vérifier l’état d’un service, la configuration par défaut présente dans le fichier converge.yml n’est pas suffisante. J’ai été bloqué un bon moment avant de trouver des précisions à ce sujet au détour d’une issue sur Github. C’est pourquoi je vais maintenant décrire la configuration que j’ai mise en place.

Cas concret

La configuration que je décrite à la suite permet donc de tester un rôle relativement simple, que j’utilise pour installer fail2ban à partir de l’un des paquets deb du projet. Paquet distribué dans la partie release du projet GitHub. Commençons par le fichier molecule.yml.

---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: geerlingguy/docker-debian10-ansible:latest
    command: ${MOLECULE_DOCKER_COMMAND:-""}
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    privileged: true
    pre_build_image: true
provisioner:
  name: ansible
  playbooks:
    prepare: prepare.yml
verifier:
  name: ansible

Cette configuration présente quelques adaptations par rapport à la configuration par défaut. En particulier, du côté de platforms, où les instructions command et volumes sont nécessaires pour que l’instruction service dans ansible soit fonctionnelle. Notons aussi l’ajout de prepare dans provisioner. Cet ajout a pour but d’effectuer quelques actions supplémentaires sur l’instance juste après son installation et avant de commencer le déploiement du rôle. Le contenu du fichier prepare.yml est le suivant :

---
- name: Prepare
  hosts: all
  become: true
  tasks:
    - name: Install ssh
      apt:
        name: [ 'ssh' ]
        state: present
        update_cache: yes
        cache_valid_time: 3600
    - name: restart ssh
      service:
        name: ssh
        state: restarted

Après quelques tentatives de tests et d’erreur au démarrage du service fail2ban, dernière étape de mon rôle, je constate que ssh ne semble pas installé par défaut dans l’image docker choisie. Problème, fail2ban sous debian active par défaut une jail pour ssh et se plaint donc de ne pas trouver les fichiers de logs pour ssh. Ces quelques lignes ont donc pour but d’installer ssh et de démarrer le service associé. On pourrait discuter du fait que ces vérifications devraient être portées par le rôle, mais cela ne me semble pas pertinent, étant donné que mon utilisation du rôle se fait toujours dans le cas d’une installation sur une machine à laquelle j’accède via ssh. Le service est donc toujours présent dans ma situation.

Vient ensuite le fichier converge.yml. Pas de modification de ce côté là.

---
- name: Converge
  hosts: all
  become: true
  tasks:
    - name: "Include role-install-fail2ban"
      include_role:
        name: "role-install-fail2ban"

Enfin verify.yml dernier fichier de configuration. Pour un premier test, j’ai fait au plus simple et je me contente de vérifier que fail2ban fait bien partie des paquets installés sur le système.

---
- name: Verify
hosts: all
tasks:
- name: Gather package facts
package_facts:
manager: auto

- name: Verify Packages
assert:
that: "'{{ item }}' in ansible_facts.packages"
with_items:
- fail2ban

Voilà pour la configuration du test de mon rôle d’installation de fail2ban.

Conclusion

Après une première soirée passée sur le sujet du test d’un rôle ansible, je suis plutôt satisfait du résultat. Bien que le rôle soit l’un de mes rôles les plus simples, cela ne devrait pas être trop compliqué de généraliser cette méthode à l’ensemble de mes rôles, mais cela nécessitera sans aucun doute de nombreuses heures de développement. Comme souvent, c’est un processus itératif qui sera effectué progressivement sur les prochains mois. La simplicité de réaliser un test sur un système vierge est vraiment appréciable et permettra de vérifier qu’un rôle modifié fonctionne toujours, et cela, sans avoir à passer par le déploiement d’un serveur vierge. Prochaines étapes : étudier l’intégration avec le système d’actions GitHub et tester le playbook permettant le déploiement automatique d’Unicoda.

Sources

Migration vers Ansible Vault

Je l’avais évoqué plusieurs fois dans quelques articles, j’utilise ansible pour gérer l’installation automatique des services que j’auto-héberge. J’étais jusqu’à présent resté dans une gestion simple des mots de passe liés aux services (comprendre « exposé en clair dans les fichiers »). Vraiment pas terrible du point de vue sécurité, mais permettant de me concentrer sur la partie (ré-)installation automatique, là où réside le vrai gain de temps. Disposant de quelques jours de congés, je me suis donc attelé à ce sujet longtemps repoussé qu’est la migration des secrets vers le vault ansible.

Un vault ansible, en quelques mots, est un fichier chiffré contenant des pairs clé/valeur permettant de référencer un secret dans un script ansible.

Configuration

Côté implémentation, j’ai choisi de stocker mon vault dans le dossier group_vars/all/ et j’ai commencé en créant deux fichiers all.yml et vault_all.yml. Dans vault_all.yml, je stocke l’ensemble de mes secrets associés à un identifiant, par exemple, le mot de passe secretpassword associé à la clé vault_test_user_password donnera le fichier suivant :

---
vault_test_user_password: secretpassword

Ensuite, dans all.yml, je référence la variable du vault et l’expose dans une nouvelle variable. Cette étape n’est pas forcément nécessaire, il est tout à fait possible d’utiliser directement l’identifiant présent dans le vault. L’intérêt de cette méthode quelque peu répétitive au moment de la mise en place, réside dans le fait que l’on dispose d’un inventaire des variables stockées dans notre fichier vault, sans avoir besoin de déchiffrer le fichier vault pour parcourir son contenu. Voici donc, en reprenant l’exemple ci-dessus, le contenu de notre fichier de référence :

---
test_user_password: "{{ vault_test_user_password }}"

Maintenant que je dispose de mon dictionnaire de secrets, je peux passer à l’opération de chiffrement de ce dernier. Par défaut, ansible demandera le mot de passe à utiliser à chaque opération de chiffrement/déchiffrement du fichier, ce qui se révèle vite rébarbatif. Heureusement, il est possible de créer un fichier .vault_pass pour y stocker le mot de passe du vault. Évidemment, on prendra soin de l’ajouter à notre .gitignore, afin de ne pas commit cette information dans notre dépôt git. Dernière étape, référencer l’emplacement du mot de passe dans la configuration ansible, soit dans ansible.cfg, par ajout de la ligne :

[defaults]
vault_password_file = .vault_pass

Maintenant que tout est prêt, il convient de chiffrer le fichier vault. Pour effectuer cette opération, on utilisera la commande :

ansible-vault encrypt group_vars/all/vault_all.yml

Le fichier vault étant désormais chiffré, l’édition s’effectuera désormais avec :

ansible-vault edit group_vars/all/vault_all.yml

Parmi les autres opérations de la commande ansible-vault, on notera en particulier decrypt, view et rekey.

Utilisation

Maintenant que tout est configuré, je peux donc remplacer les références directes au mot de passe présent dans mes playbooks, par les nouvelles variables créées. Toujours avec l’exemple précédent, on passera de l’extrait de playbook suivant :

password: secretpassword

au nouveau contenu « sécurisé »:

password: "{{ test_user_password }}"

Il ne reste plus qu’à déclencher l’exécution de l’un des playbooks afin de vérifier que tout a été configuré correctement. Je pourrais m’arrêter là; les secrets de mes playbooks sont maintenant chiffrés dans un fichier dédié, tout va pour le mieux, mais un dernier point de faiblesse subsiste, le mot de passe du vault stocké en clair dans notre fichier .vault_pass. Évidemment, si un attaquant devait mettre la main sur ce fichier, il y a fort à parier que ce serait le cadet de mes soucis, mais voyons tout de même si je peux améliorer les choses.

Sécurisation du mot de passe Vault

En parcourant la documentation ansible liée au concept de vault, j’ai découvert que le paramètre de configuration vault_password_file est susceptible de prendre un fichier de script exécutable en entrée. Seule condition, le script doit renvoyer le mot de passe à utiliser. Étant donné que j’utilise déjà password-store en ligne de commande pour la gestion de mes mots de passe, j’y ai vu une occasion parfaite d’augmenter encore la sécurité en stockant le mot de passe du vault dans une entrée de mon password-store. Quelques recherches plus tard, j’obtiens donc le script suivant; script chargé de retourner le contenu de l’entrée ansible-vault.

#!/bin/bash

###################################
## Get password from password store
# Inspired by https://github.com/paulRbr/ansible-makefile/blob/master/pass.sh
###################################

if (command -v pass >/dev/null 2>&1)
then
    existingVault=$(pass "ansible-vault" || true)

    if [ -n "${existingVault}" ]
    then
        >&2 echo "Using passphrase found at 'ansible-vault' in your password store."
        echo "${existingVault}"
    else
        >&2 echo "No passphrase found at 'ansible-vault' in your password store."
        exit 0
    fi
fi

Ma configuration ansible devient donc (Ne pas oublier de rendre le fichier de script exécutable):

[defaults]
vault_password_file = ./get-vault-pass.sh

Autres améliorations

Il est possible d’avoir plusieurs vaults dans un projet ansible. Dans l’exemple précédent, toutes les entrées du vault sont partagées avec l’ensemble des playbooks, du fait d’appartenir au groupe all. Pour simplifier l’organisation, j’ai dans mon cas déplacé tous les secrets liés à unicoda et à son playbook de déploiement dans le dossier group_vars/unicoda/ et son vault dédié. De la même façon, on peut alors envisager d’améliorer le script pour utiliser un mot de passe différent pour chaque vault, chacun des mots de passe étant alors stocké de façon chiffrée dans password-store.

Par ailleurs, j’ai découvert pendant la mise en place de vault, qu’il existe un plugin ansible maintenu par la communauté et qui permet d’aller chercher directement le contenu d’un mot de passe dans password-store. Je n’ai pas poussé plus avant l’expérimentation, mais l’utilisation de ce plugin pourrait être une alternative valable à l’utilisation de vault.

Conclusion

Grâce à cette dernière amélioration, mes secrets ansible sont désormais stockés en lieu sûr et ne sont plus accessible au premier venu. C’est un élément de sécurité que j’encourage à mettre en place immédiatement, dès que le premier secret apparaît dans le dépôt de code.

Sources

Installation de sshpass sous macOS

Toujours dans le cadre de l’exécution d’ansible sous macOS, j’ai été confronté à une difficulté supplémentaire: sshpass n’était pas installé. Impossible de se connecter au serveur pour commencer le déploiement. Pas de solution simple du côté de brew, le canal officiel ne l’incluant pas. Retour aux bases donc, avec compilation du programme depuis les sources puis installation.

sshpass est disponible sur sourceforge. Après téléchargement de l’archive de la version 1.06 du programme, il ne reste plus qu’à extraire le code source et procéder à l’installation. Soit les étapes :

tar xvzf sshpass-1.06.tar.gz
cd sshpass-1.06
./configure
make
sudo make install

Source: Ansible OS X Mavericks You Must Install the sshpass Program

Ansible sous macOS: initializeAfterForkError

En exécutant récemment un script ansible, depuis mon ordinateur de travail, un mac (pour disposer simplement d’une base Linux en entreprise), j’ai rencontré l’erreur suivante :

+[__NSPlaceholderDate initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.

Après quelques recherches, il suffit d’ajouter la variable d’environnement OBJC_DISABLE_INITIALIZE_FORK_SAFETY à la valeur YES et de relancer le script, pour que celui-ci se termine correctement.

export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES

Source : StackOverflow