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:
Adam Belkacemi 2026-10-07 10:32:09 +02:00
parent b7e866f868
commit f5a5e8d8a9
13 changed files with 377 additions and 0 deletions

10
ansible/durcissement.yml Normal file
View 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
View 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'

View 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

View 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

View 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

View 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";

View file

@ -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";

View file

@ -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."

View 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

View 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;
}
}

View file

@ -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