Aller au contenu principal

Hardening

Ce document centralise toutes les mesures de durcissement appliquees, par couche, avec commandes reproductibles.

Pour la doctrine générale (modèle de menace, politique credentials), voir politique.md.

Pour la roadmap des actions restantes, voir roadmap.md.


Scores Lynis​

Relevé le 2026-09-24 dans les rapports du dernier audit (2026-09-20). Les valeurs précédentes sur cette page dataient du 2026-04-13 : cinq mois d'écart, et six à huit points de retard — dans le bon sens.

MachineScoreAudit duAu 13/04Notes
penny82/1002026-09-2077Debian 12 / DietPi / RPi4
lancelot77/1002026-09-2070Debian 13 / Proxmox 9
galahad76/1002026-09-2068Debian 13 / Proxmox 9

Pour les redériver plutôt que les croire :

grep -a hardening_index /var/lib/lynis/lynis-report.dat # penny
grep -a hardening_index /mnt/ssd/log-homelab/lynis-report-{galahad,lancelot}.dat

Deux timers systemd, pas du cron : lynis-notify (penny, dimanche 07:15) et lynis-remote-audit (les deux nœuds PVE en pull depuis penny, dimanche 07:45). Push ntfy avec priorité variable selon le score.

Le témoin de penny n'est pas au même endroit que celui des nœuds

lynis-notify.sh écrit sa source de vérité sur la carte SD (/var/lib/lynis/lynis-report.dat). La copie sur le SSD est qualifiée par hôte (lynis-report-<hôte>.dat) depuis le 2026-08-09, pour que galahad et lancelot cessent d'écraser le rapport de penny.

Ce détail a déjà coûté : guardrail-liveness surveillait l'ancien chemin non qualifié, que plus personne n'écrivait, et a crié « muet depuis 264 h » le 2026-08-20 alors que l'audit tournait très bien — en masquant au passage un vrai écart de score pendant 11 jours.

Suggestions Lynis restantes (commun aux 3 hosts)​

Quick wins non encore appliques (chacun gagne 1-3 points) :

IDActionHosts
DEB-0810apt install apt-listbugstous
DEB-0280apt install libpam-tmpdirgalahad
DEB-0831apt install needrestartgalahad
AUTH-9262apt install libpam-passwdqc (PAM password strength)tous
AUTH-9286PASS_MIN_DAYS 1, PASS_MAX_DAYS 365, PASS_WARN_AGE 14 dans /etc/login.defstous
AUTH-9230ENCRYPT_METHOD YESCRYPT + SHA_CRYPT_MIN_ROUNDS 65536 dans /etc/login.defstous
AUTH-9328UMASK 027 dans /etc/login.defstous
BOOT-5122Mot de passe GRUB (plus invasif, peut bloquer reboot a distance)galahad+lancelot
BOOT-5264systemd-analyze security par service, hardening systemd-unitsgalahad+lancelot
KRNL-5788symlink /vmlinuz (cosmetique)galahad+lancelot

penny (RPi 4 / DietPi)​

Surface d'attaque réduite​

MesureImplémentation
WiFi désactivédtoverlay=disable-wifi dans config.txt
Bluetooth désactivéStack BT non installee
HDMI désactivéhdmi_ignore_hotplug=1, max_framebuffers=0
Audio désactivédtparam=audio=off
GPU minimal16 Mo (au lieu des 64 par defaut)

SSH (/etc/ssh/sshd_config)​

Port 2806
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
# Post-Lynis:
AllowTcpForwarding no
ClientAliveCountMax 2
Compression no
LogLevel VERBOSE
MaxSessions 2
TCPKeepAlive no
UseDNS no
X11Forwarding no
dietpi.conf override

Sur penny (DietPi), PermitRootLogin no et PasswordAuthentication no doivent être aussi dans /etc/ssh/sshd_config.d/dietpi.conf car DietPi override les defaults au reboot.

Firewall iptables​

Policy INPUT DROP. Ports ouverts uniquement :

PortServiceScope
53 (TCP/UDP)AdGuard DNSTous
80, 443Traefik HTTPSTous
853DNS-over-TLSTous
2806SSHTous (clé requise)
3000AdGuard web UILAN + Tailscale
2049 (TCP)NFS (PBS datastore)LAN + Tailscale
111 (TCP/UDP)rpcbind / portmapper (NFS)LAN + Tailscale
5403 (TCP)corosync-qnetd (qdevice)LAN + Tailscale
3101 (TCP)loki-replica (HA Loki)LAN (via Docker bridge, interne)

Le port 45876 (agent Beszel) a quitté cette liste avec le retrait de Beszel le 2026-11-03. Sur galahad et lancelot, beszel-agent a pourtant continué de tourner et d'écouter sur 45876 jusqu'au 2026-11-04, date de sa désinstallation (unité, binaire et utilisateur beszel). Retirer un service, c'est aussi retirer ses agents sur les autres hôtes.

Persistance via netfilter-persistent.


Systemd drop-ins : PrivateMounts=yes (anti-leak namespace)​

