Pare-feu nftables#

Pourquoi#

Un serveur NTP public est une cible évidente : le protocole NTP a historiquement été exploité pour des attaques par amplification, et un serveur exposé reçoit du trafic de scan en continu dès sa mise en ligne. Le pare-feu doit filtrer ce bruit avant qu’il n’atteigne les services applicatifs, avec deux objectifs distincts : limiter le débit par IP (anti-abus) et éviter que le suivi de connexion (conntrack) ne sature sous une charge de paquets UDP sans état.

Ce guide ne vise pas l’exhaustivité en matière de sécurité. Les règles ci-dessous couvrent les besoins de ce projet précis, pas un modèle de pare-feu universel. Elles doivent être adaptées à la topologie réseau propre à chaque déploiement, et complétées par un audit (chapitre Lynis) plutôt que considérées comme suffisantes telles quelles.

Comment#

Règles#

#!/usr/sbin/nft -f
flush ruleset

# Réseau d'administration autorisé pour SSH : préfixe de documentation
# (RFC 5737), à remplacer par le LAN privé
define ADMIN_NET_V4 = 192.0.2.0/24

table inet filter {
    counter cnt_drop_ntp       { }
    counter cnt_reject_nts     { }
    counter cnt_state_invalid  { }

    set ntp_meter     { type ipv4_addr ; flags dynamic ; timeout 5s  ; }
    set ntp_meter_v6  { type ipv6_addr ; flags dynamic ; timeout 5s  ; }
    set nts_meter     { type ipv4_addr ; flags dynamic ; timeout 60s ; }
    set nts_meter_v6  { type ipv6_addr ; flags dynamic ; timeout 60s ; }
    # `timeout` fixe la durée de vie d'une entrée dynamique par IP dans
    # le set : elle doit couvrir la fenêtre sur laquelle le débit est
    # évalué, sans quoi l'entrée expirerait avant que la limite ait un
    # sens. 5 s suffit pour un seuil par seconde (NTP), 60 s est
    # nécessaire pour un seuil par minute (NTS-KE).

    chain input {
        type filter hook input priority filter; policy drop;

        iif "lo" accept
        ct state established,related accept
        ct state invalid counter name "cnt_state_invalid" drop

        # NTP (UDP/123) : abusif > 10/s/IP -> drop silencieux + compteur
        # Seuil volontairement large : ce filtre réseau est aveugle au
        # protocole (contrairement au `ratelimit` de chrony, chapitre
        # dédié, plus fin), son rôle est de contenir un abus grossier
        # avant même que le paquet n'atteigne chronyd, pas de remplacer
        # la régulation applicative
        udp dport 123 meta nfproto ipv4 add @ntp_meter    { ip  saddr limit rate over 10/second } counter name "cnt_drop_ntp" drop
        udp dport 123 meta nfproto ipv6 add @ntp_meter_v6 { ip6 saddr limit rate over 10/second } counter name "cnt_drop_ntp" drop
        udp dport 123 accept

        # NTS-KE (TCP/4460) : abusif > 4/min/IP -> reset TCP + compteur
        tcp dport 4460 meta nfproto ipv4 ct state new add @nts_meter    { ip  saddr limit rate over 4/minute } counter name "cnt_reject_nts" reject with tcp reset
        tcp dport 4460 meta nfproto ipv6 ct state new add @nts_meter_v6 { ip6 saddr limit rate over 4/minute } counter name "cnt_reject_nts" reject with tcp reset
        tcp dport 4460 accept

        # SSH restreint au réseau d'administration
        ip saddr $ADMIN_NET_V4 tcp dport 22 ct state new accept

        # Web public
        tcp dport { 80, 443 } accept

        # ICMP / ICMPv6 (nécessaire pour PMTU et IPv6)
        ip  protocol icmp   accept
        ip6 nexthdr  icmpv6 accept
        udp sport 547 udp dport 546 accept
    }

    chain forward { type filter hook forward priority filter; policy drop; }
    chain output  { type filter hook output priority filter; policy accept; }
}

