ITSkillsCenter
Blog

Durcir un serveur Linux avec Ansible pas à pas

13 min de lecture

📍 Article principal : Ansible 2026 : la stack pratique pour automatiser Linux et Windows

Vous avez une pile applicative qui tourne. La même journée, vous voulez la durcir — sshd, fail2ban, ufw, paramètres kernel, alignement CIS Benchmark — sans casser ce qui marche.

Ce que vous aurez à la fin

Un rôle hardening qu’on applique sur n’importe quel serveur Debian ou Ubuntu pour atteindre un niveau de durcissement aligné sur les contrôles essentiels du CIS Benchmark, avec une configuration SSH durcie, un pare-feu UFW restrictif, fail2ban actif sur SSH et HTTP, des paramètres sysctl sécurisés et une session d’audit qui vous dit ce qui a changé. Le tout idempotent et réversible.

Prérequis

  • ansible-core 2.20 et un projet structuré en rôles.
  • Un serveur Debian 12 ou Ubuntu 24.04 LTS, fraîchement provisionné ou avec une LEMP déjà installée.
  • Accès SSH par clé publique fonctionnel — c’est le pré-requis non négociable, sans quoi la première bascule SSH va vous verrouiller dehors.
  • Niveau attendu : connaître les bases de SSH, sudo, netfilter.
  • Temps : 2 heures en lecture active, plus une session de tests.

Étape 1 — Garde-fou SSH avant tout durcissement

La règle d’or qui sauve des heures de remédiation : avant la première ligne de configuration SSH, ouvrez une seconde session SSH sur le serveur, séparée, en parallèle de celle où Ansible va tourner. Si le playbook coupe l’accès par erreur, la seconde session reste ouverte et vous laisse corriger sans avoir à passer par la console cloud.

Vérifiez aussi qu’un backup access existe : panel cloud avec console série, ou clé SSH d’urgence stockée hors-ligne. Le durcissement SSH est l’opération la plus risquée d’un projet Ansible — c’est là que les playbooks pourrissent en prod sans qu’on ait les moyens de réparer rapidement.

Étape 2 — Squelette du rôle hardening

On crée un rôle local plutôt que d’utiliser un rôle communautaire — le durcissement est l’un des rares domaines où la spécificité organisationnelle prime sur la généricité. On commence avec ansible-galaxy init roles/hardening, puis on remplit defaults/main.yml avec l’API publique du rôle :

---
# SSH
hardening_ssh_port: 22
hardening_ssh_permit_root_login: 'no'
hardening_ssh_password_authentication: 'no'
hardening_ssh_pubkey_authentication: 'yes'
hardening_ssh_max_auth_tries: 3
hardening_ssh_client_alive_interval: 300
hardening_ssh_client_alive_count_max: 2
hardening_ssh_allow_users: []   # vide = autorise tous les comptes valides

# Pare-feu UFW
hardening_ufw_default_incoming: deny
hardening_ufw_default_outgoing: allow
hardening_ufw_allow_rules:
  - { port: 22, proto: tcp, comment: 'SSH' }
  - { port: 80, proto: tcp, comment: 'HTTP' }
  - { port: 443, proto: tcp, comment: 'HTTPS' }

# Fail2ban
hardening_fail2ban_enabled: true
hardening_fail2ban_ssh_maxretry: 5
hardening_fail2ban_ssh_bantime: 3600

# Sysctl — paramètres réseau et kernel
hardening_sysctl_settings:
  net.ipv4.conf.all.rp_filter: 1
  net.ipv4.conf.default.rp_filter: 1
  net.ipv4.conf.all.accept_source_route: 0
  net.ipv4.conf.all.accept_redirects: 0
  net.ipv4.icmp_echo_ignore_broadcasts: 1
  net.ipv4.tcp_syncookies: 1
  kernel.randomize_va_space: 2
  fs.protected_hardlinks: 1
  fs.protected_symlinks: 1

Chaque variable est conservatrice par défaut : SSH refuse root, refuse les mots de passe, n’autorise que les clés publiques. Le pare-feu refuse tout en entrée sauf SSH/HTTP/HTTPS. Les sysctl activent les protections classiques sans casser les usages standards. Un opérateur qui voudrait des paramètres plus permissifs surcharge dans ses group_vars.

Étape 3 — Durcir SSH (tasks/ssh.yml)

On éclate les tâches en plusieurs fichiers pour la lisibilité. Le fichier principal tasks/main.yml appelle des sous-fichiers :

