← Tous les projets

Étude de cas — Durcissement d’un homelab auto-hébergé

Deux jours de chantier, de l’audit à la production : IPS collaboratif, pare-feu, passerelle réseau, DNS chiffré et accès d’administration.

CrowdSecnftablesFirewallWireGuardTailscaleNextDNS (DoH)dnsmasqPostfixsystemdBashFail2Ban

Ce que le projet démontre

IPS collaboratif

CrowdSec analyse les journaux Web, SSH et mail ; environ 20 000 adresses malveillantes bloquées au pare-feu via les listes communautaires.

Installation maîtrisée

Paquet dépaqueté sans démarrage, configuration corrigée, puis premier lancement et test sur une adresse de documentation.

Pare-feu résilient

Ordre systemd, relance après rechargement et watchdog : les règles de blocage survivent au « flush ruleset ».

Accès sans risque

Restriction SSH appliquée avec un retour arrière automatique à 3 minutes, annulé seulement après validation.

Diagnostic par preuve

Trace nftables d’un paquet table par table pour isoler la règle fautive au lieu de deviner.

DNS sans fuite

Un seul DNS distribué, résolution chiffrée DoH dans l’ordre, fin des requêtes envoyées à dix fournisseurs.

Description

Cette étude de cas retrace le durcissement d’un homelab de deux machines Linux : une passerelle et serveur d’applications (mini-PC x86, Debian 12) et un point d’accès Wi-Fi (carte ARM, Debian 13). La passerelle publie des services Web et mail et sort sur Internet par un tunnel WireGuard vers un fournisseur d’accès associatif, qui lui attribue une IP fixe.

L’objectif était d’ajouter une détection d’intrusion collaborative avec blocage réel au pare-feu, sans jamais perdre l’accès d’administration ni casser les services en production. Chaque étape a suivi la même discipline : audit en lecture seule, sauvegarde, correctif ciblé, validation, puis retour arrière automatique pour les changements à risque.

Le chantier a révélé plusieurs problèmes qu’un simple « apt install » aurait laissés passer : un bouncer HTTP incompatible avec le portail d’authentification Lua du serveur Web, un « flush ruleset » qui effaçait les règles de blocage à chaque rechargement du pare-feu, un générateur de règles dont le mode simulation supprimait les tables actives, un DNS qui interrogeait dix résolveurs à chaque requête, des appareils du réseau local qui contournaient la passerelle, et une protection anti-usurpation de wg-quick qui empêchait le LAN de joindre l’IP publique.

Chaque problème a été diagnostiqué avec des preuves (captures tcpdump, traces nftables, journaux) avant d’être corrigé de façon persistante. Les nouveaux scénarios de détection ont d’abord été observés en mode simulation pour écarter les faux positifs, et les alertes ont été regroupées pour rester exploitables.

Note de transparence : les adresses IP (plages de documentation RFC 5737), noms d’hôtes, ports, matériel et fournisseurs présentés ici sont modifiés pour la publication. La démarche, les problèmes rencontrés et les configurations sont ceux d’un déploiement réel.

Fonctionnalités & points techniques

  • ▹Audit en lecture seule avant chaque modification, rapport après chaque étape.
  • ▹Choix argumenté du bouncer pare-feu plutôt que du bouncer HTTP (conflit avec le portail Lua).
  • ▹Bouncer nftables limité au trafic entrant, tables dédiées, aucune règle existante modifiée.
  • ▹Résistance au « flush ruleset » : dépendance systemd, relance et watchdog testé.
  • ▹Scénarios HTTP observés 48 h en simulation, sans faux positif, avant activation.
  • ▹Listes d’exceptions pour ne jamais bloquer les accès d’administration.
  • ▹Alertes mail regroupées : une ligne par adresse, pays et opérateur.
  • ▹Passerelle du LAN via WireGuard : NAT, MSS clamping, repli si le tunnel tombe.
  • ▹Correction d’un générateur de règles dont le mode simulation supprimait les tables actives.
  • ▹DHCP et DNS centralisés, résolution DNS-over-HTTPS, un seul DNS distribué.
  • ▹Accès SSH restreint sur le lien WAN, administration de secours par Tailscale.
  • ▹Relais mail unique : expéditeur unique et envoi authentifié en TLS obligatoire.
  • ▹Document de référence tenu à jour : topologie, sauvegardes, retours arrière, points ouverts.

Extraits de configuration (anonymisés)

Extraits représentatifs du chantier. Adresses en plages de documentation (RFC 5737), noms et ports modifiés.

Bouncer CrowdSec nftables

Tables dédiées, priorité -10, hook input uniquement.