# Contournement du suivi de connexion pour NTP (performance + résilience DDoS)
table inet raw {
    chain prerouting {
        type filter hook prerouting priority raw; policy accept;
        udp dport 123 notrack
    }
    chain output {
        type filter hook output priority raw; policy accept;
        udp sport 123 notrack
    }
}
nft -c -f /etc/nftables.conf   # validation syntaxique avant activation
systemctl enable --now nftables

Le pipeline en un schéma#

         Paquet UDP/123 entrant
                    │
                    ▼
          ┌───────────────────┐
          │  table inet raw   │  udp dport 123 → notrack
          │  hook prerouting  │  ───────────────────────────────► paquet marqué « non suivi »
          └───────────────────┘
                    │
                    ▼
         ┌─────────────────────┐
         │     CONNTRACK       │  ← bypassé : aucune entrée allouée en RAM
         │  (allocation RAM)   │    (défense anti-épuisement mémoire)
         └─────────────────────┘
                    │
                    ▼
         ┌─────────────────────┐
         │  table inet filter  │
         │     chain input     │
         │  rate > 10/s/IP ?   │
         └─────────────────────┘
                    │
          ┌─────────┴─────────┐
         oui                 non
          ▼                   ▼
   drop silencieux      ┌───────────┐
                        │  accept   │  ────────────────────► socket chronyd
                        └───────────┘

Erreurs à éviter#

notrack sur le trafic NTP n’est pas juste une optimisation de performance : c’est une protection anti-DDoS. Sans cette directive, chaque paquet UDP/123 (y compris d’attaquants) alloue une entrée dans la table conntrack du noyau. Cette table a une capacité finie ; un flot suffisant de paquets peut l’épuiser, ce qui affecte alors toutes les connexions du serveur, pas seulement NTP. Le protocole NTP étant intrinsèquement sans état côté serveur (chaque requête est indépendante), le suivi de connexion n’apporte aucun bénéfice ici, seulement un risque.

  • Adresse ADMIN_NET_V4 oubliée ou mal ajustée. La politique par défaut de la chaîne input est drop : sans la règle d’exception SSH correctement configurée sur le réseau d’administration réel, c’est l’accès qui se coupe soi-même. Cette valeur doit être vérifiée avant d’activer les règles, pas après.
  • Oublier la validation syntaxique avant activation. nft -c -f valide le fichier sans l’appliquer, une erreur de syntaxe dans un fichier appliqué directement peut laisser le pare-feu dans un état incohérent, potentiellement bloquant tout accès distant y compris SSH.

Vérifier#

nft list ruleset
# Relit la configuration active, doit correspondre au fichier

# Test depuis une machine tierce (pas depuis le serveur lui-même) :
nc -zv time.example.org 123

Le vrai test de la limite de débit nécessite de générer artificiellement un trafic dépassant 10 requêtes/seconde depuis une même IP, et de vérifier que le compteur cnt_drop_ntp progresse :

watch -n1 'nft list counters table inet filter'

Ce rate-limiting réseau agit en dehors du protocole NTP : il ne distingue pas un client légitime d’un abusif, il coupe au-delà d’un seuil brut. NTP dispose par ailleurs de son propre mécanisme protocolaire de rétorsion, le Kiss-o’-Death (codes RATE/DENY renvoyés dans le champ Reference ID quand Stratum = 0). Les deux mécanismes sont complémentaires : le KoD régule les clients coopératifs, nftables contient ceux qui ne le sont pas. Voir le chapitre Anatomie du paquet NTP.


📚 Pour aller plus loin

Les attaques par amplification NTP exploitent historiquement la commande de monitoring monlist du démon ntpd (retirée depuis longtemps des implémentations modernes, dont chrony) : une requête de quelques octets pouvait générer une réponse des centaines de fois plus volumineuse, dirigée vers une victime usurpée. Le rate-limiting par IP mis en place ici, combiné à l’absence de fonctionnalité monlist dans chrony, ferme ce vecteur d’attaque à la source.