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
|
||||
Loading…
Add table
Add a link
Reference in a new issue