Je vois ta situation.
Dans ce cas pourquoi ne pas tout interdire par défaut et autoriser uniquement certain AS.
En gros les AS des FAI français et un Belge.
J’ai quelque machines configurées comme ça, tranquillité absolue 
Un script met à jour les CIDR des AS chaque dimanche.
Au cas cela t’intéresse je te passe ma conf nftable à adapter avec IPset du coup.
Nftables écrit par la même personne que Iptables est stable depuis 15 ans et corrige tout les défauts (nombreux) de Iptables.
Il m’a falu un bon moment pour me mettre à la logique un peu différentes, je manipule Iptables depuis tellement longtemps… ça a été dur 
La conf à adapter (je n’ai pas d’Iptables désolé) :
#!/usr/sbin/nft -f
flush ruleset
# ============================================================
# 1. whitelist (priorite -20) - passe avant tout le reste
# ============================================================
table inet whitelist {
set admin_whitelist4 {
type ipv4_addr
flags interval
elements = { 51.38.215.32, 80.13.223.198, 127.0.0.1 }
}
set monitor_whitelist4 {
type ipv4_addr
flags interval
elements = { 45.90.160.153, 94.143.138.32 }
}
set ovh_monitor_whitelist4 {
type ipv4_addr
flags interval
elements = { }
}
set system_whitelist4 {
type ipv4_addr
flags interval
elements = { 1.1.1.1, 8.8.8.8 }
}
set client_whitelist4 {
type ipv4_addr
flags interval
}
chain input {
type filter hook input priority -20; policy accept;
ip saddr @admin_whitelist4 accept
ip saddr @monitor_whitelist4 accept
ip saddr @ovh_monitor_whitelist4 accept
ip saddr @system_whitelist4 accept
ip saddr @client_whitelist4 accept
}
}
# ============================================================
# 3. blacklist-geo (priorite -15) - blocage pays
# ============================================================
table inet blacklist-geo {
set country4-ir {
type ipv4_addr
flags interval
# elements = { ... } # 1740 prefixes (Iran) - `fw update-country`
}
set country6-ir {
type ipv6_addr
flags interval
# elements = { ... } # 570 prefixes
}
set country4-kp {
type ipv4_addr
flags interval
elements = { 5.62.56.160/30, 31.6.16.15,
57.73.214.0/23, 61.149.72.86,
175.45.176.0/22, 185.130.45.98,
194.50.99.122, 194.164.173.174,
196.48.114.0/24, 196.196.114.0/24,
196.197.114.0/24, 196.199.114.0/24,
210.52.109.0/24 }
}
set country6-kp { type ipv6_addr; flags interval; }
chain input {
type filter hook input priority -15; policy accept;
ip saddr @country4-ir drop
ip6 saddr @country6-ir drop
ip saddr @country4-kp drop
ip6 saddr @country6-kp drop
}
}
# ============================================================
# 7. firewall-main (priorite 0, policy drop)
# ============================================================
table inet firewall-main {
set dns4-accept {
type ipv4_addr
flags interval
elements = { 1.1.1.1, 8.8.4.4, 8.8.8.8, 9.9.9.9 }
}
set portscan4-drop {
type ipv4_addr
flags dynamic,timeout
timeout 90d
}
set portscan6-drop {
type ipv6_addr
flags dynamic,timeout
timeout 90d
}
chain input {
type filter hook input priority filter; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
ip saddr @portscan4-drop drop
ip6 saddr @portscan6-drop drop
icmp type echo-request limit rate 10/second burst 5 packets accept
icmpv6 type echo-request limit rate 10/second burst 5 packets accept
tcp dport { 80, 443 } accept
tcp dport 250 accept
udp sport 67 udp dport 68 accept
limit rate 5/minute burst 5 packets log prefix "[nftables] DROP IN: " level info
counter drop
}
chain forward {
type filter hook forward priority filter; policy drop;
counter drop
}
chain output {
type filter hook output priority filter; policy drop;
ct state established,related accept
ip daddr @dns4-accept udp dport 53 accept
ip daddr @dns4-accept tcp dport 53 accept
ip daddr 127.0.0.53 udp dport 53 accept
ip daddr 127.0.0.53 tcp dport 53 accept
udp dport 123 accept
tcp dport { 80, 443 } accept
tcp dport { 25, 465, 587 } accept
tcp dport 250 accept
udp sport 68 udp dport 67 accept
icmp type { echo-reply, echo-request } accept
icmpv6 type { echo-request, echo-reply } accept
udp dport 33434-33534 accept
meta skuid 0 accept
limit rate 5/minute burst 5 packets log prefix "[nftables] DROP OUT: " level info
counter drop
}
}
# ============================================================
# 8. whitelist-isp (priorite -10) - genere par update-as-whitelist.sh
# ============================================================
include "/etc/nftables.d/whitelist-isp.nft"
Le script de mise jour update-as-whitelist.sh :
#!/bin/bash
#===============================================================================
# Script: update-as-whitelist.sh
# Description: Met à jour les sets nftables avec les préfixes des FAI français
# Date: 2025-12-12
#===============================================================================
set -euo pipefail
#-------------------------------------------------------------------------------
# CONFIGURATION
#-------------------------------------------------------------------------------
# Liste des AS des FAI français à autoriser
# Format: "AS_NUMBER:DESCRIPTION"
declare -a AS_LIST=(
"3215:Orange"
"12322:Free"
"51207:Free_Mobile"
"15557:SFR"
"5410:Bouygues"
"21502:SFR_Business"
"6799:Numericable"
"35540:OVH_Telecom"
"5432:Proximus"
"199636:Free_Pro"
"8220:COLT"
"16347:ADISTA"
)
# Fichiers et répertoires
NFT_OUTPUT_FILE="/etc/nftables.d/whitelist-isp.nft"
NFT_OUTPUT_DIR="/etc/nftables.d"
LOG_FILE="/var/log/update-as-whitelist.log"
TMP_DIR="/tmp/as-whitelist-$$"
# Configuration mail
MAIL_TO="xxx@xx.fr"
MAIL_FROM="hosname@xxx.com"
HOSTNAME=$(hostname -f)
# API RIPEstat
RIPESTAT_API="https://stat.ripe.net/data/announced-prefixes/data.json"
API_TIMEOUT=30
# Mode
DRY_RUN=false
VERBOSE=false
#-------------------------------------------------------------------------------
# FONCTIONS
#-------------------------------------------------------------------------------
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
send_error_mail() {
local subject="$1"
local body="$2"
log_error "Envoi mail d'erreur à $MAIL_TO"
echo -e "Subject: [ALERTE] $subject\nFrom: $MAIL_FROM\nTo: $MAIL_TO\n\n$body\n\n-- \nServeur: $HOSTNAME\nDate: $(date)\nLog: $LOG_FILE" | sendmail -t
}
cleanup() {
rm -rf "$TMP_DIR" 2>/dev/null || true
}
trap cleanup EXIT
usage() {
cat << EOF
Usage: $(basename "$0") [OPTIONS]
Met à jour les sets nftables avec les préfixes des FAI français.
Options:
-n, --dry-run Mode simulation (ne modifie rien)
-v, --verbose Mode verbeux
-h, --help Affiche cette aide
Exemple:
$(basename "$0") --dry-run # Teste sans appliquer
$(basename "$0") # Applique les changements
EOF
exit 0
}
# Récupère les préfixes d'un AS depuis RIPEstat
fetch_as_prefixes() {
local as_number="$1"
local ip_version="$2" # 4 ou 6
local output_file="$3"
local url="${RIPESTAT_API}?resource=AS${as_number}"
local response
local prefixes
response=$(curl -s --max-time "$API_TIMEOUT" "$url" 2>/dev/null) || {
log_error "Échec requête API pour AS${as_number}"
return 1
}
# Vérifier que la réponse est valide
if ! echo "$response" | jq -e '.data.prefixes' >/dev/null 2>&1; then
log_error "Réponse API invalide pour AS${as_number}"
return 1
fi
# Extraire les préfixes IPv4 ou IPv6 directement dans le fichier
if [ "$ip_version" = "4" ]; then
echo "$response" | jq -r '.data.prefixes[].prefix' | grep -v ":" > "$output_file" 2>/dev/null || true
else
echo "$response" | jq -r '.data.prefixes[].prefix' | grep ":" > "$output_file" 2>/dev/null || true
fi
# Nettoyer les lignes vides
sed -i '/^$/d' "$output_file" 2>/dev/null || true
local count=$(wc -l < "$output_file" 2>/dev/null || echo "0")
log_info "AS${as_number}: $count préfixes IPv${ip_version} récupérés"
return 0
}
# Agrège les préfixes pour éliminer les chevauchements
aggregate_prefixes() {
local input_file="$1"
local output_file="$2"
local ip_version="$3"
# Nettoyer le fichier d'entrée (supprimer lignes vides)
sed -i '/^$/d' "$input_file" 2>/dev/null || true
if [ ! -s "$input_file" ]; then
: > "$output_file"
return 0
fi
if [ "$ip_version" = "4" ]; then
# IPv4 : utiliser aggregate (paquet Debian)
sort -u "$input_file" | aggregate -q > "$output_file" 2>/dev/null || {
# Fallback: pas d'agrégation si erreur
sort -u "$input_file" > "$output_file"
}
else
# IPv6 : utiliser Python ipaddress.collapse_addresses()
python3 << PYTHON_SCRIPT > "$output_file"
import ipaddress
import sys
prefixes = []
with open("$input_file", "r") as f:
for line in f:
line = line.strip()
if line:
try:
prefixes.append(ipaddress.ip_network(line, strict=False))
except ValueError:
pass
if prefixes:
collapsed = list(ipaddress.collapse_addresses(prefixes))
for net in sorted(collapsed, key=lambda x: (x.network_address, x.prefixlen)):
print(net)
PYTHON_SCRIPT
fi
# Nettoyer le fichier de sortie
sed -i '/^$/d' "$output_file" 2>/dev/null || true
}
# Génère le fichier nftables complet avec table et filtrage
generate_nft_file() {
local output_file="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
cat > "$output_file" << EOF
#!/usr/sbin/nft -f
# Fichier généré automatiquement par update-as-whitelist.sh
# NE PAS MODIFIER MANUELLEMENT
# Dernière mise à jour: $timestamp
#
# Whitelist des FAI français - Filtrage par AS
# Priorité -10 : après whitelist admin (-20), avant firewall-main (0)
#
# Liste des AS autorisés:
EOF
# Ajouter la liste des AS en commentaire
for as_entry in "${AS_LIST[@]}"; do
local as_number="${as_entry%%:*}"
local as_desc="${as_entry##*:}"
echo "# - AS$as_number ($as_desc)" >> "$output_file"
done
echo "" >> "$output_file"
echo "table inet whitelist-isp {" >> "$output_file"
# Générer les sets pour chaque AS
for as_entry in "${AS_LIST[@]}"; do
local as_number="${as_entry%%:*}"
local as_desc="${as_entry##*:}"
local v4_file="${TMP_DIR}/as${as_number}_v4_aggregated.txt"
local v6_file="${TMP_DIR}/as${as_number}_v6_aggregated.txt"
# Set IPv4
echo "" >> "$output_file"
echo " # $as_desc (AS$as_number) - IPv4" >> "$output_file"
echo " set as4_${as_number} {" >> "$output_file"
echo " type ipv4_addr" >> "$output_file"
echo " flags interval" >> "$output_file"
if [ -s "$v4_file" ]; then
echo " elements = {" >> "$output_file"
sed 's/$/,/' "$v4_file" | sed '$ s/,$//' | sed 's/^/ /' >> "$output_file"
echo " }" >> "$output_file"
fi
echo " }" >> "$output_file"
# Set IPv6
echo "" >> "$output_file"
echo " # $as_desc (AS$as_number) - IPv6" >> "$output_file"
echo " set as6_${as_number} {" >> "$output_file"
echo " type ipv6_addr" >> "$output_file"
echo " flags interval" >> "$output_file"
if [ -s "$v6_file" ]; then
echo " elements = {" >> "$output_file"
sed 's/$/,/' "$v6_file" | sed '$ s/,$//' | sed 's/^/ /' >> "$output_file"
echo " }" >> "$output_file"
fi
echo " }" >> "$output_file"
done
# Set pour les IP admin (copie de la whitelist admin)
cat >> "$output_file" << 'ADMIN_SETS'
# IP administrateurs - toujours autorisées (copie de inet whitelist)
set admin_whitelist4 {
type ipv4_addr
flags interval
elements = {
127.0.0.1,
xxx.xx.xx.xx,
xxx.xx.xx.xx
}
}
# IP monitoring - toujours autorisées
set monitor_whitelist4 {
type ipv4_addr
flags interval
elements = {
xx.xx.xxx.xxx, xx.xx.xxx.xxx
}
}
# OVH monitoring
set ovh_monitor_whitelist4 {
type ipv4_addr
flags interval
elements = {
92.222.184.0/24,
92.222.185.0/24,
92.222.186.0/24,
167.114.37.0/24,
139.99.1.144/28,
193.70.125.118/32,
148.113.25.1/32,
139.99.187.247/32,
72.251.7.222/32,
51.195.135.163/32,
51.38.117.56/32,
51.38.25.100/32,
57.130.4.212/32,
213.186.33.13/32
}
}
ADMIN_SETS
# Générer la chaîne input avec le filtrage
cat >> "$output_file" << 'CHAIN_START'
# Chaîne de filtrage - DROP si pas dans un AS français ou whitelist admin
chain input {
type filter hook input priority -10; policy accept;
# Laisser passer le trafic local
iif lo accept
# Laisser passer les connexions établies
ct state established,related accept
# Laisser passer ICMP (ping)
meta l4proto icmp accept
meta l4proto icmpv6 accept
# Laisser passer les IP admin/monitoring
ip saddr @admin_whitelist4 accept
ip saddr @monitor_whitelist4 accept
ip saddr @ovh_monitor_whitelist4 accept
CHAIN_START
# Générer les règles accept pour chaque AS (IPv4)
echo " # === Whitelist FAI français IPv4 ===" >> "$output_file"
for as_entry in "${AS_LIST[@]}"; do
local as_number="${as_entry%%:*}"
local as_desc="${as_entry##*:}"
echo " ip saddr @as4_${as_number} accept # $as_desc" >> "$output_file"
done
echo "" >> "$output_file"
echo " # === Whitelist FAI français IPv6 ===" >> "$output_file"
for as_entry in "${AS_LIST[@]}"; do
local as_number="${as_entry%%:*}"
local as_desc="${as_entry##*:}"
echo " ip6 saddr @as6_${as_number} accept # $as_desc" >> "$output_file"
done
# Fin de la chaîne : drop le reste
cat >> "$output_file" << 'CHAIN_END'
# Tout le reste est droppé (IP non françaises)
# Note: Les IP admin sont passées avant (whitelist priorité -20)
log prefix "[WHITELIST-ISP] DROP: " level info
counter drop
}
}
CHAIN_END
}
#-------------------------------------------------------------------------------
# MAIN
#-------------------------------------------------------------------------------
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--dry-run) DRY_RUN=true; shift ;;
-v|--verbose) VERBOSE=true; shift ;;
-h|--help) usage ;;
*) log_error "Option inconnue: $1"; usage ;;
esac
done
# Démarrage
log_info "=========================================="
log_info "Début de la mise à jour des whitelist AS"
log_info "Mode: $([ "$DRY_RUN" = true ] && echo "DRY-RUN" || echo "PRODUCTION")"
log_info "=========================================="
# Créer le répertoire temporaire
mkdir -p "$TMP_DIR"
mkdir -p "$NFT_OUTPUT_DIR"
# Variable pour tracker les erreurs
HAD_ERRORS=false
ERROR_DETAILS=""
# Récupérer et agréger les préfixes pour chaque AS
for as_entry in "${AS_LIST[@]}"; do
as_number="${as_entry%%:*}"
as_desc="${as_entry##*:}"
log_info "Traitement de $as_desc (AS$as_number)..."
# IPv4
v4_raw="${TMP_DIR}/as${as_number}_v4_raw.txt"
v4_agg="${TMP_DIR}/as${as_number}_v4_aggregated.txt"
if ! fetch_as_prefixes "$as_number" "4" "$v4_raw"; then
HAD_ERRORS=true
ERROR_DETAILS="${ERROR_DETAILS}\n- Échec récupération IPv4 pour AS${as_number} ($as_desc)"
else
aggregate_prefixes "$v4_raw" "$v4_agg" "4"
count_raw=$(wc -l < "$v4_raw" 2>/dev/null | tr -d ' ' || echo "0")
count_agg=$(wc -l < "$v4_agg" 2>/dev/null | tr -d ' ' || echo "0")
log_info " IPv4: $count_raw préfixes -> $count_agg après agrégation"
fi
# IPv6
v6_raw="${TMP_DIR}/as${as_number}_v6_raw.txt"
v6_agg="${TMP_DIR}/as${as_number}_v6_aggregated.txt"
if ! fetch_as_prefixes "$as_number" "6" "$v6_raw"; then
HAD_ERRORS=true
ERROR_DETAILS="${ERROR_DETAILS}\n- Échec récupération IPv6 pour AS${as_number} ($as_desc)"
else
aggregate_prefixes "$v6_raw" "$v6_agg" "6"
count_raw=$(wc -l < "$v6_raw" 2>/dev/null | tr -d ' ' || echo "0")
count_agg=$(wc -l < "$v6_agg" 2>/dev/null | tr -d ' ' || echo "0")
log_info " IPv6: $count_raw préfixes -> $count_agg après agrégation"
fi
done
# En cas d'erreur, ne rien modifier et alerter
if [ "$HAD_ERRORS" = true ]; then
log_error "Des erreurs sont survenues, aucune modification effectuée"
send_error_mail "Échec mise à jour whitelist AS sur $HOSTNAME" \
"Le script de mise à jour des whitelist AS a rencontré des erreurs.\n\nDétails:${ERROR_DETAILS}\n\nAucune modification n'a été appliquée au firewall.\nLes règles actuelles restent en place."
exit 1
fi
# Générer le fichier nftables
NFT_TMP="${TMP_DIR}/whitelist-isp.nft"
log_info "Génération du fichier nftables..."
generate_nft_file "$NFT_TMP"
# Valider la syntaxe
# Note: nft -c valide contre le ruleset existant, donc on doit d'abord
# sauvegarder et supprimer la table existante pour éviter les conflits
log_info "Validation de la syntaxe nftables..."
BACKUP_TABLE="${TMP_DIR}/whitelist-isp-backup.nft"
nft list table inet whitelist-isp > "$BACKUP_TABLE" 2>/dev/null || true
nft delete table inet whitelist-isp 2>/dev/null || true
NFT_RESULT=0
NFT_ERROR=$(nft -c -f "$NFT_TMP" 2>&1) || NFT_RESULT=$?
# Restaurer la table existante après validation
if [ -s "$BACKUP_TABLE" ]; then
nft -f "$BACKUP_TABLE" 2>/dev/null || true
fi
if [ $NFT_RESULT -ne 0 ]; then
log_error "Erreur de syntaxe dans le fichier généré: $NFT_ERROR"
send_error_mail "Erreur syntaxe nftables sur $HOSTNAME" \
"Le fichier nftables généré contient des erreurs de syntaxe.\n\nErreur: $NFT_ERROR\n\nAucune modification n'a été appliquée."
exit 1
fi
log_info "Syntaxe validée avec succès"
# Statistiques
total_v4=$(cat ${TMP_DIR}/*_v4_aggregated.txt 2>/dev/null | grep -c . || echo "0")
total_v6=$(cat ${TMP_DIR}/*_v6_aggregated.txt 2>/dev/null | grep -c . || echo "0")
log_info "Total: $total_v4 préfixes IPv4, $total_v6 préfixes IPv6"
# Appliquer ou afficher
if [ "$DRY_RUN" = true ]; then
log_info "[DRY-RUN] Fichier généré mais non appliqué"
log_info "[DRY-RUN] Contenu:"
echo "---"
cat "$NFT_TMP"
echo "---"
else
# Sauvegarder l'ancien fichier
if [ -f "$NFT_OUTPUT_FILE" ]; then
cp "$NFT_OUTPUT_FILE" "${NFT_OUTPUT_FILE}.backup"
fi
# Copier le nouveau fichier
cp "$NFT_TMP" "$NFT_OUTPUT_FILE"
# Recharger nftables
log_info "Rechargement de nftables..."
# D'abord supprimer la table si elle existe
nft delete table inet whitelist-isp 2>/dev/null || true
# Charger le nouveau fichier
# Charger le nouveau fichier
if nft -f "$NFT_OUTPUT_FILE"; then
log_info "Whitelist AS mise à jour avec succès"
else
log_error "Échec du rechargement nftables"
# Restaurer le backup
if [ -f "${NFT_OUTPUT_FILE}.backup" ]; then
cp "${NFT_OUTPUT_FILE}.backup" "$NFT_OUTPUT_FILE"
nft delete table inet whitelist-isp 2>/dev/null || true
nft -f "$NFT_OUTPUT_FILE" 2>/dev/null || true
fi
send_error_mail "Échec rechargement nftables sur $HOSTNAME" \
"Le rechargement de nftables a échoué.\nLe backup a été restauré."
exit 1
fi
fi
log_info "=========================================="
log_info "Fin de la mise à jour"
log_info "=========================================="
exit 0
Bon je te l’acorde c’est un peu une usine à gaz mais tu auras un serveur injoingnable par tout ce qui n’est pas explicitement authorisé (serveur coffre fort en qq sorte).