YAML
homelab-securise.txt · lignes 6–24
# --- [1] Bouncer CrowdSec nftables : hook input uniquement --------------------# /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml (extrait)mode: nftablesupdate_frequency: 10snftables:  ipv4:    enabled: true    set-only: false    table: crowdsec    chain: crowdsec-chain    priority: -10  ipv6:    enabled: true    set-only: false    table: crowdsec6    chain: crowdsec6-chain    priority: -10nftables_hooks:  - input        # forward volontairement absent : le trafic routé LAN / VPN n'est pas filtré ici

Installation contrôlée et test

Configuration corrigée avant le premier démarrage, puis test sur une adresse de documentation.

BASH
homelab-securise.txt · lignes 26–37
# --- [2] Installation contrôlée : corriger la configuration AVANT le démarrage -dpkg --unpack crowdsec-firewall-bouncer-nftables_*.deb      # conffiles déposés en .dpkg-newCFG=/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yamlsed -i '/^nftables_hooks:/,/^[^ ]/{/^  - forward$/d}' "$CFG.dpkg-new"dpkg --configure crowdsec-firewall-bouncer-nftables         # clé API + premier démarragegrep -q '^  - forward$' "$CFG" && { systemctl stop crowdsec-firewall-bouncer; exit 1; }# Test fonctionnel avec une adresse de documentationcscli decisions add --ip 203.0.113.123 --duration 5m --reason validationsleep 15nft list table ip crowdsec | grep -q 203.0.113.123 && echo "OK : adresse bloquée au pare-feu"cscli decisions delete --ip 203.0.113.123

Résister au « flush ruleset »

Ordre systemd, relance après rechargement de nftables et watchdog.

INI
homelab-securise.txt · lignes 39–54
# --- [3] Résister au "flush ruleset" de nftables.conf -------------------------# /etc/systemd/system/crowdsec-firewall-bouncer.service.d/10-after-nftables.conf[Unit]After=nftables.service# /etc/systemd/system/nftables.service.d/zz-crowdsec-bouncer.conf[Service]ExecStartPost=-/bin/systemctl --no-block try-restart crowdsec-firewall-bouncer.serviceExecReload=-/bin/systemctl --no-block try-restart crowdsec-firewall-bouncer.service# /etc/systemd/system/crowdsec-nft-watchdog.service (timer toutes les 5 min)[Service]Type=oneshotExecStart=/bin/bash -c 'systemctl is-active --quiet crowdsec-firewall-bouncer || exit 0; \  nft list table ip crowdsec >/dev/null 2>&1 || { logger -t crowdsec-nft-watchdog "table absente"; \  systemctl restart crowdsec-firewall-bouncer; }'

Restriction SSH avec retour arrière

Rechargement atomique d’une seule table et rollback automatique à 3 minutes.

BASH
homelab-securise.txt · lignes 56–76
# --- [4] Restriction SSH sur le lien WAN, avec retour arrière automatique -------F=/etc/nftables.d/40-ssh-bastion.confBK=/root/40-ssh-bastion.conf.before-$(date +%Y%m%d-%H%M%S)cp -a "$F" "$BK"cat > "$F" <<'NFT'table inet ssh-bastion {    chain input {        type filter hook input priority filter -20; policy accept;        ip saddr 10.40.0.0/24 tcp dport 2222 accept comment "LAN"        iifname "wg0" tcp dport 2222 accept comment "VPN d'administration"        iifname "tailscale0" ip saddr 100.64.0.10 tcp dport 2222 accept comment "poste d'admin"        iifname "wg-wan" ip saddr { 203.0.113.50 } tcp dport 2222 accept comment "IP d'admin publique"        tcp dport 2222 drop comment "tout le reste"    }}NFTnft -c -f "$F" || { cp -a "$BK" "$F"; exit 1; }systemd-run --unit=ssh-bastion-rollback --on-active=180 /bin/bash -c \  "cp -a '$BK' '$F' && { echo 'flush table inet ssh-bastion'; cat '$F'; } | nft -f -"{ echo 'flush table inet ssh-bastion'; cat "$F"; } | nft -f -   # rechargement atomique d'une seule table# Nouvelle session validée → systemctl stop ssh-bastion-rollback.timer

Passerelle du LAN

NAT vers le tunnel, MSS clamping et repli.

NFT
homelab-securise.txt · lignes 78–94
# --- [5] Passerelle du LAN via le FAI associatif (NAT + MSS clamping) ----------table ip nat_wan {  chain postrouting {    type nat hook postrouting priority srcnat; policy accept;    ip saddr 10.40.0.0/24  oifname "wg-wan" masquerade comment "LAN → FAI associatif"    ip saddr 10.40.10.0/24 oifname "wg-wan" masquerade comment "clients VPN → Internet"    ip saddr 10.40.0.0/24  oifname "eth0"   masquerade comment "repli si le tunnel tombe"  }}table inet fwd_wan {  chain forward {    type filter hook forward priority filter; policy accept;    oifname "wg-wan" tcp flags syn tcp option maxseg size set rt mtu comment "MSS clamping (MTU 1420)"    iifname "eth0" oifname "wg-wan" accept    iifname "wg-wan" oifname "eth0" ct state established,related accept  }}