Contexte : tout service systemd avec ProtectSystem=strict|full ou ProtectKernelModules=yes et sans PrivateMounts=yes leak ses bind-mounts RO (namespace shared par defaut) vers le mount tree host global. Résultat : /etc, /usr, /boot deviennent RO pour toutes les sessions — apt install casse, pct set casse, etc.

Fix systematique : drop-in PrivateMounts=yes sur chaque service hardene.

Services concernes (identifies 2026-04-19)​

HostServices avec drop-in PrivateMounts=yes
pennyssh, fail2ban, systemd-networkd, systemd-timesyncd, auditd, fstrim, systemd-hostnamed, systemd-localed, systemd-logind, systemd-timedated (10 services)
galahadssh, fail2ban, postfix, chrony, beszel-agent (désinstallé le 2026-11-04), systemd-logind (6 services au relevé)
lancelotidem galahad

Pattern drop-in​

mkdir -p /etc/systemd/system/<svc>.service.d
cat > /etc/systemd/system/<svc>.service.d/private-mounts.conf <<EOF
[Service]
PrivateMounts=yes
EOF
systemctl daemon-reload
systemctl restart <svc>

Scan détection​

Pour trouver un service leaky sur un host :

for svc in $(systemctl list-units --type=service --state=active --no-legend | awk '{print $1}'); do
ps=$(systemctl show "$svc" -p ProtectSystem --value)
pm=$(systemctl show "$svc" -p PrivateMounts --value)
if [ "$ps" = "strict" ] || [ "$ps" = "full" ]; then
[ "$pm" != "yes" ] && echo "LEAK: $svc (PS=$ps PM=$pm)"
fi
done

Vérification mount state​

findmnt /etc /usr /boot 2>&1
# Doit ne rien renvoyer (aucun bind-mount RO separe)
touch /etc/.testwrite && rm /etc/.testwrite && echo OK

Voir operations/depannage.md section "/etc, /usr, /boot read-only" pour remediation si re-apparaît.

sysctl hardening (/etc/sysctl.d/99-hardening.conf)​

# Network
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1

# TCP
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2

# ICMP
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1

login.defs + umask​

UMASK 027
SHA_CRYPT_MIN_ROUNDS 500000
SHA_CRYPT_MAX_ROUNDS 500000

Core dumps desactives (/etc/security/limits.d/disable-core.conf)​

* hard core 0
* soft core 0

Modules kernel bloques (/etc/modprobe.d/)​

blacklist firewire-core
blacklist firewire-ohci
blacklist dccp
blacklist sctp
blacklist rds
blacklist tipc

Watchdog hardware​

Voir os.md pour la configuration complète. Timeout 15s, module bcm2835_wdt.

Comptes​

  • root : mot de passe fort (vault), SSH disabled
  • gabins : uid 1002, sudo NOPASSWD, clé SSH
  • sguardian, smanager (legacy DietPi) : nologin + locked

fail2ban​

Jail sshd sur port 2806, 3 tentatives, ban 1h. Ignore LAN + Tailscale (127.0.0.1/8, 192.168.1.0/24, 100.64.0.0/10). Backend systemd.

auditd​

Rules dans /etc/audit/rules.d/homelab.rules :

  • Auth events (/etc/passwd, /etc/shadow, /etc/sudoers)
  • SSH config changes
  • Root exec calls
  • Kernel modules (insmod/rmmod)
  • Crontabs
  • Firewall + sysctl
  • Docker config + socket

unattended-upgrades​

Actif, reboot auto a 4h si nécessaire (après backups 3h). Upgrades sécurité Debian uniquement.

Docker daemon​

Voir os.md pour la configuration complète. Points sécurité : icc: false (inter-container OFF), no-new-privileges: true.


galahad + lancelot (Proxmox 9 / Trixie)​

SSH​

Idem penny mais :

  • Port 2807 (galahad) / 2808 (lancelot)
  • PermitRootLogin prohibit-password (Proxmox nécessité root pour opérations cluster : pvecm, corosync)
  • PasswordAuthentication no

Firewall Proxmox cluster (/etc/pve/firewall/cluster.fw)​

[OPTIONS]
enable: 1
policy_in: DROP
policy_out: ACCEPT

[RULES]
IN ACCEPT -p icmp -log nolog
IN ACCEPT -p tcp -dport 8006 -source 192.168.1.0/24 -log nolog
IN ACCEPT -p tcp -dport 8006 -source 100.64.0.0/10 -log nolog
IN ACCEPT -p tcp -dport 2807 -log nolog
IN ACCEPT -p tcp -dport 2808 -log nolog
IN ACCEPT -source 192.168.1.0/24 -p tcp -dport 3128 -log nolog
IN ACCEPT -i tailscale0 -log nolog

Services actifs​

Servicegalahadlancelot
fail2banActif (SSH + Proxmox jails)Actif (SSH + Proxmox jails)
unattended-upgradesActifActif
auditdActifActif (reactive 2026-04-13 — retrait des watch Docker absents host)
lynis weeklyTimer lynis-remote-audit, dimanche 07:45Timer lynis-remote-audit, dimanche 07:45
rpcbindDisabled + maskedDisabled + masked

