durcit le serveur : pare-feu nftables, SSH par clé, fail2ban, chrony, mises à jour de sécurité
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b7e866f868
commit
f5a5e8d8a9
13 changed files with 377 additions and 0 deletions
10
ansible/durcissement.yml
Normal file
10
ansible/durcissement.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
# Étape 2 du déploiement Syllabus : durcissement du serveur bare metal.
|
||||
# Touche SSH et le pare-feu — à exécuter uniquement après accord explicite
|
||||
# d'Adam, conformément à la règle 3 du prompt de déploiement.
|
||||
- name: Durcissement du serveur Syllabus
|
||||
hosts: syllabus
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- durcissement
|
||||
2
ansible/inventory.ini
Normal file
2
ansible/inventory.ini
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[syllabus]
|
||||
paillasse ansible_host=163.172.4.195 ansible_user=adam ansible_ssh_private_key_file=~/.ssh/id_ed25519_claude ansible_ssh_common_args='-o IdentitiesOnly=yes'
|
||||
10
ansible/roles/durcissement/defaults/main.yml
Normal file
10
ansible/roles/durcissement/defaults/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
# Ports ouverts sur l'interface publique. Rien d'autre n'est accepté en entrée.
|
||||
durcissement_ports_ouverts:
|
||||
- 22
|
||||
- 80
|
||||
- 443
|
||||
|
||||
# Délai avant retour automatique à l'état d'avant durcissement, si la vérification
|
||||
# post-changement échoue (voir CLAUDE.md et le prompt de déploiement, règle 4).
|
||||
durcissement_delai_filet_secondes: 300
|
||||
23
ansible/roles/durcissement/handlers/main.yml
Normal file
23
ansible/roles/durcissement/handlers/main.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
- name: recharger nftables
|
||||
ansible.builtin.systemd:
|
||||
name: nftables
|
||||
state: restarted
|
||||
enabled: true
|
||||
|
||||
- name: recharger ssh
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: reloaded
|
||||
|
||||
- name: redémarrer fail2ban
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
enabled: true
|
||||
|
||||
- name: redémarrer chrony
|
||||
ansible.builtin.systemd:
|
||||
name: chrony
|
||||
state: restarted
|
||||
enabled: true
|
||||
178
ansible/roles/durcissement/tasks/main.yml
Normal file
178
ansible/roles/durcissement/tasks/main.yml
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
---
|
||||
- name: installer les paquets de durcissement
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- nftables
|
||||
- fail2ban
|
||||
- chrony
|
||||
- unattended-upgrades
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: régler le fuseau horaire sur Europe/Paris
|
||||
community.general.timezone:
|
||||
name: Europe/Paris
|
||||
|
||||
- name: vérifier la présence de systemd-timesyncd
|
||||
ansible.builtin.stat:
|
||||
path: /lib/systemd/system/systemd-timesyncd.service
|
||||
register: durcissement_timesyncd_unit
|
||||
|
||||
- name: désactiver systemd-timesyncd (remplacé par chrony)
|
||||
ansible.builtin.systemd:
|
||||
name: systemd-timesyncd
|
||||
state: stopped
|
||||
enabled: false
|
||||
when: durcissement_timesyncd_unit.stat.exists
|
||||
|
||||
- name: configurer chrony
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
# Géré par Ansible (rôle durcissement) : configuration par défaut de Debian,
|
||||
# conservée volontairement (pools NTP standard). Pas de changement nécessaire
|
||||
# ici pour l'instant : la présence du paquet et le service actif suffisent.
|
||||
dest: /etc/chrony/conf.d/durcissement.conf
|
||||
mode: "0644"
|
||||
notify: redémarrer chrony
|
||||
|
||||
- name: configurer les mises à jour automatiques (listes + sécurité)
|
||||
ansible.builtin.template:
|
||||
src: 20auto-upgrades.j2
|
||||
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||
mode: "0644"
|
||||
|
||||
- name: interdire le redémarrage automatique par unattended-upgrades
|
||||
ansible.builtin.template:
|
||||
src: 51unattended-upgrades-local.conf.j2
|
||||
dest: /etc/apt/apt.conf.d/51unattended-upgrades-local
|
||||
mode: "0644"
|
||||
|
||||
- name: configurer fail2ban (jail sshd, backend nftables)
|
||||
ansible.builtin.template:
|
||||
src: jail.local.j2
|
||||
dest: /etc/fail2ban/jail.local
|
||||
mode: "0644"
|
||||
notify: redémarrer fail2ban
|
||||
|
||||
- name: déployer le script de retour à l'état d'avant durcissement
|
||||
ansible.builtin.template:
|
||||
src: durcissement-rollback.sh.j2
|
||||
dest: /usr/local/sbin/durcissement-rollback.sh
|
||||
mode: "0700"
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
# --- À partir d'ici : modifications du pare-feu et de SSH. ---
|
||||
# Règle 4 (anti-coupure) : on arme un retour automatique avant de toucher à quoi
|
||||
# que ce soit, et on ne l'annule qu'après avoir vérifié l'accès depuis une
|
||||
# session SSH toute neuve (voir la vérification plus bas, delegate_to: localhost).
|
||||
|
||||
- name: armer le filet de sécurité ({{ durcissement_delai_filet_secondes }} s)
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
systemd-run --unit=durcissement-rollback
|
||||
--on-active={{ durcissement_delai_filet_secondes }}
|
||||
/usr/local/sbin/durcissement-rollback.sh
|
||||
become: true
|
||||
changed_when: true
|
||||
|
||||
- name: déployer la politique de pare-feu nftables
|
||||
ansible.builtin.template:
|
||||
src: nftables.conf.j2
|
||||
dest: /etc/nftables.conf
|
||||
mode: "0640"
|
||||
owner: root
|
||||
group: root
|
||||
validate: "nft -c -f %s"
|
||||
notify: recharger nftables
|
||||
|
||||
- name: appliquer le pare-feu immédiatement
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: déployer la configuration SSH durcie
|
||||
ansible.builtin.template:
|
||||
src: sshd-durcissement.conf.j2
|
||||
dest: /etc/ssh/sshd_config.d/99-durcissement.conf
|
||||
mode: "0644"
|
||||
owner: root
|
||||
group: root
|
||||
register: durcissement_sshd_conf
|
||||
notify: recharger ssh
|
||||
|
||||
- name: valider la syntaxe sshd avant de recharger
|
||||
ansible.builtin.command: sshd -t
|
||||
become: true
|
||||
changed_when: false
|
||||
|
||||
- name: appliquer la configuration SSH
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: pause avant vérification (laisser les services redémarrer)
|
||||
ansible.builtin.pause:
|
||||
seconds: 3
|
||||
|
||||
- name: "vérification : nouvelle connexion SSH par clé (depuis le Mac)"
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
ssh -o IdentitiesOnly=yes -o IdentityFile=~/.ssh/id_ed25519_claude
|
||||
-o ControlMaster=no -o ControlPath=none -o BatchMode=yes
|
||||
-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new
|
||||
adam@163.172.4.195 echo CONNEXION_CLE_OK
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: durcissement_verif_cle
|
||||
changed_when: false
|
||||
|
||||
- name: "vérification : le mot de passe est refusé"
|
||||
# BatchMode empêche seulement l'invite interactive ; le serveur annonce quand
|
||||
# même au client les méthodes qu'il accepte. On vérifie donc le contenu du
|
||||
# message d'erreur ("password" ne doit plus y figurer), pas seulement le rc.
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
ssh -o IdentitiesOnly=yes -o PubkeyAuthentication=no
|
||||
-o PreferredAuthentications=password,keyboard-interactive
|
||||
-o BatchMode=yes -o ConnectTimeout=10
|
||||
adam@163.172.4.195 echo NE_DOIT_PAS_S_AFFICHER
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: durcissement_verif_mdp
|
||||
changed_when: false
|
||||
failed_when: >
|
||||
durcissement_verif_mdp.rc == 0 or
|
||||
'password' in durcissement_verif_mdp.stderr
|
||||
|
||||
- name: "vérification : root est refusé"
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
ssh -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
root@163.172.4.195 echo NE_DOIT_PAS_S_AFFICHER
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: durcissement_verif_root
|
||||
changed_when: false
|
||||
failed_when: durcissement_verif_root.rc == 0
|
||||
|
||||
- name: "vérification : seuls 22, 80, 443 sont ouverts (nmap depuis le Mac)"
|
||||
# -p- (65535 ports) est beaucoup trop lent face à une politique "drop" : chaque
|
||||
# port fermé attend le timeout au lieu d'un rejet immédiat, et ça dépasse le
|
||||
# délai du filet de sécurité avant même d'avoir fini de scanner. Les 1000 ports
|
||||
# les plus courants (défaut de nmap) suffisent à vérifier qu'il n'y a rien
|
||||
# d'inattendu, et -T4 accélère le scan.
|
||||
ansible.builtin.command:
|
||||
cmd: nmap -Pn -T4 --open 163.172.4.195
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: durcissement_verif_nmap
|
||||
changed_when: false
|
||||
|
||||
- name: afficher le résultat nmap
|
||||
ansible.builtin.debug:
|
||||
var: durcissement_verif_nmap.stdout_lines
|
||||
|
||||
- name: désarmer le filet de sécurité (tout est vérifié)
|
||||
ansible.builtin.command:
|
||||
cmd: systemctl stop durcissement-rollback.timer
|
||||
become: true
|
||||
changed_when: true
|
||||
3
ansible/roles/durcissement/templates/20auto-upgrades.j2
Normal file
3
ansible/roles/durcissement/templates/20auto-upgrades.j2
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// Géré par Ansible (rôle durcissement) — ne pas modifier à la main.
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Géré par Ansible (rôle durcissement) — ne pas modifier à la main.
|
||||
// Un redémarrage est une opération à risque (règle 3 du prompt de déploiement) :
|
||||
// il doit toujours être décidé et déclenché par Adam, jamais automatiquement.
|
||||
Unattended-Upgrade::Automatic-Reboot "false";
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
# Géré par Ansible (rôle durcissement) — filet de sécurité anti-coupure SSH.
|
||||
#
|
||||
# Déclenché par un timer systemd transient ({{ durcissement_delai_filet_secondes }} s)
|
||||
# programmé juste avant de modifier le pare-feu ou SSH. Si la vérification depuis
|
||||
# le Mac échoue, ce script remet le serveur dans l'état d'avant durcissement
|
||||
# (pare-feu complètement ouvert, pas de restriction SSH). Si tout va bien, le
|
||||
# timer est annulé avant de se déclencher et ce script ne s'exécute jamais.
|
||||
set -e
|
||||
|
||||
nft flush ruleset
|
||||
rm -f /etc/ssh/sshd_config.d/99-durcissement.conf
|
||||
systemctl reload ssh || systemctl restart ssh
|
||||
|
||||
logger -t durcissement-rollback "Rollback automatique déclenché : pare-feu et SSH remis à l'état d'avant durcissement."
|
||||
18
ansible/roles/durcissement/templates/jail.local.j2
Normal file
18
ansible/roles/durcissement/templates/jail.local.j2
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Géré par Ansible (rôle durcissement) — ne pas modifier à la main.
|
||||
[DEFAULT]
|
||||
bantime = 1h
|
||||
findtime = 10m
|
||||
# 10, pas 5 : nos propres vérifications de durcissement (tentative mot de passe
|
||||
# refusée, tentative root refusée) comptent comme des échecs. À 5, on s'est
|
||||
# banni nous-mêmes dès le 2e ou 3e passage du playbook en quelques minutes
|
||||
# (voir docs/postmortems/2026-10-07-fail2ban-auto-ban.md). 10 reste largement
|
||||
# suffisant contre un bruteforce réel.
|
||||
maxretry = 10
|
||||
backend = systemd
|
||||
# Le serveur n'a pas iptables installé (seulement nftables) : il faut le dire
|
||||
# explicitement à fail2ban, sinon son action par défaut échoue.
|
||||
banaction = nftables-multiport
|
||||
banaction_allports = nftables-allports
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
29
ansible/roles/durcissement/templates/nftables.conf.j2
Normal file
29
ansible/roles/durcissement/templates/nftables.conf.j2
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/sbin/nft -f
|
||||
# Géré par Ansible (rôle durcissement) — ne pas modifier à la main.
|
||||
# Politique : tout est refusé en entrée sauf loopback, trafic déjà établi,
|
||||
# ICMP, et les ports {{ durcissement_ports_ouverts | join(', ') }}.
|
||||
|
||||
flush ruleset
|
||||
|
||||
table inet filter {
|
||||
chain input {
|
||||
type filter hook input priority 0; policy drop;
|
||||
|
||||
iif lo accept
|
||||
ct state established,related accept
|
||||
ct state invalid drop
|
||||
|
||||
ip protocol icmp accept
|
||||
meta l4proto ipv6-icmp accept
|
||||
|
||||
tcp dport { {{ durcissement_ports_ouverts | join(', ') }} } accept
|
||||
}
|
||||
|
||||
chain forward {
|
||||
type filter hook forward priority 0; policy drop;
|
||||
}
|
||||
|
||||
chain output {
|
||||
type filter hook output priority 0; policy accept;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Géré par Ansible (rôle durcissement) — ne pas modifier à la main.
|
||||
# Lu en premier (Include est la première ligne de sshd_config), donc ces valeurs
|
||||
# l'emportent sur les réglages par défaut du fichier principal.
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
X11Forwarding no
|
||||
|
|
@ -29,3 +29,40 @@ Avant de changer quoi que ce soit, il fallait savoir précisément sur quoi on a
|
|||
**Suite**
|
||||
|
||||
Étape 2 : durcissement SSH + pare-feu nftables + fail2ban/chrony/unattended-upgrades, via un rôle Ansible depuis le Mac. C'est une étape qui touche SSH et le réseau : je vais détailler la commande avant de l'exécuter, prévoir un retour arrière automatique (anti-coupure), et ne redémarrer/couper rien sans l'accord explicite d'Adam pour les points couverts par la règle 3 du prompt de déploiement.
|
||||
|
||||
## 2026-10-07 — Étape 2 : durcissement du serveur
|
||||
|
||||
**Fait**
|
||||
|
||||
- Rôle Ansible `durcissement` (`ansible/roles/durcissement/`) + playbook `ansible/durcissement.yml`, appliqués après accord explicite d'Adam.
|
||||
- Pare-feu nftables : tout refusé en entrée sauf loopback, trafic établi, ICMP, et les ports 22/80/443.
|
||||
- SSH : `PermitRootLogin no`, `PasswordAuthentication no` (connexion par clé uniquement), via un drop-in `/etc/ssh/sshd_config.d/99-durcissement.conf`.
|
||||
- `fail2ban` (jail `sshd`, backend `nftables` car pas d'`iptables` sur l'image), `chrony` (remplace `systemd-timesyncd`), `unattended-upgrades` (sécurité auto, **redémarrage automatique explicitement désactivé** — un reboot reste une décision d'Adam).
|
||||
- Filet de sécurité anti-coupure : timer `systemd-run` de 5 minutes armé avant toute modification du pare-feu/SSH, script de retour à l'état précédent (`/usr/local/sbin/durcissement-rollback.sh`), désarmé seulement après vérification réussie depuis une connexion SSH toute neuve + `nmap`.
|
||||
- Vérifié deux fois l'idempotence (rejouer le playbook ne change rien d'inattendu).
|
||||
|
||||
**Commandes clés**
|
||||
|
||||
```bash
|
||||
cd ansible && script -q /dev/null ansible-playbook durcissement.yml -i inventory.ini
|
||||
ssh paillasse "sudo systemctl list-timers | grep durcissement" # filet de sécurité
|
||||
nmap -Pn -T4 --open 163.172.4.195
|
||||
```
|
||||
|
||||
**Problèmes rencontrés et solution**
|
||||
|
||||
1. Bug dans le rôle : le `notify` pour recharger `sshd` manquait sur la tâche de dépôt de la config SSH → `sshd` jamais rechargé au 1er essai → le filet de sécurité s'est déclenché (normal, rien de cassé) → corrigé en ajoutant le `notify`.
|
||||
2. Le test de vérification `nmap -p-` (65535 ports) était beaucoup trop lent face à une politique `drop` (chaque port fermé attend un timeout) → dépassait les 5 minutes du filet de sécurité → 2ᵉ déclenchement automatique du rollback (normal, rien de cassé) → corrigé en limitant le scan aux ports par défaut + `-T4` (4,5 s au lieu de plusieurs minutes).
|
||||
3. **Incident réel : auto-bannissement par fail2ban.** Les tentatives volontairement ratées (mot de passe, root) de nos propres tests de vérification, répétées sur plusieurs runs rapprochés, ont dépassé le seuil `maxretry = 5` de fail2ban : l'IP du Mac d'Adam s'est fait bannir par son propre pare-feu. Résolu via la console web Scaleway (hors bande) avec `fail2ban-client set sshd unbanip`. Post-mortem complet : `docs/postmortems/2026-10-07-fail2ban-auto-ban.md`. Correctif : `maxretry` relevé à 10.
|
||||
|
||||
**Critère vérifié**
|
||||
|
||||
- Connexion par clé : OK. Mot de passe : refusé (`Permission denied (publickey)`, sans mention de `password` dans les méthodes offertes). Root : refusé. `nmap -Pn -T4 --open 163.172.4.195` depuis le Mac : seul le port 22 est ouvert (80/443 autorisés par le pare-feu mais rien n'écoute encore dessus — normal, le portail arrive à l'étape 8).
|
||||
|
||||
**Comprendre**
|
||||
|
||||
Durcir un serveur, c'est fondamentalement risqué pour l'accès qu'on a déjà : chaque changement de pare-feu ou de SSH peut potentiellement nous enfermer dehors. D'où le filet de sécurité (un minuteur qui annule tout automatiquement si on ne confirme pas que ça marche dans les 5 minutes) : il s'est déclenché deux fois pendant la mise au point, et les deux fois c'était le bon comportement — un bug ailleurs, pas un défaut du filet. L'incident fail2ban est différent et plus subtil : ce n'est pas le pare-feu qui nous a bloqués, c'est un service de protection qu'on vient d'installer qui a réagi à nos propres tests comme à une attaque. C'est un vrai piège classique en administration système, qui vaut la peine d'être raconté en entretien : on protège un système contre tout le monde, y compris soi-même si on ne fait pas attention au volume de ses propres tentatives.
|
||||
|
||||
**Suite**
|
||||
|
||||
Étape 3 : sauvegardes vers Dedibackup (archives `tar.zst` chiffrées `age`, rotation 7 quotidiennes + 4 hebdomadaires, timer systemd).
|
||||
|
|
|
|||
40
docs/postmortems/2026-10-07-fail2ban-auto-ban.md
Normal file
40
docs/postmortems/2026-10-07-fail2ban-auto-ban.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Post-mortem — auto-bannissement fail2ban pendant le durcissement
|
||||
|
||||
- **Date** : 2026-10-07
|
||||
- **Durée de l'incident** : ~1 h (accès SSH restauré manuellement au bout de quelques minutes via la console Scaleway)
|
||||
- **Gravité** : faible — aucune perte de données, aucune coupure de service (rien n'est encore en production), juste une perte temporaire d'accès SSH depuis le Mac d'Adam.
|
||||
- **Impact** : Claude Code ne pouvait plus se connecter en SSH au serveur Syllabus depuis le Mac d'Adam.
|
||||
|
||||
## Résumé
|
||||
|
||||
Pendant la mise au point du rôle Ansible `durcissement` (étape 2 du déploiement), le playbook a été relancé plusieurs fois en quelques minutes pour corriger des bugs (voir plus bas) et vérifier l'idempotence. Chaque exécution du playbook tente volontairement une connexion avec mot de passe et une connexion root, pour vérifier qu'elles sont bien refusées. Ces tentatives volontairement ratées ont été comptées par **fail2ban** comme des échecs d'authentification. Avec le seuil initial (`maxretry = 5` sur `findtime = 10m`), l'IP publique du Mac d'Adam a dépassé ce seuil et s'est fait bannir par son propre pare-feu, pendant `bantime = 1h`.
|
||||
|
||||
## Chronologie
|
||||
|
||||
1. 1er run du playbook : bug découvert (le `notify` pour recharger `sshd` manquait sur la tâche de déploiement de la config SSH). Le pare-feu a été appliqué mais SSH non rechargé → le test « root refusé » échoue réellement (root pouvait encore se connecter). Le filet de sécurité (timer de 5 min) n'est jamais désarmé → il se déclenche et remet tout à l'état d'origine. Aucune conséquence négative : c'est exactement le rôle de ce filet.
|
||||
2. Correction du bug, 2ᵉ run : le test nmap (`-p-`, les 65535 ports) s'est révélé trop lent face à une politique de pare-feu `drop` (chaque port fermé attend un timeout au lieu d'un rejet immédiat). Le run dépasse les 5 minutes du filet de sécurité avant d'arriver à l'étape de désarmement → 2ᵉ déclenchement automatique du rollback. Aucune conséquence négative, même cause (filet de sécurité qui fait son travail).
|
||||
3. Correction du scan nmap (ports par défaut + `-T4`, 4,5 s au lieu de plusieurs minutes), 3ᵉ run : succès complet, toutes les vérifications passent, le filet est désarmé normalement.
|
||||
4. Relance d'un 4ᵉ run pour vérifier l'idempotence (bonne pratique, règle 7 du prompt de déploiement) : **SSH refuse la connexion** (`Connection refused`, pas un timeout). Le ping fonctionne : le serveur est up, seul le port 22 est concerné.
|
||||
5. Diagnostic : fail2ban a banni l'IP publique du Mac (`89.86.35.23`), déclenché par l'accumulation des tentatives « mot de passe refusé » et « root refusé » des runs 1 à 3, toutes dans la même fenêtre `findtime` de 10 minutes.
|
||||
6. Résolution : Adam s'est connecté à la console web Scaleway (hors bande, sans dépendre du réseau/SSH) et a lancé `sudo fail2ban-client set sshd unbanip 89.86.35.23` directement sur le serveur. Accès SSH restauré immédiatement.
|
||||
7. Correctif durable : `maxretry` relevé de 5 à 10 dans `ansible/roles/durcissement/templates/jail.local.j2`, pour tolérer les futures vérifications répétées sans affaiblir significativement la protection contre un vrai bruteforce.
|
||||
|
||||
## Cause racine
|
||||
|
||||
Le seuil de bannissement de fail2ban ne tenait pas compte du fait que **le playbook de durcissement lui-même génère des échecs d'authentification volontaires** à chaque exécution, pour vérifier que les protections fonctionnent. Plusieurs exécutions rapprochées (pendant la mise au point) ont donc eu le même effet qu'une attaque par force brute à faible volume, et fail2ban a réagi exactement comme prévu — contre nous.
|
||||
|
||||
## Ce qui a bien fonctionné
|
||||
|
||||
- Le filet de sécurité anti-coupure (timer `systemd-run` de 5 minutes, règle 4 du prompt de déploiement) a fonctionné parfaitement aux essais 1 et 2 : aucune coupure réelle, retour automatique et propre à l'état précédent, confirmé dans les journaux (`journalctl -t durcissement-rollback`).
|
||||
- fail2ban a démontré qu'il fonctionne (il a banni une IP suite à des échecs répétés), même si c'était la mauvaise IP.
|
||||
- La console web du fournisseur (hors bande, indépendante du réseau du serveur) a permis de résoudre l'incident sans attendre l'expiration du bannissement.
|
||||
|
||||
## Actions correctives
|
||||
|
||||
- [x] `maxretry` relevé de 5 à 10 dans le jail `sshd` (`ansible/roles/durcissement/templates/jail.local.j2`).
|
||||
- [x] Scan nmap de vérification limité aux ports par défaut (au lieu de `-p-`), avec `-T4` : 4,5 s au lieu de plusieurs minutes, pour rester largement dans la fenêtre du filet de sécurité.
|
||||
- [ ] À envisager plus tard (pas fait maintenant, pas nécessaire dans l'immédiat) : un jail ou une règle dédiée qui ignore les tentatives provenant du processus de vérification Ansible lui-même, si le besoin se reproduit.
|
||||
|
||||
## Comprendre
|
||||
|
||||
Fail2ban compte les échecs de connexion SSH, peu importe qui les fait : un vrai attaquant ou nos propres tests. Comme le rôle de durcissement vérifie activement que le mot de passe et root sont refusés — donc provoque volontairement des échecs — relancer ce rôle plusieurs fois en peu de temps ressemble, du point de vue de fail2ban, à une attaque. C'est un classique en administration système : les outils de protection automatique protègent contre tout le monde, y compris leurs propres opérateurs. La leçon à retenir : quand on écrit un test qui provoque volontairement un échec d'authentification, il faut aussi réfléchir à l'effet cumulé de ce test sur les systèmes de détection d'intrusion, surtout si le test va être rejoué souvent pendant la mise au point.
|
||||
Loading…
Add table
Add a link
Reference in a new issue