Trace nftables d’un paquet

Table temporaire avec nftrace pour suivre un paquet et identifier la règle qui le jette.

BASH
homelab-securise.txt · lignes 96–103
# --- [6] Diagnostic : tracer un paquet table par table ------------------------nft add table inet zz_tracenft add chain inet zz_trace pre '{ type filter hook prerouting priority -350; }'nft add rule inet zz_trace pre ip saddr 10.40.0.50 ip daddr 198.51.100.25 tcp dport 443 meta nftrace set 1timeout 15 nft monitor trace#   ip wg-quick-wg-wan preraw rule iifname != "wg-wan" ip daddr 198.51.100.25#   fib saddr type != local drop (verdict drop)        ← anti-usurpation de wg-quicknft delete table inet zz_trace

Correctif persistant WireGuard

Exceptions ajoutées par PostUp avant la règle anti-usurpation de wg-quick, pour le LAN et les clients VPN.

INI
homelab-securise.txt · lignes 105–107
# Correctif persistant — /etc/wireguard/wg-wan.conf, section [Interface]PostUp = nft insert rule ip wg-quick-%i preraw iifname "wg0" ip saddr 10.40.10.0/24 ip daddr 198.51.100.25 acceptPostUp = nft insert rule ip wg-quick-%i preraw iifname "eth0" ip saddr 10.40.0.0/24 ip daddr 198.51.100.25 accept

DNS sans fuite

Amont DoH dans l’ordre et DNS distribué unique.

INI
homelab-securise.txt · lignes 109–116
# --- [7] DNS : un amont chiffré, dans l'ordre, sans fuite ----------------------# /etc/dnsmasq.d/lan.conf (extrait)strict-order                    # remplace "all-servers" : chaque requête partait vers 10 résolveursno-resolvserver=127.0.0.1#5555           # client DNS-over-HTTPS local (profil filtré)server=45.90.28.0               # secours si le client DoH est arrêtédhcp-option=option:dns-server,10.40.0.2   # un seul DNS distribué : plus de contournement# Les requêtes DNSBL du serveur mail conservent des résolveurs dédiés.

Alertes regroupées

Modèle Go : une ligne par adresse avec pays, opérateur et scénarios.

YAML
homelab-securise.txt · lignes 118–137
# --- [8] Alertes regroupées : une ligne par adresse ---------------------------# /etc/crowdsec/notifications/email.yaml (extrait) — ~300 mails/jour → 1 toutes les 30 min maxgroup_wait: 30mgroup_threshold: 50format: |  {{- $scen := dict -}}{{- $info := dict -}}  {{- range . -}}    {{- $ip := .Source.Value -}}    {{- $s := trimPrefix "crowdsecurity/" .Scenario -}}    {{- if hasKey $scen $ip -}}      {{- $_ := set $scen $ip (printf "%s, %s" (get $scen $ip) $s) -}}    {{- else -}}      {{- $_ := set $scen $ip $s -}}      {{- $_ := set $info $ip (printf "%s · AS%s" (default "?" .Source.Cn) (default "?" .Source.AsNumber)) -}}    {{- end -}}  {{- end -}}  {{ len $scen }} IP bloquée(s) — {{ len . }} alerte(s) regroupée(s)  {{- range $ip, $s := $scen }}  {{ $ip }}  {{ get $info $ip }}  {{ $s }}  {{- end }}

Architecture technique

Détection
CrowdSec
Journaux Nginx, SSH et mail ; scénarios, signaux et listes communautaires.
Réponse
Bouncer nftables
Tables dédiées, hook input, environ 20 000 adresses bloquées.
Pare-feu
nftables
Tables indépendantes : géofiltrage, bastion SSH, NAT, services.
Accès Internet
WireGuard
Tunnel vers un FAI associatif, IP fixe, passerelle du LAN.
DNS / DHCP
dnsmasq + DoH
Résolution locale, amont chiffré, un seul DNS distribué.
Administration
Tailscale + SSH
Accès privé, clés uniquement, retour arrière automatique.
Supervision
Postfix + journald
Alertes regroupées, traces nftables, captures ciblées.

Technologies utilisées

CrowdSecnftablesFirewallWireGuardTailscaleNextDNS (DoH)dnsmasqPostfixsystemdBashFail2Ban