Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 :

bash
sudo ss -tlnup
text
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 :

SourcePortPourquoi
Réseau d'administration 192.168.10.208/28TCP 22Administration en SSH
Postes 192.168.10.64/26 et administration 192.168.10.208/28TCP 80, 443GLPI et ses agents d'inventaire (le port 80 ne fait que rediriger vers HTTPS)
srv-journaux 192.168.10.180TCP 3306Grafana lit la base (chapitre 5)
ToutICMP écho, ICMPv6 de voisinageDiagnostic (ping) et fonctionnement d'IPv6
Le reste—Refusé et journalisé

/etc/nftables.conf :

text
#!/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émentRôle
table inet filtreUne table pour IPv4 et IPv6 à la fois (famille inet)
set adminUn ensemble nommé d'adresses, réutilisable dans plusieurs règles ; flags interval permet d'y mettre des réseaux
hook input … policy dropLa chaîne qui voit le trafic destiné au serveur ; tout ce qui n'est pas accepté est jeté
ct state established,related acceptLes réponses aux connexions déjà acceptées, et celles que le serveur a ouvertes, passent
ct state invalid dropLes paquets incohérents sont jetés
iif "lo" acceptLes é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 dropLe serveur n'est pas un routeur : il ne transmet rien
hook output … policy acceptLe 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 :

bash
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 :

ServeurRègles d'entrée en plus de SSH depuis @admin
srv-web1, srv-web2ip saddr { 192.168.10.228, 192.168.10.229 } tcp dport 80 accept
lb1, lb2tcp 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-partagesSMB (TCP 445) depuis les réseaux qui utilisent le partage, NFS (TCP 2049) depuis srv-glpi (exercice ci-dessous)
srv-journauxip 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é.

bash
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 :

ini
# /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ètreRôle
findtime, maxretryAu-delà de 5 échecs en 10 minutes depuis une même adresse…
bantime… l'adresse est bloquée une heure
[sshd] et journalmatchLe 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
ignoreipDes 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)
bash
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.

ServiceRéglageVérification
SystèmeMises à jour de sécurité automatiquessystemctl list-timers 'apt-daily*', /var/log/unattended-upgrades/
SystèmeHeure synchronisée sur l'annuairechronyc tracking
SSHPas de root direct, clés pour les comptes locaux, accès depuis ADMIN seulementsudo sshd -T | grep -E 'permitrootlogin|passwordauthentication', règles nftables
NginxVersion masquée, site par défaut qui ne sert rien, fichiers cachés refuséscurl -I, curl -I http://<adresse IP>
NginxHTTPS seul, HSTS, TLS 1.2 et 1.3curl -sI http://… (301), openssl s_client
PHPexpose_php = Off, display_errors = Off, un pool et un compte par applicationcurl -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 applicationss -tlnp, SHOW GRANTS
CertificatsRenouvellement automatique, date superviséecertbot renew --dry-run, supervision
SambaSMB 3 minimum, pas d'invité, droits par groupetestparm -s
NFSExport limité à des adresses, root_squashexportfs -v
PostfixPas de relais ouvert : loopback-only ou mynetworks restreintpostconf inet_interfaces mynetworks
JournauxEnvoyés à srv-journaux, agent Wazuh actiflogger puis lecture sur srv-journaux ; état de l'agent
Pare-feu localPolitique drop en entrée, ouvertures documentéesnft 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) :

bash
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.

Du serveur (en haut) vers le client (en bas) : une commande par étage1. Le service tourne-t-il ?systemctl status nginx2. Écoute-t-il sur le bon port ?ss -tlnp3. Répond-il en local ?curl -I http://localhost4. Le pare-feu local le laisse-t-il passer ?nft list ruleset5. Le réseau et pfSense le laissent-ils passer ?nc -zv srv-glpi 4436. Le nom se résout-il ?host glpi7. Le certificat est-il valide ?openssl s_client8. L'application fonctionne-t-elle ?tail *.error.logÉtages 5 à 7, en couleur : à vérifier depuis le poste client. Les autres : sur le serveur.
Du serveur au client : chaque étage se vérifie par une commande, et le premier qui échoue désigne la panne
ÉtageCommandeSi ça échoue
1. Le service tournesystemctl status nginx php8.4-fpm mariadbLire journalctl -u <service> : configuration invalide, port déjà pris, disque plein
2. Il écoutess -tlnp | grep -E ':80|:443'Directive listen ou bind-address, service qui écoute en local seulement
3. Il répond en localcurl -kI https://localhostErreur de l'application ou de la liaison Nginx et PHP-FPM (chapitre 4)
4. Le pare-feu localsudo nft list ruleset, journalctl -k -g nft-refusRègle manquante
5. Le réseauDepuis le client : nc -zv srv-glpi.ad.sorbier.example 443 ; sur le serveur : sudo tcpdump -ni ens18 port 443Routage, VLAN, règle pfSense (le paquet n'arrive pas au serveur)
6. Le DNShost glpi.ad.sorbier.example depuis le clientEnregistrement absent ou faux, mauvais serveur DNS sur le client
7. Le certificatopenssl s_client -connect glpi.ad.sorbier.example:443 -servername glpi.ad.sorbier.exampleNom, date, chaîne, racine inconnue (chapitre 6)
8. L'applicationtail /var/log/nginx/glpi.error.log /var/log/glpi/php-errors.logBase 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
text
#!/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é ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Exercices corrigés de services Linux type E6 et E7 SISRRevoir le sommaire du cours