Aller au contenu
Infrastructure as Code medium

Sécuriser ses playbooks Ansible : 6 axes critiques (secrets, supply chain, audit)

15 min de lecture

Logo Ansible

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).

  • 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).

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. Trois patterns selon le contexte :

Type de secretOutil recommandéRaison
Token API stable, mot de passe d'appAnsible Vault encrypt_stringVersionné Git, inline, diffs lisibles
Secrets multi-environnements (dev/prod)vault-id par environnementMots de passe distincts, rotation isolée
Credentials BDD dynamiques (TTL court)HashiCorp Vault / OpenBaoGénération on-demand, lease automatique
Mots de passe partagés entre humainsPassboltUI navigateur, OpenPGP, audit par utilisateur
Secrets cloud (AWS, Azure, GCP)IAM / Workload IdentityPas de credential statique
Anti-patternRisqueSolution
Mot de passe en clair dans group_vars/all.ymlExposition Git, logs, historique navigateurAnsible Vault
vault_password_file commitéTout le monde peut déchiffrer.gitignore + variable d'environnement
Secret affiché dans la sortie verboseVisibilité dans CI/CD, AWX, ELKno_log: true sur les tâches sensibles
Un seul mot de passe Vault pour dev + prodCompromission totale en cas de fuiteVault IDs par environnement
Credentials dans --extra-vars CLIVisible dans ps aux, history shellFichier chiffré ou variable d'env
- 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_response

no_log: true masque toute la sortie de la tâche (args, stdout, stderr) — y compris en -v ou en cas d'échec.

Quand Ansible Vault ne suffit plus (équipe > 5, audit fin requis, secrets dynamiques) :

- 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: true

Détails dans le guide intégration HashiCorp Vault.

Les collections Galaxy ne sont pas exemptes des attaques supply chain (typosquatting, takeover de package, dépendances malicieuses). Trois mesures s'imposent :

# 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 branche

Jamais latest ni de branche Git — le drift est garanti et silencieux. Toujours un tag ou un SHA Git pour la reproductibilité.

Fenêtre de terminal
# 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é.

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.

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.

  • 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 + ControlPersist pour éviter de retaper la passphrase à chaque tâche.
  • Pas de --ask-pass — c'est un signal qu'une clé manque.
  • Compte non-root avec sudo ciblé via community.general.sudoers :

    - community.general.sudoers:
    name: ansible-deploy
    user: ansible
    nopassword: true
    commands:
    - /usr/bin/systemctl restart nginx
    - /usr/bin/systemctl reload nginx
    - /usr/local/bin/deploy-app.sh
    state: present
    become: true

    La règle ci-dessus permet à l'utilisateur ansible de redémarrer nginx et lancer un script précis sans mot de passe — mais pas d'avoir un shell root complet. Bien plus restrictif que commands: ALL.

  • Pas de become: true au 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.

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).

Ces 3 modules exécutent des commandes arbitraires sur la cible. Sans précaution, ils sont une porte d'entrée pour l'injection :

# ❌ DANGEREUX — injection si user_input contient "; rm -rf /"
- ansible.builtin.shell: "rm /tmp/{{ user_input }}"
# ✅ SÉCURISÉ — module dédié, validation côté Ansible
- ansible.builtin.file:
path: "/tmp/{{ user_input | regex_replace('[^a-zA-Z0-9._-]', '') }}"
state: absent

Rè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 inévitable
- ansible.builtin.shell: "grep {{ pattern | quote }} /var/log/auth.log"
changed_when: false
  • Aucun module dédié n'existe (cas rare en 2026, vérifier deux fois).
  • Bootstrap d'un host sans Python : raw est obligatoire.
  • Lecture-seule pour récupérer une valeur : avec register: + changed_when: false.

Détails dans raw / command / shell.

OutilRôleFréquence
yamllintSyntaxe YAML stricte (interdire yes/no ambigus)Pre-commit
ansible-lint --profile production80+ règles : FQCN, idempotence, anti-patternsPre-commit + CI bloquante
ansible-test sanity --dockerValidation collection (FQCN, types, doc)CI sur push
Molecule + testinfraTests fonctionnels rôle multi-distrosCI sur PR
ansible-playbook --check --diffDétection de drift sur fleetCron quotidien
# .github/workflows/ansible-ci.yml — extrait
permissions: {} # 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.yml

Règles non négociables 2026 :

  • permissions: {} global + permissions minimales par job.
  • Actions pinnées par SHA, jamais par tag.
  • persist-credentials: false sur actions/checkout.
  • Lint bloquant, pas seulement informatif — sinon les warnings s'accumulent.
# SELinux enforcing (mandatory en prod RHEL — objectif RHCE)
- ansible.posix.selinux:
state: enforcing
policy: targeted
become: true
# Durcissement réseau (anti-spoofing, syncookies)
- 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
- ansible.posix.firewalld:
service: ssh
permanent: true
immediate: true
state: enabled
zone: public
become: true

Pour les environnements soumis à CIS / ANSSI / STIG, déclencher OpenSCAP via Ansible :

- 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: true
  • ANSIBLE_LOG_PATH=/var/log/ansible.log dans ansible.cfg pour 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.log
no_log = False # explicite, contrôlé par tâche
host_key_checking = True # ne JAMAIS désactiver en prod
display_skipped_hosts = False
callbacks_enabled = ansible.posix.profile_tasks, ansible.posix.timer
[privilege_escalation]
become = True
become_method = sudo
become_ask_pass = False
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=accept-new
pipelining = True

host_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.

  • Tous les secrets dans Vault, jamais en clair. no_log: true sur les tâches sensibles.
  • Pinning strict + ansible-galaxy collection verify dans la CI.
  • become: true ciblé, sudoers précis (commandes, pas ALL).
  • Modes quoté + restrictifs : "0600" secrets, "0640" configs sensibles, "0644" public.
  • ansible-lint --profile production en CI bloquante.
  • SELinux enforcing, firewalld permanent + immediate, host_key_checking = True.
  • Audit trail : ANSIBLE_LOG_PATH ou AAP/AWX en prod.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn