Chapitre 1320 minIntermédiaire
Sécuriser et dépanner un serveur de services : nftables, fail2ban et méthode
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- réduire les ports exposés d'un serveur ;
- écrire le pare-feu local nftables d'un serveur à partir d'une matrice de flux ;
- bloquer les attaques par force brute avec fail2ban sans bloquer les administrateurs ;
- vérifier le durcissement de chaque service ;
- dépanner un service étage par étage, du serveur au client.
3 exercices corrigés1 schéma
Sécuriser un serveur de services, c'est réduire ce qui est exposé (ports, comptes, informations), filtrer ce qui reste avec un pare-feu local, bloquer automatiquement les attaques répétées et durcir chaque service. Dépanner, c'est trouver méthodiquement la couche qui ne fonctionne pas, du service au réseau. Ce chapitre configure le pare-feu local nftables de srv-glpi, installe fail2ban, rassemble les réglages de durcissement des chapitres précédents dans une liste de contrôle, et donne une méthode de dépannage applicable à tous les services. Le durcissement du système lui-même (SSH, comptes, sudo, AppArmor, mises à jour) est traité dans le cours Linux — Administration système, et les principes du durcissement dans le cours Cybersécurité des infrastructures.
Réduire la surface d'attaque
Chaque port ouvert est une porte. Commence par la liste de ce qui écoute vraiment :
sudo ss -tlnup
Netid State Local Address:Port Process
udp UNCONN 127.0.0.1:323 users:(("chronyd",pid=611,fd=5))
tcp LISTEN 127.0.0.1:3306 users:(("mariadbd",pid=1027,fd=21))
tcp LISTEN 127.0.0.1:25 users:(("master",pid=1290,fd=13))
tcp LISTEN 0.0.0.0:22 users:(("sshd",pid=702,fd=3))
tcp LISTEN 0.0.0.0:80 users:(("nginx",pid=812,fd=5))
tcp LISTEN 0.0.0.0:443 users:(("nginx",pid=812,fd=6))
Lis la colonne Local Address : 127.0.0.1 signifie que le service n'est joignable que depuis la machine (MariaDB, Postfix, chrony), 0.0.0.0 ou * qu'il écoute sur toutes les interfaces. Sur srv-glpi, seuls SSH, HTTP et HTTPS doivent être joignables depuis le réseau. Tout autre port ouvert est soit un service à désinstaller, soit un service à faire écouter en local.
Le pare-feu local avec nftables
Le pare-feu pfSense filtre les flux entre les VLAN ; il ne voit pas ce qui circule à l'intérieur d'un même VLAN. Un pare-feu local sur chaque serveur ajoute une seconde barrière (on parle de défense en profondeur) : si un serveur voisin est compromis, il ne peut joindre de srv-glpi que ce qui est explicitement autorisé. Sous Debian 13, le pare-feu du noyau se configure avec nftables, qui remplace iptables.
La politique de srv-glpi, tirée de la matrice des flux :
| Source | Port | Pourquoi |
|---|---|---|
Réseau d'administration 192.168.10.208/28 | TCP 22 | Administration en SSH |
Postes 192.168.10.64/26 et administration 192.168.10.208/28 | TCP 80, 443 | GLPI et ses agents d'inventaire (le port 80 ne fait que rediriger vers HTTPS) |
srv-journaux 192.168.10.180 | TCP 3306 | Grafana lit la base (chapitre 5) |
| Tout | ICMP écho, ICMPv6 de voisinage | Diagnostic (ping) et fonctionnement d'IPv6 |
| Le reste | — | Refusé et journalisé |
/etc/nftables.conf :
#!/usr/sbin/nft -f
flush ruleset
table inet filtre {
set admin {
type ipv4_addr
flags interval
elements = { 192.168.10.208/28 }
}
set clients_glpi {
type ipv4_addr
flags interval
elements = { 192.168.10.64/26, 192.168.10.208/28 }
}
chain entree {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
icmp type echo-request accept
icmpv6 type { echo-request, nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } accept
ip saddr @admin tcp dport 22 accept
ip saddr @clients_glpi tcp dport { 80, 443 } accept
ip saddr 192.168.10.180 tcp dport 3306 accept
limit rate 10/minute log prefix "nft-refus: "
}
chain transfert {
type filter hook forward priority filter; policy drop;
}
chain sortie {
type filter hook output priority filter; policy accept;
}
}
| Élément | Rôle |
|---|---|
table inet filtre | Une table pour IPv4 et IPv6 à la fois (famille inet) |
set admin | Un ensemble nommé d'adresses, réutilisable dans plusieurs règles ; flags interval permet d'y mettre des réseaux |
hook input … policy drop | La chaîne qui voit le trafic destiné au serveur ; tout ce qui n'est pas accepté est jeté |
ct state established,related accept | Les réponses aux connexions déjà acceptées, et celles que le serveur a ouvertes, passent |
ct state invalid drop | Les paquets incohérents sont jetés |
iif "lo" accept | Les échanges internes à la machine (Nginx vers PHP-FPM, PHP vers MariaDB) |
limit rate 10/minute log prefix … | Journalise une partie des refus (10 par minute au plus, pour ne pas saturer le journal) avant que la politique ne les jette |
hook forward … policy drop | Le serveur n'est pas un routeur : il ne transmet rien |
hook output … policy accept | Le serveur peut ouvrir les connexions dont il a besoin (DNS, annuaire, mises à jour, relais SMTP) |
Appliquer sans se couper. Une règle SSH fausse, et tu perds la main sur le serveur. Vérifie d'abord la syntaxe, garde la console de Proxmox ouverte, puis applique :
sudo nft -c -f /etc/nftables.conf # vérifie sans appliquer
sudo systemctl enable --now nftables # applique, et à chaque démarrage
sudo nft list ruleset
journalctl -k -g "nft-refus" --since "10 min ago" # les refus journalisés
Pour les autres serveurs du cours, seules les règles d'ouverture changent :
| Serveur | Règles d'entrée en plus de SSH depuis @admin |
|---|---|
srv-web1, srv-web2 | ip saddr { 192.168.10.228, 192.168.10.229 } tcp dport 80 accept |
lb1, lb2 | tcp dport { 80, 443 } accept, ip saddr @admin tcp dport 8404 accept, ip protocol 112 ip saddr { 192.168.10.228, 192.168.10.229 } accept (VRRP) ; sur lb1 seulement : ip saddr 192.168.10.229 tcp dport 8888 accept (défis Let's Encrypt relayés par lb2) |
srv-partages | SMB (TCP 445) depuis les réseaux qui utilisent le partage, NFS (TCP 2049) depuis srv-glpi (exercice ci-dessous) |
srv-journaux | ip saddr { 192.168.10.176/28, 192.168.10.224/28 } tcp dport { 1514, 1515 } accept (agents Wazuh : seulement les serveurs, DMZ comprise) et ip saddr { 192.168.10.64/26, 192.168.10.128/27, 192.168.10.160/28, 192.168.10.176/28, 192.168.10.208/28, 192.168.10.224/28 } tcp dport 6514 accept (syslog TLS depuis les zones internes, comme la matrice des flux ; pas depuis le VLAN 40 des visiteurs) |
Bloquer les attaques par force brute avec fail2ban
Une attaque par force brute essaie des milliers de mots de passe sur un service exposé ; une attaque par dictionnaire essaie les mots de passe et les noms de comptes les plus courants. fail2ban lit les journaux, compte les échecs par adresse et, au-delà d'un seuil, ajoute une règle de pare-feu qui bloque cette adresse pendant un temps donné.
sudo apt install fail2ban
Sous Debian 13, fail2ban est préconfiguré : la prison (jail) sshd est active, lit le journal systemd et bloque avec nftables (/etc/fail2ban/jail.d/defaults-debian.conf). Ajoute tes réglages dans un fichier à toi, jamais dans jail.conf, que les mises à jour remplacent :
# /etc/fail2ban/jail.d/sorbier.conf
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
# Ne jamais bloquer le réseau d'administration
ignoreip = 127.0.0.1/8 ::1 192.168.10.208/28
# Debian 13 : les échecs SSH sont tracés par sshd-session
[sshd]
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/*.error.log
| Paramètre | Rôle |
|---|---|
findtime, maxretry | Au-delà de 5 échecs en 10 minutes depuis une même adresse… |
bantime | … l'adresse est bloquée une heure |
[sshd] et journalmatch | Le fichier defaults-debian.conf de Debian 13 limite la prison aux messages du programme sshd (_COMM=sshd). Or, depuis OpenSSH 9.8, les échecs d'authentification sont écrits par sshd-session : sans cette ligne, la prison ne voit aucun échec et ne bloque personne |
ignoreip | Des adresses jamais bloquées : sans cette ligne, un administrateur qui se trompe cinq fois de mot de passe se bloque lui-même |
[nginx-http-auth] | Une prison fournie par fail2ban pour les échecs d'authentification HTTP de Nginx (la page /stats/ du chapitre 2) |
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client get sshd journalmatch # vérifie que sshd-session est bien lu
# _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session
sudo fail2ban-client status sshd
# Status for the jail: sshd
# |- Filter
# | |- Currently failed: 1
# | |- Total failed: 38
# | `- Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session
# `- Actions
# |- Currently banned: 2
# |- Total banned: 4
# `- Banned IP list: 198.51.100.23 203.0.113.99
sudo fail2ban-client set sshd unbanip 198.51.100.23 # débloquer une adresse
Durcir chaque service : la liste de contrôle
Ce tableau rassemble les réglages vus dans le cours. C'est une base de recette : chaque ligne se vérifie par une commande.
| Service | Réglage | Vérification |
|---|---|---|
| Système | Mises à jour de sécurité automatiques | systemctl list-timers 'apt-daily*', /var/log/unattended-upgrades/ |
| Système | Heure synchronisée sur l'annuaire | chronyc tracking |
| SSH | Pas de root direct, clés pour les comptes locaux, accès depuis ADMIN seulement | sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication', règles nftables |
| Nginx | Version masquée, site par défaut qui ne sert rien, fichiers cachés refusés | curl -I, curl -I http://<adresse IP> |
| Nginx | HTTPS seul, HSTS, TLS 1.2 et 1.3 | curl -sI http://… (301), openssl s_client |
| PHP | expose_php = Off, display_errors = Off, un pool et un compte par application | curl -I (pas de X-Powered-By), ps -o user,cmd -C php-fpm8.4 |
| MariaDB | Écoute locale ou restreinte, root par socket, un compte limité par application | ss -tlnp, SHOW GRANTS |
| Certificats | Renouvellement automatique, date supervisée | certbot renew --dry-run, supervision |
| Samba | SMB 3 minimum, pas d'invité, droits par groupe | testparm -s |
| NFS | Export limité à des adresses, root_squash | exportfs -v |
| Postfix | Pas de relais ouvert : loopback-only ou mynetworks restreint | postconf inet_interfaces mynetworks |
| Journaux | Envoyés à srv-journaux, agent Wazuh actif | logger puis lecture sur srv-journaux ; état de l'agent |
| Pare-feu local | Politique drop en entrée, ouvertures documentées | nft list ruleset |
systemd peut aussi enfermer un service : lui interdire d'écrire hors de certains dossiers, de voir les fichiers personnels, d'obtenir de nouveaux privilèges. systemd-analyze security note l'exposition de chaque service de 0 (très enfermé) à 10 (aucune restriction) :
systemd-analyze security nginx.service
La plupart des services des paquets Debian obtiennent un score élevé, faute de restrictions dans leur unité. On peut en ajouter avec systemctl edit nginx (directives ProtectSystem=, ProtectHome=, NoNewPrivileges=…), mais avec prudence : une restriction trop forte empêche le service de fonctionner. C'est une piste d'amélioration à citer, à tester sur une maquette.
La méthode de dépannage
Quand un utilisateur dit « GLPI ne marche pas », la panne peut se trouver à chaque étage du chemin. La méthode : partir du service et remonter vers le client, en vérifiant chaque couche avec une commande, jusqu'à celle qui échoue.
| Étage | Commande | Si ça échoue |
|---|---|---|
| 1. Le service tourne | systemctl status nginx php8.4-fpm mariadb | Lire journalctl -u <service> : configuration invalide, port déjà pris, disque plein |
| 2. Il écoute | ss -tlnp | grep -E ':80|:443' | Directive listen ou bind-address, service qui écoute en local seulement |
| 3. Il répond en local | curl -kI https://localhost | Erreur de l'application ou de la liaison Nginx et PHP-FPM (chapitre 4) |
| 4. Le pare-feu local | sudo nft list ruleset, journalctl -k -g nft-refus | Règle manquante |
| 5. Le réseau | Depuis le client : nc -zv srv-glpi.ad.sorbier.example 443 ; sur le serveur : sudo tcpdump -ni ens18 port 443 | Routage, VLAN, règle pfSense (le paquet n'arrive pas au serveur) |
| 6. Le DNS | host glpi.ad.sorbier.example depuis le client | Enregistrement absent ou faux, mauvais serveur DNS sur le client |
| 7. Le certificat | openssl s_client -connect glpi.ad.sorbier.example:443 -servername glpi.ad.sorbier.example | Nom, date, chaîne, racine inconnue (chapitre 6) |
| 8. L'application | tail /var/log/nginx/glpi.error.log /var/log/glpi/php-errors.log | Base injoignable, droits sur les fichiers, extension PHP manquante |
tcpdump est le juge de paix entre « le réseau » et « le serveur » : si les paquets du client n'arrivent pas sur l'interface du serveur, inutile de chercher dans Nginx ; s'ils arrivent et que le serveur ne répond pas, la cause est sur le serveur (pare-feu local, service).
Exercices
Exercice 1 sur 3 Le pare-feu de srv-partages Réussi À revoir
Écris le fichier /etc/nftables.conf de srv-partages : SSH depuis le réseau d'administration ; SMB (TCP 445) depuis le VLAN des postes 192.168.10.64/26 et le VLAN de l'entrepôt 192.168.10.128/27 ; NFS (TCP 2049) depuis srv-glpi seulement ; ICMP écho ; refus journalisé de tout le reste.
Voir la réponseMasquer la réponse de l'exercice 1
#!/usr/sbin/nft -f
flush ruleset
table inet filtre {
set admin {
type ipv4_addr
flags interval
elements = { 192.168.10.208/28 }
}
set clients_smb {
type ipv4_addr
flags interval
elements = { 192.168.10.64/26, 192.168.10.128/27 }
}
chain entree {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
icmp type echo-request accept
icmpv6 type { echo-request, nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } accept
ip saddr @admin tcp dport 22 accept
ip saddr @clients_smb tcp dport 445 accept
ip saddr 192.168.10.184 tcp dport 2049 accept
limit rate 10/minute log prefix "nft-refus: "
}
chain transfert {
type filter hook forward priority filter; policy drop;
}
chain sortie {
type filter hook output priority filter; policy accept;
}
}
Puis sudo nft -c -f /etc/nftables.conf et sudo systemctl enable --now nftables, avec la console de l'hyperviseur ouverte. Les flux des postes et de l'entrepôt vers le VLAN 20 traversent aussi pfSense, qui doit les autoriser ; le flux NFS de srv-glpi, dans le même VLAN, n'est filtré que par cette règle locale.
Tu avais trouvé ?
Exercice 2 sur 3 Dépanner pas à pas Réussi À revoir
Lundi matin, les techniciens ne peuvent plus ouvrir https://glpi.ad.sorbier.example : le navigateur affiche « Ce site est inaccessible » après un long délai. Voici tes premières vérifications : systemctl status nginx sur srv-glpi est actif ; ss -tlnp montre Nginx sur 0.0.0.0:443 ; curl -kI https://localhost sur le serveur répond 302 ; depuis un poste, host glpi.ad.sorbier.example répond srv-glpi.ad.sorbier.example is an alias … has address 192.168.10.184 ; depuis ce poste, nc -zv srv-glpi.ad.sorbier.example 443 échoue par dépassement de délai ; pendant ce test, sudo tcpdump -ni ens18 port 443 sur le serveur ne montre aucun paquet. Où est la panne, et que vérifies-tu ensuite ?
Voir la réponseMasquer la réponse de l'exercice 2
Les étages 1 à 3 (service, écoute, réponse locale) et 6 (DNS) sont bons. Le paquet du poste n'arrive pas jusqu'à l'interface du serveur : ce n'est ni Nginx, ni le pare-feu local (qui, lui, verrait passer le paquet dans tcpdump avant de le jeter : tcpdump capture avant nftables). La panne est sur le chemin réseau entre le VLAN 10 et le VLAN 20 : la règle du pare-feu pfSense qui autorise les postes vers srv-glpi en TCP 443 (modifiée, désactivée, ou jamais créée pour la nouvelle adresse), ou le routage entre les VLAN.
Suite : consulter les journaux de pfSense (refus de 192.168.10.x vers 192.168.10.184:443), vérifier la règle sur l'interface du VLAN des postes, et l'historique des changements de configuration du pare-feu depuis vendredi. Un délai dépassé (et non un refus immédiat) est d'ailleurs typique d'un pare-feu qui jette les paquets sans répondre.
Tu avais trouvé ?
Exercice 3 sur 3 L'administrateur bloqué Réussi À revoir
Après avoir activé fail2ban sur srv-glpi sans ligne ignoreip, Moussa s'est trompé cinq fois du mot de passe de son compte d'administration du domaine en se connectant en SSH depuis le poste d'administration. Il ne peut plus se connecter, et ses collègues non plus. Explique, débloque l'accès, et empêche que cela recommence.
Voir la réponseMasquer la réponse de l'exercice 3
fail2ban a compté cinq échecs depuis l'adresse 192.168.10.209 en moins de 10 minutes et l'a bloquée pour une heure dans nftables. Toute l'administration passe par ce poste : plus personne ne peut administrer srv-glpi en SSH.
Débloquer, depuis la console de l'hyperviseur : sudo fail2ban-client set sshd unbanip 192.168.10.209.
Prévenir : ajouter ignoreip = 127.0.0.1/8 ::1 192.168.10.208/28 dans un fichier de /etc/fail2ban/jail.d/, puis sudo systemctl restart fail2ban. Le réseau d'administration est déjà protégé autrement (accès limité, comptes nominatifs, bastion) ; le bloquer n'apporte rien, et fait perdre la main sur les serveurs au pire moment.
Tu avais trouvé ?