LXC (100, 101, 102, 103)​

SSH​

Tous les LXC ont le même hardening SSH :

PermitRootLogin no
PasswordAuthentication no

Acces root dans les LXC : pct enter <VMID> depuis l'hyperviseur (pas besoin de SSH root). SSH direct via gabins + clé Ed25519 si nécessaire pour les scripts (monitoring, backup).


Réseaux Docker — isolation et ICC​

Relevé le 2026-11-04 avec docker network inspect.

RéseauTypeICCContainersNote
proxybridgeenabledtraefik, authelia, homepage, forgejo, outline, ntfy, dozzle, cyberchef, stirling-pdf, status, crowdsec, loki-replica, loki-replica-gateICC enabled by design pour Traefik -> backends. Implication : tout container compromis sur ce réseau peut joindre les autres en HTTP interne (ex: traefik:8080 admin API).
socketbridge internal: trueenabledsocket-proxy, traefik, homepage, dozzle, autohealPas d'acces internet. ICC limité a la API socket-proxy whitelistee.
hosthostN/AadguardStack réseau de l'host (Tailscale tourne nativement, hors Docker)

Mitigations en place​

VecteurMitigation
Acces externe (Internet/LAN) sur services backendTous via Traefik HTTPS + Authelia (ForwardAuth ou OIDC)
Acces interne ancien WUDRésolu — WUD remplacé par Watchtower (headless, pas d'API/UI).
Container compromis -> Docker APIsocket-proxy whitelistee (CONTAINERS/NETWORKS/EVENTS/IMAGES, pas EXEC/SECRETS/VOLUMES)
Container compromis -> Authelia internalsSessions chiffrées + storage encryption_key

A faire (P2)​

  • ICC=false sur réseau proxy : casserait Traefik routing -> nécessité split en sous-réseaux (1 par backend, partagé avec Traefik).

Docker (tous containers)​

Global (daemon.json)​

  • no-new-privileges: true
  • icc: false (inter-container communication OFF sur bridge par defaut)

Socket proxy​

lscr.io/linuxserver/socket-proxy sur réseau socket (internal, pas d'internet). Whitelist :

  • CONTAINERS, NETWORKS, SERVICES, TASKS, EVENTS, IMAGES, INFO, VERSION, PING
  • POST: 1 (pour autoheal)
  • Bloque : EXEC, SECRETS, BUILD, VOLUMES, CONFIGS, SWARM, NODES, AUTH, SYSTEM

Clients socket-proxy : Traefik, Homepage, Dozzle, autoheal.

Clients socket direct : aucun depuis le retrait de Portainer le 2026-11-03 (seul socket-proxy monte le socket).

Per-container (cap_drop + cap_add)​

Containercap_dropcap_add
Vaultwarden (LXC 102)ALLNET_BIND_SERVICE, CHOWN, DAC_OVERRIDE, SETGID, SETUID
AutheliaALLSETGID, SETUID, DAC_OVERRIDE
TraefikALLNET_BIND_SERVICE
HomepageALL(aucune, read-only)
socket-proxyALL (par design)(géré par le proxy)
AdGuardnon applicableDHCP + host network nécessité plus

read_only (rootfs immuable)​

Containerread_onlyTmpfsNote
Traefik✅ OK/tmp, /runteste 2026-04-13
Homepage✅ OK/tmp, /app/.next/cacheteste 2026-04-13
Authelia❌ KO—ecrit /app/.healthcheck.env au startup (pas seulement healthcheck), impossible en l'état
Vaultwarden (LXC 102)❌ KO—SQLite DB + icons cache (design)

Ports directs supprimés​

Tous les services passent par Traefik HTTPS (443) + Authelia ForwardAuth. Ports directs supprimés :

  • Homepage : 3100
  • Traefik dashboard : 8080 (accessible uniquement via réseau socket pour healthcheck)

Healthchecks​

Tous les services accessibles ont un healthcheck (wget/curl). Autoheal restart les containers unhealthy après 3 échecs consecutifs.


Voir aussi​

  • Authelia (SSO) — configuration SSO, clients OIDC, secrets rotation
  • Traefik — middlewares, TLS, reverse proxy
  • Backups — architecture, retention, restauration
  • Tailscale ACLs — VPN mesh, key expiry, ACL policy

Vérification rapide​

Check complet en une commande :

# Sur chaque host
echo "=== $(hostname) ==="
echo "SSH: $(systemctl is-active sshd)"
echo "fail2ban: $(systemctl is-active fail2ban 2>/dev/null)"
echo "auditd: $(systemctl is-active auditd 2>/dev/null)"
echo "Lynis score: $(grep -oP '^hardening_index=\K[0-9]+' /var/log/lynis-report.dat 2>/dev/null)"
echo "Firewall: $(iptables -L INPUT 2>/dev/null | grep -oP 'policy \K\w+' || pve-firewall status 2>&1 | grep -oP 'Status: \K\S+')"