---
- ansible.builtin.import_tasks: ssh.yml
  tags: ['hardening', 'ssh']

- ansible.builtin.import_tasks: firewall.yml
  tags: ['hardening', 'firewall']

- ansible.builtin.import_tasks: fail2ban.yml
  tags: ['hardening', 'fail2ban']

- ansible.builtin.import_tasks: sysctl.yml
  tags: ['hardening', 'sysctl']

Et tasks/ssh.yml :

---
- name: Déposer la configuration sshd durcie
  ansible.builtin.template:
    src: sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: '0600'
    validate: 'sshd -t -f %s'
    backup: true
  notify: restart sshd

Trois précautions essentielles dans cette tâche. Le drapeau validate: 'sshd -t -f %s' teste la syntaxe avec sshd avant le remplacement — un fichier mal formé est rejeté avant qu’il ne casse le service. Le drapeau backup: true conserve une copie horodatée du fichier précédent — utile pour restaurer rapidement en cas de problème non détecté par sshd -t. Le mode 0600 applique le bon niveau de confidentialité.

Le gabarit templates/sshd_config.j2 :

Port {{ hardening_ssh_port }}
PermitRootLogin {{ hardening_ssh_permit_root_login }}
PasswordAuthentication {{ hardening_ssh_password_authentication }}
PubkeyAuthentication {{ hardening_ssh_pubkey_authentication }}
ChallengeResponseAuthentication no
UsePAM yes
MaxAuthTries {{ hardening_ssh_max_auth_tries }}
ClientAliveInterval {{ hardening_ssh_client_alive_interval }}
ClientAliveCountMax {{ hardening_ssh_client_alive_count_max }}
X11Forwarding no
PermitEmptyPasswords no
AllowAgentForwarding no
AllowTcpForwarding no
PrintMotd no
{% if hardening_ssh_allow_users %}
AllowUsers {{ hardening_ssh_allow_users | join(' ') }}
{% endif %}
Subsystem sftp /usr/lib/openssh/sftp-server
KexAlgorithms curve25519-sha256@libssh.org,curve25519-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

La sélection des KexAlgorithms, Ciphers et MACs retire les algorithmes faibles ou obsolètes. C’est l’un des contrôles que le CIS Benchmark vérifie en priorité, et c’est aussi ce que recommandent les Mozilla OpenSSH Guidelines en profil Modern. Pour valider depuis l’extérieur, on utilise ssh-audit qui doit retourner un score « good » sur les trois dimensions.

Le handler dans handlers/main.yml :

---
- name: restart sshd
  ansible.builtin.systemd_service:
    name: ssh
    state: restarted

Étape 4 — Pare-feu UFW (tasks/firewall.yml)

UFW est la couche conviviale au-dessus de nftables sur Ubuntu et Debian récents. On l’utilise pour la simplicité de la déclaration ; les administrateurs avancés peuvent passer directement à nftables via la collection community.general.nftables.

---
- name: Installer ufw
  ansible.builtin.apt:
    name: ufw
    state: present

- name: Définir les politiques par défaut UFW
  community.general.ufw:
    direction: "{{ item.dir }}"
    policy: "{{ item.policy }}"
  loop:
    - { dir: incoming, policy: "{{ hardening_ufw_default_incoming }}" }
    - { dir: outgoing, policy: "{{ hardening_ufw_default_outgoing }}" }

- name: Autoriser les ports déclarés
  community.general.ufw:
    rule: allow
    port: "{{ item.port }}"
    proto: "{{ item.proto }}"
    comment: "{{ item.comment | default('') }}"
  loop: "{{ hardening_ufw_allow_rules }}"

- name: Activer UFW et lancer le service
  community.general.ufw:
    state: enabled

L’ordre est crucial : on pose les politiques par défaut, puis on ajoute les règles d’autorisation, puis seulement à la fin on active le pare-feu. Activer UFW avant d’avoir autorisé SSH coupe immédiatement la connexion en cours. Le module community.general.ufw est livré avec la collection community.general à ajouter à collections/requirements.yml si elle n’y est pas déjà.

Étape 5 — Fail2ban (tasks/fail2ban.yml)

---
- name: Installer fail2ban
  ansible.builtin.apt:
    name: fail2ban
    state: present
  when: hardening_fail2ban_enabled

