
Ansible amplifie tout : les bonnes pratiques comme les mauvaises. Un playbook mal sécurisé peut exposer des secrets à toute votre équipe, propager une mauvaise configuration à 200 serveurs en 30 secondes, ou ouvrir une porte dérobée via une collection Galaxy compromise. Cette page consolide les 6 axes de sécurité à appliquer transversalement à tout projet Ansible, en complément des sections spécialisées (secrets-vault, auditer un rôle, collections requirements).
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Identifier les 6 axes de sécurité d'un projet Ansible et leurs anti-patterns courants.
- Choisir la bonne stratégie de gestion de secrets (Ansible Vault, HashiCorp Vault, Passbolt) selon le contexte.
- Auditer la supply chain Galaxy (collections, rôles tiers, signatures GPG).
- Appliquer le moindre privilège côté contrôle (compte, clés SSH) et côté managé (sudoers ciblés).
- Durcir les managed nodes via les modules Ansible (SELinux enforcing, sysctl, firewalld, OpenSCAP).
- Tracer et auditer l'exécution (logs, callback plugins, AAP/AWX RBAC).
Prérequis
Section intitulée « Prérequis »- Comprendre la structure d'un playbook et la précédence des variables.
- Avoir manipulé Ansible Vault au moins une fois (premier vault).
- Connaître les bases supply chain (signatures GPG, pinning sémantique).
Axe 1, Gestion des secrets
Section intitulée « Axe 1, Gestion des secrets »Tous les secrets (mots de passe, tokens API, clés privées TLS, certificats) doivent être chiffrés avec Ansible Vault avant d'entrer dans Git, et masqués au runtime avec no_log: true. Ces deux protections répondent à deux fuites différentes : le chiffrement couvre le secret au repos dans le dépôt Git, no_log le couvre en transit dans les sorties d'exécution. Le choix de l'outil de stockage, lui, dépend de la durée de vie du secret et du nombre de personnes qui doivent y accéder :
| Type de secret | Outil recommandé | Raison |
|---|---|---|
| Token API stable, mot de passe d'app | Ansible Vault encrypt_string | Versionné Git, inline, diffs lisibles |
| Secrets multi-environnements (dev/prod) | vault-id par environnement | Mots de passe distincts, rotation isolée |
| Credentials BDD dynamiques (TTL court) | HashiCorp Vault / OpenBao | Génération on-demand, lease automatique |
| Mots de passe partagés entre humains | Passbolt | UI navigateur, OpenPGP, audit par utilisateur |
| Secrets cloud (AWS, Azure, GCP) | IAM / Workload Identity | Pas de credential statique |
Les 5 anti-patterns à proscrire absolument
Section intitulée « Les 5 anti-patterns à proscrire absolument »Ces cinq pratiques reviennent dans presque tous les projets Ansible repris en cours de route. Elles ont un point commun : le secret finit lisible par quelqu'un qui n'aurait pas dû y accéder, sans que personne s'en rende compte. La colonne « Risque » indique où la fuite se produit concrètement, la colonne « Solution » la contre-mesure à appliquer.
| Anti-pattern | Risque | Solution |
|---|---|---|
Mot de passe en clair dans group_vars/all.yml | Exposition Git, logs, historique navigateur | Ansible Vault |
vault_password_file commité | Tout le monde peut déchiffrer | .gitignore + variable d'environnement |
| Secret affiché dans la sortie verbose | Visibilité dans CI/CD, AWX, ELK | no_log: true sur les tâches sensibles |
| Un seul mot de passe Vault pour dev + prod | Compromission totale en cas de fuite | Vault IDs par environnement |
Credentials dans --extra-vars CLI | Visible dans ps aux, history shell | Fichier chiffré ou variable d'env |
Utiliser no_log correctement
Section intitulée « Utiliser no_log correctement »no_log est un mot-clé de tâche qui se pose au même niveau que name:, pas dans les paramètres du module. Il vaut false par défaut : sans lui, un mot de passe passé en argument est réaffiché tel quel dans la sortie de la tâche.
- name: Créer un user avec mot de passe (à masquer) ansible.builtin.user: name: alice password: "{{ vault_alice_password_hash }}" no_log: true # OBLIGATOIRE : sinon le hash apparaît en clair dans les logs
- name: Appel API authentifié (à masquer) ansible.builtin.uri: url: "{{ api_url }}/v1/users" headers: Authorization: "Bearer {{ vault_api_token }}" method: POST body: "{{ payload }}" body_format: json no_log: true register: api_responseno_log: true masque toute la sortie de la tâche (args, stdout, stderr), y compris en -v ou en cas d'échec.
Intégration avec un secret manager externe
Section intitulée « Intégration avec un secret manager externe »Quand Ansible Vault ne suffit plus (équipe > 5, audit fin requis, secrets dynamiques), le secret n'est plus stocké dans le dépôt Git mais récupéré à l'exécution auprès d'un serveur externe. Le lookup ci-dessous s'authentifie en AppRole (un couple identifiant/secret propre à la machine, pas à un humain) et lit la clé password d'un secret KV version 2 :
- name: Récupérer un secret KV depuis HashiCorp Vault ansible.builtin.set_fact: db_password: "{{ lookup('community.hashi_vault.vault_kv2_get', 'kv/myapp/db', url='https://vault.example.com:8200', auth_method='approle', role_id=lookup('env', 'VAULT_ROLE_ID'), secret_id=lookup('env', 'VAULT_SECRET_ID')).data.password }}" no_log: trueDétails dans le guide intégration HashiCorp Vault.
Axe 2, Supply chain Galaxy
Section intitulée « Axe 2, Supply chain Galaxy »Les collections Galaxy ne sont pas exemptes des attaques supply chain (typosquatting, takeover de package, dépendances malicieuses). Une collection est du code exécuté avec les privilèges du playbook, souvent en root sur des dizaines de machines : elle mérite le même niveau de contrôle qu'une dépendance applicative. Trois mesures s'imposent : figer les versions, vérifier ce qui est réellement téléchargé, et maîtriser la source de téléchargement.
Pinning strict
Section intitulée « Pinning strict »Le fichier requirements.yml déclare les collections dont le projet dépend, avec leur version. C'est ce fichier que ansible-galaxy collection install -r requirements.yml lit, et c'est le seul endroit où le numéro de version est décidé : sans contrainte explicite, Galaxy installe la dernière publiée, qui change sans prévenir.
# requirements.yml, versionné dans Git---collections: - name: ansible.posix version: "2.0.0" # exact, pas >=2.0.0
- name: community.general version: ">=8.0.0,<10.0.0" # range majeur locké
- name: stephrobert.webapp source: https://github.com/stephrobert/ansible-collection-webapp.git type: git version: v1.2.3 # tag, JAMAIS "main" ou brancheJamais latest ni de branche Git, le drift est garanti et silencieux. Toujours un tag ou un SHA Git pour la reproductibilité.
Vérification d'intégrité
Section intitulée « Vérification d'intégrité »Figer une version ne garantit pas que le contenu téléchargé soit celui d'origine. La sous-commande verify relit les collections déjà installées et les confronte au serveur, sans rien réinstaller ; l'option -p désigne le répertoire où elles ont été déposées.
# Dans la CI, après install :ansible-galaxy collection verify -r requirements.yml -p collections/verify compare les checksums + signatures GPG du tarball installé avec ce que le mirror déclare. Un mirror compromis qui substitue un tarball malveillant est détecté.
Mirror privé en entreprise
Section intitulée « Mirror privé en entreprise »Pour les fleets sensibles, déployer un Galaxy NG ou Automation Hub privé qui mirror Galaxy public. Avantages : audit centralisé, scan Trivy automatique sur les nouveaux uploads, possibilité de bloquer une collection compromise avant qu'elle ne touche la prod.
Auditer un rôle avant install
Section intitulée « Auditer un rôle avant install »Cf. la checklist d'audit en 7 axes : auteur, activité, tests, README, code, permissions, dépendances. Un rôle malveillant qui tourne en root sur 50 serveurs = compromission totale. Le coût d'audit (10 min) est négligeable face au coût d'incident.
Axe 3, Moindre privilège
Section intitulée « Axe 3, Moindre privilège »Ansible se connecte en SSH depuis une machine, le control node, vers des machines cibles, les managed nodes. Ces deux extrémités se durcissent séparément : d'un côté l'identité qui se connecte, de l'autre ce que cette identité a le droit de faire une fois arrivée. Le principe est le même des deux côtés : un compte compromis ne doit ouvrir que le strict nécessaire, jamais un accès root généralisé sur la flotte.
Côté control node
Section intitulée « Côté control node »Le control node concentre les clés d'accès à toute l'infrastructure : c'est la machine la plus sensible du dispositif. Quatre réflexes limitent les dégâts si elle est compromise.
- Compte dédié Ansible (jamais root SSH direct sur les cibles).
- Clé SSH Ed25519 générée spécifiquement pour le contrôle, distincte de votre clé personnelle.
ssh-agent+ControlPersistpour éviter de retaper la passphrase à chaque tâche.- Pas de
--ask-pass, c'est un signal qu'une clé manque.
Côté managed node
Section intitulée « Côté managed node »Sur la machine cible, l'élévation de privilèges passe par become, qui appelle sudo par défaut. Plutôt que d'autoriser ALL, on déclare la liste exacte des commandes que le compte Ansible peut lancer en root.
-
Compte non-root avec
sudociblé viacommunity.general.sudoers:- name: Déclarer une règle sudocommunity.general.sudoers:name: ansible-deployuser: ansiblenopassword: truecommands:- /usr/bin/systemctl restart nginx- /usr/bin/systemctl reload nginx- /usr/local/bin/deploy-app.shstate: presentbecome: trueLa règle ci-dessus permet à l'utilisateur
ansiblede redémarrer nginx et lancer un script précis sans mot de passe, mais pas d'avoir un shell root complet. Bien plus restrictif quecommands: ALL. -
Pas de
become: trueau niveau du play par défaut, uniquement sur les tâches qui en ont besoin. Réduit la surface d'attaque en cas de bug.
Credentials par environnement
Section intitulée « Credentials par environnement »Le cloisonnement se joue dans l'arborescence de l'inventaire. Les secrets ne vivent pas dans les mêmes fichiers que les variables publiques : chaque groupe reçoit un vault.yml distinct, chiffré avec son propre mot de passe.
inventory/├── hosts.yml├── group_vars/│ ├── all.yml # variables publiques globales│ ├── all/vault.yml # secrets globaux (chiffrés Vault)│ ├── prod/main.yml # variables publiques prod│ ├── prod/vault.yml # secrets prod (vault-id "prod")│ ├── staging/main.yml│ └── staging/vault.yml # vault-id "staging" (mot de passe différent)Avec vault-id distincts pour staging et prod, un développeur qui a accès à staging ne peut pas déchiffrer les secrets prod. La rotation est aussi locale (changer le mot de passe prod sans toucher à staging).
Axe 4, Modules à risque (shell, command, raw)
Section intitulée « Axe 4, Modules à risque (shell, command, raw) »Ces 3 modules exécutent des commandes arbitraires sur la cible. shell passe la commande à un interpréteur (/bin/sh par défaut), command l'exécute directement sans interpréteur, raw l'envoie en SSH brut sans même passer par Python. Dès qu'une variable est interpolée dans la chaîne exécutée par shell ou raw, elle devient un vecteur d'injection de commande : ce que contient la variable est exécuté au même titre que ce que vous avez écrit.
# ❌ DANGEREUX : injection si user_input contient "; rm -rf /"- ansible.builtin.shell: "rm /tmp/{{ user_input }}"
# ✅ SÉCURISÉ : module dédié, validation côté Ansible- name: Supprimer le fichier ciblé ansible.builtin.file: path: "/tmp/{{ user_input | regex_replace('[^a-zA-Z0-9._-]', '') }}" state: absentRègle : avant d'écrire command: ou shell:, chercher le module dédié avec ansible-doc -l | grep <thème>. Si c'est vraiment nécessaire, échapper les variables avec | quote :
# Si shell est vraiment inévitable, ici parce qu'il y a un tube- name: Compter les occurrences dans le journal d'authentification ansible.builtin.shell: | set -o pipefail grep {{ pattern | quote }} /var/log/auth.log | wc -l args: executable: /bin/bash register: hits changed_when: false failed_when: hits.rc not in [0, 1]Notez que sans le tube, shell: ne serait pas nécessaire : un simple grep passe très bien par command:, qui n'ouvre aucun interpréteur et supprime donc le risque d'injection à la racine.
Quand command:/shell: sont légitimes
Section intitulée « Quand command:/shell: sont légitimes »Le recours à ces modules n'est pas une faute en soi, il devient un problème quand il remplace un module qui existe. Trois situations le justifient réellement.
- Aucun module dédié n'existe (cas rare en 2026, vérifier deux fois).
- Bootstrap d'un host sans Python :
rawest obligatoire. - Lecture-seule pour récupérer une valeur : avec
register:+changed_when: false.
Détails dans raw / command / shell.
Axe 5, Validation du code
Section intitulée « Axe 5, Validation du code »Un playbook non testé est une modification qui part en production sans relecture. La validation Ansible s'organise en couches : la syntaxe d'abord, les règles de qualité ensuite, le comportement réel en dernier. Chacune se branche à un moment différent du cycle, du pre-commit local jusqu'au contrôle de dérive planifié.
Stack 2026
Section intitulée « Stack 2026 »Le tableau se lit de haut en bas comme un ordre de passage : plus on descend, plus le contrôle est coûteux et tardif. La colonne « Fréquence » indique où brancher chaque outil pour qu'il échoue le plus tôt possible.
| Outil | Rôle | Fréquence |
|---|---|---|
yamllint | Syntaxe YAML stricte (interdire yes/no ambigus) | Pre-commit |
ansible-lint --profile production | 80+ règles : FQCN, idempotence, anti-patterns | Pre-commit + CI bloquante |
ansible-test sanity --docker | Validation collection (FQCN, types, doc) | CI sur push |
| Molecule + testinfra | Tests fonctionnels rôle multi-distros | CI sur PR |
ansible-playbook --check --diff | Détection de drift sur fleet | Cron quotidien |
Intégration CI/CD durcie
Section intitulée « Intégration CI/CD durcie »Le workflow qui lint vos playbooks manipule le code de toute votre infrastructure : il se durcit comme n'importe quel pipeline sensible. Les deux points critiques sont le jeton GITHUB_TOKEN, dont les droits doivent être remis à zéro puis rouverts job par job, et l'empreinte des actions utilisées, pinnées par SHA de commit pour qu'une republication de tag ne change pas le code exécuté.
# .github/workflows/ansible-ci.yml, extraitpermissions: {} # GLOBAL : aucune permission par défaut
jobs: lint: runs-on: ubuntu-24.04 permissions: contents: read steps: - uses: actions/checkout@<SHA40> with: persist-credentials: false - run: pipx install ansible-lint - run: ansible-lint --profile=production . # bloquant - run: ansible-galaxy collection verify -r requirements.ymlRègles non négociables 2026 :
permissions: {}global + permissions minimales par job.- Actions pinnées par SHA, jamais par tag.
persist-credentials: falsesuractions/checkout.- Lint bloquant, pas seulement informatif, sinon les warnings s'accumulent.
Axe 6, Durcissement OS et traçabilité
Section intitulée « Axe 6, Durcissement OS et traçabilité »Les cinq axes précédents protègent le playbook ; celui-ci concerne ce que le playbook applique sur les machines. Ansible est bien placé pour porter un référentiel de durcissement, parce qu'il le rend rejouable et vérifiable : le même code applique la configuration et, relancé en --check, détecte la dérive. La traçabilité complète le dispositif en gardant la trace de qui a exécuté quoi.
Modules de durcissement
Section intitulée « Modules de durcissement »Trois modules de la collection ansible.posix couvrent l'essentiel d'un durcissement Linux : selinux pour le contrôle d'accès obligatoire, sysctl pour les paramètres du noyau, firewalld pour le filtrage réseau. Ils sont idempotents : relancés sur une machine déjà conforme, ils ne changent rien et ne remontent aucun changed.
# SELinux enforcing (mandatory en prod RHEL, objectif RHCE)- name: Régler le mode SELinux ansible.posix.selinux: state: enforcing policy: targeted become: true
# Durcissement réseau (anti-spoofing, syncookies)- name: Régler un paramètre noyau ansible.posix.sysctl: name: "{{ item.name }}" value: "{{ item.value }}" sysctl_file: /etc/sysctl.d/99-hardening.conf reload: true loop: - { name: net.ipv4.tcp_syncookies, value: '1' } - { name: net.ipv4.conf.all.rp_filter, value: '1' } - { name: kernel.kptr_restrict, value: '2' } - { name: kernel.dmesg_restrict, value: '1' } become: true
# Firewalld avec règle d'or permanent + immediate- name: Ouvrir le service dans le pare-feu ansible.posix.firewalld: service: ssh permanent: true immediate: true state: enabled zone: public become: trueOpenSCAP pour la conformité CIS / ANSSI / STIG
Section intitulée « OpenSCAP pour la conformité CIS / ANSSI / STIG »OpenSCAP évalue une machine face à un référentiel de conformité publié (CIS, ANSSI, STIG) et produit un rapport règle par règle. Pour les environnements soumis à ces référentiels, le déclencher via Ansible permet d'auditer toute la flotte d'un seul run et de récupérer les rapports au même endroit :
- name: Audit CIS via OpenSCAP ansible.builtin.command: > oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_cis --results /var/log/oscap-cis-{{ inventory_hostname }}.xml /usr/share/xml/scap/ssg/content/ssg-rhel10-ds.xml register: scap_result changed_when: false failed_when: scap_result.rc not in [0, 2] # rc=2 = "des règles ont échoué", pas une erreur fatale become: trueLogs et audit
Section intitulée « Logs et audit »Par défaut, Ansible n'écrit aucun fichier de log : la sortie défile dans le terminal et disparaît avec lui. En cas d'incident, il ne reste plus rien pour établir quelle tâche a changé quoi et quand. Activer une journalisation persistante est le minimum sur un control node partagé.
ANSIBLE_LOG_PATH=/var/log/ansible.logdansansible.cfgpour persister chaque run.- Callback plugins (
profile_tasks,timer,json) pour des sorties structurées. - AAP / AWX en prod : audit trail centralisé, RBAC fin, secrets stockés isolés, intégration LDAP/SAML.
# ansible.cfg, config sécurité[defaults]log_path = /var/log/ansible.logno_log = False # explicite, contrôlé par tâchehost_key_checking = True # ne JAMAIS désactiver en proddisplay_skipped_hosts = Falsecallbacks_enabled = ansible.posix.profile_tasks, ansible.posix.timer
[privilege_escalation]become = Truebecome_method = sudobecome_ask_pass = False
[ssh_connection]ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=accept-newpipelining = Truehost_key_checking = True est non négociable en production, sans lui, vous acceptez aveuglément n'importe quelle clé hôte (MITM possible). Pour le bootstrap initial, préférer accept-new (accepte le premier hôte mais refuse un changement ultérieur) plutôt que False.
Sécurité non négociable, récapitulatif
Section intitulée « Sécurité non négociable, récapitulatif »Les six axes se recoupent, et il est facile d'en appliquer cinq en laissant le sixième ouvert. Cette liste condense les points sur lesquels aucun arbitrage n'est acceptable en production ; elle sert de contrôle avant de brancher un projet Ansible sur une flotte réelle.
- Tous les secrets dans Vault, jamais en clair.
no_log: truesur les tâches sensibles. - Pinning strict +
ansible-galaxy collection verifydans la CI. become: trueciblé,sudoersprécis (commandes, pasALL).- Modes quoté + restrictifs :
"0600"secrets,"0640"configs sensibles,"0644"public. ansible-lint --profile productionen CI bloquante.- SELinux enforcing, firewalld permanent + immediate,
host_key_checking = True. - Audit trail :
ANSIBLE_LOG_PATHou AAP/AWX en prod.