- name: Configurer la prison SSH
  ansible.builtin.copy:
    dest: /etc/fail2ban/jail.d/sshd.local
    content: |
      [sshd]
      enabled = true
      port = {{ hardening_ssh_port }}
      maxretry = {{ hardening_fail2ban_ssh_maxretry }}
      bantime = {{ hardening_fail2ban_ssh_bantime }}
      findtime = 600
    owner: root
    group: root
    mode: '0644'
  when: hardening_fail2ban_enabled
  notify: restart fail2ban

- name: Activer et démarrer fail2ban
  ansible.builtin.systemd_service:
    name: fail2ban
    state: started
    enabled: true
  when: hardening_fail2ban_enabled

L’erreur classique sur fail2ban est de modifier jail.conf directement — il sera écrasé à chaque mise à jour du paquet. La bonne pratique consiste à déposer ses overrides dans jail.d/, fichiers que le paquet ne touche jamais. Les valeurs choisies (maxretry=5, bantime=3600) bloquent une IP pendant une heure après cinq échecs en dix minutes — agressif mais pas paralysant pour un développeur qui se trompe de mot de passe deux fois.

Étape 6 — Sysctl (tasks/sysctl.yml)

---
- name: Appliquer les paramètres sysctl durcis
  ansible.posix.sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    sysctl_file: /etc/sysctl.d/99-hardening.conf
    state: present
    reload: true
  loop: "{{ hardening_sysctl_settings | dict2items }}"

Le module ansible.posix.sysctl écrit les valeurs dans un fichier dédié sous /etc/sysctl.d/ plutôt que dans /etc/sysctl.conf. Cette séparation rend le rôle réversible — on supprime le fichier 99-hardening.conf et on revient aux valeurs par défaut de la distribution. Le drapeau reload: true exécute sysctl -p pour appliquer immédiatement sans redémarrage.

Les paramètres choisis couvrent l’essentiel : rp_filter rejette les paquets dont l’adresse source semble usurpée, tcp_syncookies protège contre les SYN floods, kernel.randomize_va_space active l’ASLR complet, fs.protected_* empêche les attaques par lien symbolique sur /tmp.

Étape 7 — Premier déploiement et vérification

On exécute le rôle avec une seconde session SSH ouverte :

ansible-playbook -i inventory/ playbooks/hardening.yml \
  --vault-password-file ~/.ansible/vault-pass-dev \
  --diff --limit role_web

La sortie défile, on observe les changed sur SSH, UFW, fail2ban, sysctl. À la fin, depuis la seconde session SSH (toujours ouverte), on vérifie que la nouvelle session marche en ouvrant une troisième :

ssh -v ansible@<ip-cible>

Si la connexion réussit, on peut fermer la seconde session de garde-fou. Sinon, on garde celle de garde-fou ouverte et on diagnostique depuis elle, sans toucher à la troisième.

On vérifie ensuite chaque couche :

ansible -i inventory/ role_web -m ansible.builtin.command -a 'ufw status verbose'
ansible -i inventory/ role_web -m ansible.builtin.command -a 'fail2ban-client status sshd'
ansible -i inventory/ role_web -m ansible.builtin.command -a 'sysctl net.ipv4.tcp_syncookies'

Les sorties confirment que les politiques sont en place, que la prison SSH est active avec un compteur d’IP bannies, et que le sysctl prend la valeur attendue.

Étape 8 — Aligner sur le CIS Benchmark sans le réinventer

Dans la continuité que ces bases, on s’appuie sur le projet open source Ansible Lockdown du groupe Ansible Lockdown, qui maintient des rôles communautaires alignés sur les CIS Benchmarks pour Ubuntu, RHEL, Debian. Le rôle UBUNTU24-CIS par exemple couvre près de 200 contrôles individuellement activables. On l’utilise en complément du rôle local : le rôle local impose les paramètres minimaux non négociables, le rôle CIS gère les contrôles plus fins et offre la traçabilité d’audit nécessaire pour une conformité formelle.

L’intégration se fait via roles/requirements.yml :

- name: ansible-lockdown.UBUNTU24-CIS
  src: https://github.com/ansible-lockdown/UBUNTU24-CIS.git
  scm: git
  version: main

Et un play dédié qui applique le rôle avec les exclusions pertinentes — certains contrôles CIS sont incompatibles avec une LEMP qui tourne (par exemple le contrôle « no listening services on port 80 »). Le rôle Lockdown documente comment exclure proprement.

Étape 9 — Audit après durcissement

Le rôle a tourné, les services sont en place, mais comment confirmer que le serveur est réellement durci et non simplement reconfiguré ? Trois outils légers donnent une mesure objective.

Lynis est un scanner d’audit local qui parcourt la configuration système et attribue un score de durcissement. Il vit sur les dépôts officiels Debian/Ubuntu, on l’installe et on l’exécute via Ansible :

ansible -i inventory/ role_web -m ansible.builtin.apt -a 'name=lynis state=present' --become
ansible -i inventory/ role_web -m ansible.builtin.command -a 'lynis audit system --quick' --become

Le rapport remonte dans /var/log/lynis.log. Un score brut autour de 70 sur 100 est typique d’un serveur fraîchement durci ; au-delà de 85 on rentre dans le territoire des serveurs à conformité formelle. Le score n’est pas une fin en soi — Lynis recommande des contrôles parfois inadaptés à un serveur applicatif — mais il oriente vers les axes d’amélioration.

Le scan SSH externe. Depuis votre poste, ssh-audit vérifie depuis l’extérieur les algorithmes effectivement négociés :

pipx install ssh-audit
ssh-audit <ip-cible>

Un serveur durci selon les guidelines Mozilla doit afficher un score « good » sur les kex, les ciphers et les MACs. Les warnings sur les algorithmes faibles disparaissent après le rôle.

Le test du pare-feu. Un nmap -sT <ip-cible> depuis l’extérieur ne doit retourner que les ports déclarés ouverts. Tout autre port retourné open indique soit un service exposé non prévu, soit une règle UFW manquante. La discipline d’avoir UFW + le pare-feu cloud aligne deux niveaux de protection — si l’un faille, l’autre couvre.

Erreurs fréquentes

Erreur Cause Solution
Verrouillé hors du serveur après le run Port SSH changé sans mise à jour de l’inventaire Console cloud + restauration du backup sshd_config
UFW bloque le trafic légitime Règle manquante pour un port applicatif Ajouter dans hardening_ufw_allow_rules et relancer
fail2ban bannit le développeur maxretry trop strict pour les essais légitimes fail2ban-client unban <ip> pour le débloquer
sshd refuse de démarrer Algorithme listé non disponible dans la version d’OpenSSH Le drapeau validate aurait dû le détecter — vérifier qu’il est posé
Sysctl ignoré au reboot Fichier dans /run/sysctl.d/ au lieu de /etc/sysctl.d/ Vérifier la directive sysctl_file du module

Tutoriels frères

Sur le même thème

FAQ

Faut-il changer le port SSH ?
La réponse moderne est nuancée. Changer le port (par exemple en 2222) réduit le bruit des scanners automatiques mais n’apporte aucune sécurité réelle face à un attaquant ciblé qui scannera tous les ports. Le bénéfice principal est la lisibilité des logs — moins de tentatives bruyantes facilite la détection des vraies attaques. Si vous changez le port, mettez à jour hardening_ssh_port, la règle UFW correspondante, et la déclaration port dans la prison fail2ban.

UFW ou nftables direct ?
UFW est suffisant pour la majorité des cas et plus simple à manipuler depuis Ansible. Pour des règles complexes (NAT, marquages, sets dynamiques) on bascule sur nftables via la collection community.general. Sur les serveurs très sollicités, nftables offre aussi de meilleures performances.

Le rôle CIS suffit-il ?
Pour une conformité formelle (PCI-DSS, ISO 27001), il fournit la traçabilité d’audit nécessaire mais reste un point de départ. Un audit indépendant, des tests d’intrusion réguliers et des procédures organisationnelles complètent la démarche.

Comment tester le durcissement sans casser la prod ?
On l’applique d’abord sur un environnement de staging identique en topologie, on lance une suite de tests fonctionnels, puis on déploie en prod par vagues — d’abord un serveur, on observe quelques heures, puis le reste. Pour les rôles complexes, Molecule permet de tester le rôle isolément dans des conteneurs avant même le staging.

Les paramètres sysctl peuvent-ils casser des applications ?
Oui, certains. net.ipv4.tcp_timestamps=0 par exemple bloque certains équilibreurs de charge anciens. La règle est de connaître chaque paramètre activé, pas de copier une liste trouvée sur Internet. Le rôle proposé ici reste prudent.

Faut-il aussi durcir IPv6 ?
Oui, surtout si IPv6 est actif. Les paramètres net.ipv6.conf.* miroirent leurs équivalents IPv4 ; on les ajoute à hardening_sysctl_settings. UFW gère IPv6 par défaut.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité