Roadmap sécurité
État au 2026-08-29. Priorité : impact / effort.
Pour la doctrine (threat model, politique credentials) : politique.md. Pour les implémentations (sysctl, firewall, SSH) : hardening.md. Pour le contexte projet global : projet/roadmap.md.
En cours / a faire
P1 — Important (action user-side requise)
DR drill from cold — jamais teste
Tu as :
- 22 daily snapshots sur Cloudflare R2 ✓
- restic check + drill mensuels ✓
- Sops + age + 2 YubiKeys DR ✓
Mais jamais reconstruit penny depuis zero. Le drill mensuel vérifie intégrité données, pas full restore. Scénario reel : SSD meurt → Pi neuf → restore R2 + sops → est-ce que ca reboot dans un état exploitable en X heures ?
Effort : 1/2 journee. Pi 4 spare ou VM x86. Suivre break-glass + dr-drill-scénario-1, chronometrer, noter les surprises.
P2 — Avant demenagement / OPNsense
VLANs + OPNsense
Bloque par achat hardware. Voir architecture/réseau-cible.md.
Chiffrement au repos (ZFS) — SKIP DOCUMENTÉ
Voir decisions.md. Skip pour homelab domicile (modèle de menace ne le justifie pas ; casse boot unattended). A reconsiderer si demenagement avec serveurs en transit ou stockage données client/médical.
Egress firewall (penny + galahad + lancelot) — DONE 2026-05-05
Phase 1 (egress-audit.sh, audit LOG only) : déployée 2026-04-14 sur penny, étendue 2026-04-19 sur galahad+lancelot. Collecte ~3 semaines.
Phase 2 (egress-phase2.sh, OUTPUT DROP + whitelist port-based) : déployée 2026-05-05 sur les 3 hosts.
Whitelist :
- Skip lo + 192.168.1.0/24 + 100.64.0.0/10 (Tailscale) + 172.16.0.0/12 (Docker bridges) + ESTABLISHED/RELATED
- ICMP, DNS (53/853), NTP (123), HTTP (80), HTTPS (443), Tailscale STUN (3478) + P2P (41641)
- PVE nodes (galahad+lancelot) : SMTP (25 + 587) pour postfix outbound mail
Validation 30 min sur chaque host : DROP counter = 6 packets sur 30 min, 100% SSDP UPnP multicast (239.255.255.250:1900, TTL=1, bénin LAN-only).
Rollback safety : apply arme un timer systemd egress-phase2-rollback 5 min ; si confirm n'est pas lance avant, restore auto des iptables pre-Phase 2. Backup dans /tmp/iptables-pre-egress-phase2.rules.
Persistance reboot : iptables-persistent package + iptables-save > /etc/iptables/rules.v4 + netfilter-persistent.service enabled.
Script tracke dans homelab-config/scripts/egress-phase2.sh.
Healthchecks Beszel + Portainer + beszel-agent — NON APPLICABLE
Les 3 images sont scratch/distroless (Go static binary) : aucun outil CLI (wget, curl, sh) n'est disponible dans le container. Pas de subcommand healthcheck non plus. Healthchecks impossibles sans rebuilder les images.
Mitigation : monitoring via homelab_monitor.sh (check Docker containers stopped/unhealthy) + Beszel agent metriques.
PVE firewall logging — DONE 2026-04-19
/etc/pve/firewall/cluster.fw : toutes les rules ont -log warning (déployé 2026-04-19 dans le commit hardening systemd mount leaks). Audit visibility OK sur tentatives bloquees.
YubiKey sur clés SSH client — DONE
[email protected] keys présentés dans /home/gabins/.ssh/authorized_keys sur penny + galahad + lancelot, et /root/.ssh/authorized_keys sur penny. Clés touch-required pour sudo.
Mot de passe GRUB (galahad + lancelot) — DEFERE
Suggestion Lynis BOOT-5122. Defere : risque lock boot remote (si patch /etc/grub.d/10_linux loupe, aucune entry boot sans password) >> gain marginal (attaquant avec acces physique peut déjà booter USB). User-side seulement.
P3 — Nice to have / hardening avance
Process / supply chain
-
RenovateDONE — actif sur homelab-config (auto-PR deps, Dependency Dashboard issue #19). -
CI/CD GitHub ActionsDONE — homelab-config : sucre lint+tests, scripts bash+shellcheck, yaml+compose, secret scan ; gate avant merge (PR workflow). -
AIDE (file integrity monitoring)DONE 2026-07-07 —aide-check.timerquotidien à 04:30 sur penny (~20 min de run), baseline/var/lib/aide/aide.db, notification ntfy uniquement sur écart et dédupliquée par empreinte du constat. Debian 12 n'a pasaide.wrapperet masquedailyaidecheck: le timer est maison. Exclusions/mntet/var/logdansaide.conf.d/99_homelab_exclusions, sinon le run dépasse 3 h.remarqueLe rebaseline passe par security-updates.sh, pas par un hook aptaide-check.shetaide-refresh.shannoncent tous deux un hook/etc/apt/apt.conf.d/99aide-refresh-baseline. Ce hook n'existe pas — et c'est voulu :Unattended-Upgrade::Post-Invoken'est pas implémenté par unattended-upgrades 2.9.1+nmu3 (apt-configle parse, u-u ne le lit jamais), ce qui avait gelé la baseline au 2026-07-07 et fait alerter AIDE chaque jour sur quatre semaines cumulées de paquets.Le déclencheur réel depuis le 2026-08-05 est un appel direct à
aide-refresh.shen fin desecurity-updates.sh, conditionné à un upgrade effectif. Les deux en-têtes de script n'ont simplement jamais été mis à jour et désignent un mécanisme mort : les lire au pied de la lettre fait conclure à tort que le rebaseline est manuel. Seul le rebaseline après une modification de conf faite à la main demande de lanceraide-refresh.shsoi-même. -
Trivy scheduleDONE 2026-06-25 —trivy-scan.sh+ timer hebdo (dim 06:00). 1er run : 27 CRITICAL / 641 HIGH sur 15 images → images tierces à bumper (traefik-crowdsec-bouncer, docker-socket-proxy). Déclencheur revu le 2026-08-31, voir l'encadré.attentionFixedVersionne veut pas dire qu'une image corrigée existeLe scan a longtemps notifié sur tout total CRITICAL non nul, avec pour consigne « bumper l'image +
@sha256». Mesure faite le 30/08, image par image : sur les 25 CRITICAL rapportées, aucune n'était corrigeable par un bump.La confusion vient du champ
FixedVersionde Trivy. Il signifie « un correctif existe dans l'écosystème du paquet » — Alpine a publiétar 7.5.19— et non « une image corrigée est disponible ». Tant que le mainteneur n'a pas reconstruit, l'information est vraie et inutilisable.La preuve, sur la seule image réellement en dérive du lot :
épinglée (homepage v2.0.0) amont (v2.1.2) CRITICAL 1 ( CVE-2026-59873tar)1, la même HIGH 13 13 Une semaine de correctifs applicatifs, zéro CVE en moins — alors que cette CRITICAL porte bien un
FixedVersion. Les quatre autres images concernées étaient déjà au digest amont : rien à ré-épingler.Le déclencheur est donc devenu le gain mesuré. Pour chaque image porteuse de CRITICAL, le script tire l'image amont et la scanne. Rien en baisse → silence, mais silence borné : un point de situation part tous les 30 jours s'il reste des CRITICAL. Le décompte des correctifs de paquet reste affiché comme contexte, avec la nuance écrite dans le message.
Deux corrections structurelles au passage :
- Scanner l'ID d'image déployé, pas le tag. Un
docker pullfait ailleurs (digest-drift-check, geste humain) rebranche le tag, et le scan portait alors sur ce qui n'est pas en service. - Comparer les ID d'image, jamais les digests. Une même image porte
plusieurs
RepoDigests(index OCI vs manifest list Docker) et le test naïf fabrique de faux écarts. Deux ID égaux = même contenu.
Corollaire pratique : avant de recommander un bump pour une CVE, tirer l'image amont et la scanner. Si le compte ne baisse pas, il n'y a pas d'action — le dire, plutôt que de produire un chiffre qui fait croire le contraire.
- Scanner l'ID d'image déployé, pas le tag. Un
Network / détection
- HIDS (Wazuh / CrowdSec extension fonctionne déjà sur Trixie).
- IDS réseau (Suricata / Zeek) — a considerer une fois OPNsense stable.
- Bastion SSH LXC — Tailscale SSH couvre déjà 80%, faible valeur ajoutee.
Hardening cosmetiques
SymlinkDONE 2026-09-02 — et pas comme prévu. L'item était noté « done partiellement : penny », mais penny non plus ne le tenait pas : le lien désignait encore/vmlinuz(Lynis KRNL-5788, cosmetique)/boot/vmlinuz-6.12.75avec un noyau 6.12.96 en service. Un lien posé à la main ne peut pas tenir ici —do_symlinks=0dans/etc/kernel-img.confsur penny (raspi-firmware) et les noyaux Proxmox ne posent pas ces liens. C'est donc un hook/etc/kernel/postinst.d/zz-vmlinuz-symlinkqui les repose à chaque installation de noyau, sur les trois hôtes. Voir le journal du 2026-09-02.SMTP migration port 25 → 587 auth submissionDONE 2026-05-11 (relay Proton submission, voir operations/r2-migration.md section connexes).Renommer Tailscale hostsDONE.pve1/pve2→galahad/lancelot
P4 — Defere / decision documentée
- Mot de passe GRUB (galahad + lancelot) — Lynis BOOT-5122. Defere : risque lock boot remote >> gain (attaquant avec acces physique peut déjà booter USB).
- Chiffrement disque ZFS — SKIP documenté, voir P2 ci-dessus.
Déjà fait
Les blocs ci-dessous enregistrent ce qui a été fait au moment où ça a été fait. Ils ne sont pas maintenus au présent, et trois d'entre eux induiraient en erreur aujourd'hui :
- « Pinning digests sur Vaultwarden, Authelia, Traefik » : ce sont désormais les 22
images qui sont épinglées,
pulseayant été la dernière le 2026-09-02. - « Healthchecks Beszel + Portainer + beszel-agent : non applicable » : les trois en ont un aujourd'hui. Le relevé du jour donne 12 conteneurs sur 22 avec healthcheck, voir optimisations OS.
- Toute mention de Watchtower : le conteneur est retiré depuis le 2026-07-06.
Pour l'état réel, lire hardening et services, pas cette section.
Observabilite / documentation
- Threat model documenté (politique.md)
- Politique rotation / revocation documentée
- Procédure revocation Tailscale (réseau.md)
- Break-glass procédure (break-glass.md)
- DR drill Vaultwarden (RTO 7s)
- Scope token Cloudflare vérifie (1 zone)
Backups
- Proxmox Backup Server (LXC 103 "pbs" sur lancelot, IP 192.168.1.33) — backup natif de tous les LXC (100, 101, 102, 103) via API PBS
- Datastore "main" sur NFS penny (
/mnt/ssd/pbs-datastore, 65536 chunk dirs) - NFS export
all_squash anonuid=100034(UID mappe LXC unprivileged) - Traefik route
backup.home.gabin-simond.fr→ PBS UI - Authelia OIDC realm "authelia" pour login PBS + comptes break-glass locaux
- vzdump-permfix-hook.sh sur galahad + lancelot (workaround pct.conf permissions, TEMPORAIRE — a supprimer après migration ZFS)
- Cloud offsite migré B2 → Cloudflare R2 EU — 2026-05-11. 4 repos restic (
restic,restic-vault,restic-dnsfailover,restic-logs) + PBS datastore via rclone direct. Endpoints3:https://<account-id>.eu.r2.cloudflarestorage.com/homelab-backups. 0 transaction cap (vs B2 daily caps), 0 egress fee. - Vaultwarden restic-direct (LXC 102 → R2:restic-vault, SQLite atomic snapshot, cron 02h, indépendant de PBS)
- Off-site R2 chiffré (restic AES-256) pour penny configs + volumes Docker
- Volumes Docker stages sur
/mnt/ssd/.restic-stagingpuis backup restic - Retention 7d/4w/6m + prune automatique
- restic check mensuel (1er du mois 4h, structure + 10% data subset)
- RPO uniforme 24h sur tout le homelab (Vaultwarden inclus)
Monitor B2 cap probe(check_b2_cap, 2026-05-11) — retirée. R2 n'a pas de cap journalier, donc la sonde n'avait plus d'objet ; nicheck_b2_capniB2_ACCOUNT_IDn'existent encore danshomelab_monitor.sh(vérifié le 2026-08-29).
Authentification
- PVE root@pam TFA cluster-wide (WebAuthn YK1 + WebAuthn YK2 + TOTP + Recovery Keys, propagé via pmxcfs) — 2026-05-11. Relying Party
home.gabin-simond.fr, accès via Traefikhttps://galahad.home.gabin-simond.fr. - Authelia 2FA (TOTP + WebAuthn FIDO2 YubiKey)
- Authelia regulation anti brute-force (5 retries / 2min / ban 10min) — 2026-04-14
- Consent mode
pre-configured(1 an) sur tous les clients OIDC - SSH clés uniquement, ports custom, MaxAuthTries 3
- SSH hardening LXC (100, 101, 102, 103) :
PermitRootLogin no,PasswordAuthentication no— 2026-04-16 - penny SSH :
PermitRootLogin no,PasswordAuthentication no(dietpi.conf override) — 2026-04-16 - galahad/lancelot SSH :
PermitRootLogin prohibit-password(exception cluster Proxmox) — 2026-04-16 - Comptes
gabinspartout, plus degabinlegacy - Sudo NOPASSWD (clé SSH = auth forte)
- SSH hardening Lynis (
AllowTcpForwarding no, etc.) - Login.defs hardened (UMASK 027, ENCRYPT_METHOD YESCRYPT, SHA_CRYPT_MIN_ROUNDS 65536, PASS_MIN/MAX/WARN) sur penny+galahad+lancelot
- PAM password strength (libpam-passwdqc), libpam-tmpdir, needrestart, apt-listbugs sur 3 hosts
OIDC SSO (Authelia)
- Proxmox galahad + lancelot (Administrator sur gabins@authelia)
- PBS (LXC 103,
gabins@autheliaAdmin sur /, two_factor, pre-configured consent 1y) — 2026-04-16 - Portainer
- Beszel
- Grafana (GrafanaAdmin, admin legacy désactivé)
- Rotation 2026-04-13 des 4 client_secret + AdGuard bcrypt — plains stockes Vaultwarden
ForwardAuth Authelia (services sans OIDC natif)
- Traefik dashboard
- Homepage (etait sans auth)
- AdGuard primaire (etait bcrypt seul)
- AdGuard secondaire dns-failover (route dns-failover.home.*)
- WUD (etait exposé anonyme — remplacé par Watchtower headless 2026-04-14)
Authelia session config explicite
expiration: 4hinactivity: 30mremember_me: 1M
Traefik TLS hardening
- minVersion TLS 1.2 (TLS 1.3 default actif)
- Cipher suites Mozilla intermediate (ChaCha20+AES-GCM ECDHE only)
- Curves X25519/P256/P384
- sniStrict (rejette SNI non-route)
Réseau
- Firewall iptables penny (INPUT DROP)
- Firewall Proxmox cluster (galahad + lancelot)
- Firewall iptables LXC (vault, logs, dns-failover, pbs) — INPUT DROP + whitelist par rôle
- NFS rules (ports 2049, 111) pour LAN + Tailscale sur penny — 2026-04-16
- Suppression règles temporaires vault-import (port 8765) — 2026-04-16
- sysctl hardening (rp_filter, SYN flood, source route, martians)
- kernel.kptr_restrict=2 + kernel.yama.ptrace_scope=2 (galahad+lancelot ; YAMA n/a kernel RPi)
- Rate limit Traefik Authelia (100 req/s SPA)
insecureSkipVerifyscope Proxmox only (serversTransport dedie, retire du global Traefik) — 2026-04-14- DNSSEC validation (AdGuard primaire + dns-failover)
- CAA records Cloudflare
- Security headers HTTPS (HSTS, X-Frame, Referrer, Permissions-Policy)
- Topic ntfy randomise (hex 32 chars, plus de topic prévisible) — 2026-04-14
Containers
- docker-socket-proxy + réseau
socketisolé cap_drop: ALLsur Authelia, Traefik, Homepage, Watchtower, Beszelno-new-privileges: trueglobal- ICC disabled sur bridge par defaut
- Plus aucun port direct exposé (tout via Traefik HTTPS)
read_only: true+ tmpfs sur Traefik, Homepage, Beszel, Watchtower- Pinning digests
@sha256:...sur Vaultwarden, Authelia, Traefik (supply chain) - Cosign installe (apt Trixie) — vérifie : aucune des 3 images critiques n'est signed upstream, attendre évolution
- Healthchecks Beszel + Portainer + beszel-agent : non applicable (images scratch/distroless, aucun outil CLI disponible)
Systemd-units hardening
- fail2ban : exposure 9.6 -> 4.7 (OK 🙂) sur penny+galahad+lancelot
- ssh : exposure 9.6 -> 8.1 sur penny+galahad+lancelot
- lynis : exposure 9.6 -> ~5
- Docker/cron/getty : non hardened (besoin privileges, casserait service)
Audit / détection
- Lynis + cron hebdo + ntfy (penny 76, galahad 68, lancelot 69)
- auditd actif sur penny + galahad + lancelot (auth, sudo, SSH, crontabs, firewall, Docker socket sur hosts Docker)
- fail2ban (penny + galahad)
- unattended-upgrades (penny + galahad)
- rpcbind désactivé (galahad + lancelot) ; activé sur penny pour NFS (PBS datastore)
Observabilite
- Loki + Grafana Alloy sur LXC
logs - Grafana renomme
logs.home.gabin-simond.fr - Dashboards : Homelab Overview (nouveau), Auth & Sécurité (ameliore), Traefik Access (ameliore), Logs Explorer
- Alerting rules (Authelia auth failures, fail2ban bans, Traefik 5xx, auditd sudo) -> ntfy
- Topic ntfy corrige dans repo git + Grafana contact point (2026-04-14)
- Retention 30 jours
Surveillance / résilience
- Watchdog hardware BCM2835 (penny)
- Autoheal (restart containers unhealthy)
- Auto-recovery SSD (device rename détection)
- dns-failover LXC (health check externe penny depuis galahad)
- DNS redondant primaire/secondaire
- AdGuard wildcard
*.home.gabin-simond.fr→ penny (Traefik reverse proxy) — 2026-04-16 - adguard-sync.sh : synchronisation primary → secondary (avec vérification canary) — 2026-04-16
- homelab_monitor.sh : checks
check_adguard_sync,check_backup_freshness,check_vault_backup_freshness— 2026-04-16 - Cluster quorum via qdevice sur penny — 3 votes, survit perte 1 node — 2026-04-19
- Auto-repair docker (
check_docker_autorepair) —docker compose up -dautomatique si stack vide + daemon UP > 2 min, circuit breaker 3/24h — 2026-04-19 - Cascade alert suppression —
house-down/lancelot-downsuppriment les alertes enfants redondantes — 2026-04-19 - check_cluster_hosts + check_house (Freebox + internet reach) — cause-racine avant symptomes — 2026-04-19
- check_restic_repos_freshness — surveillé 4 repos B2 avec seuils par repo — 2026-04-19
Supply chain code
- Pre-commit hook anti-leak (
scripts/pre-commit-secret-scan.sh) sur homelab-config + homelab-doc - Patterns : AWS, GitHub, Tailscale, B2, CF, PEM, password=, secret=
- 12/13 images Docker pinnees @sha256 (loki-replica ajoute 2026-04-19, reste 1 non-pinnee decelee)
Secrets management
- Authelia secrets externalises (jwt, session, encryption_key, hmac → fichiers
/config/secrets/chmod 600, charges viaAUTHELIA_*_FILEenv vars) — 2026-04-14 - Clé JWKS OIDC dans
oidc.pemséparée (gitignored) - Configuration Authelia versionnable sans secrets inline
- Sops + age scellement in-place des secrets authelia+crowdsec credentials — 2026-04-19
- Unseal au boot via
homelab-unseal.service→ tmpfs/run/homelab/→ bind-mount Docker - Age private key backupee sur B2 (
/root/.config/sops/agedanshomelab_backup.shpaths) — 2026-04-19
Hardening systemd mount leaks
- Drop-ins
PrivateMounts=yessur 10 services penny (ssh, fail2ban, networkd, timesyncd, auditd, fstrim, hostnamed, localed, logind, timedated) — 2026-04-17/19 - Idem 6 services sur galahad + lancelot (ssh, fail2ban, postfix, chrony, beszel-agent, logind) — 2026-04-19
- Fix la cascade
/etc /usr /bootRO au niveau namespace host via ProtectSystem=strict/full shared mount propagation - PVE firewall cluster.fw :
-log nolog→-log warningsur galahad+lancelot — 2026-04-19
Observabilite HA
- Alloy dual-write Loki : primary (LXC 101 lancelot:3100) + replica (container loki-replica penny:3101) depuis les 3 hosts — 2026-04-19 étendu aux PVE nodes
- Survit perte lancelot : replica continue sur penny
- WAL Alloy rejoue si un sink down = zero perte
Backup tiers indépendants PBS
- 4 chaines restic-direct vers Cloudflare R2 (EU) :
restic(penny daily),restic-vault(LXC 102 hourly),restic-dnsfailover(LXC 100 daily, 2026-04-19),restic-logs(LXC 101 daily, 2026-04-19) — migré depuis B2 2026-05-11 restic-check-monthly.shmulti-repo : structure + 10% data subset sur les 4 repos — 2026-04-19- Freshness monitor par repo avec seuils dedies (vault 3h hourly, autres 30h daily) —
check_restic_repos_freshnesspatché pour supporter S3 URLs 2026-05-11
SMTP submission outbound — port 25 fermé
- Postfix relay sur galahad + lancelot →
smtp.protonmail.ch:587(auth via Proton SMTP submission token, sealed sops) — 2026-05-11 /etc/postfix/sasl_passwdmode 600, postmap'd, chmod 600 sur.dbsender_canonicalregexp rewrite →[email protected](Proton refuse sinon MAIL FROM mismatch)/etc/aliases:root: [email protected]+newaliases- Chroot disabled sur smtp unix service (Debian default chrootait, masquait
/usr/lib/.../sasl2/) - libsasl2-modules installé sur les 2 nodes (sans : "No worthy mechs found")
- Egress firewall : port 25 outbound DROPPED, port 587 only sur galahad+lancelot — 2026-05-11. Plus de spam-relay risk si compromission container.
- Script
setup-postfix-relay.sh: seal/deploy/test/status/rollback, idempotent
Convention comptes et service accounts
- comptes.md : 3 tiers (root break-glass, gabins admin 2FA, svc-* automation)
- Naming convention, arbre de decision, stockage Vaultwarden — 2026-04-16
- Matrice par type de système (machines, PVE, PBS, Authelia, Docker, LXC, services web)
Migrations terminees
- Vaultwarden : penny Docker → LXC 102
vaultsur galahad (isolé de logs sur lancelot, 2026-04-13) - Volume Docker orphelin
config_vaultwarden-datasupprimé de penny (2026-04-14) - Proxmox cluster : pve1/pve2 -> galahad/lancelot
- Galahad + lancelot déjà sur Trixie (Debian 13 / PVE 9.1.7 / kernel 6.17.2-1-pve) — uniformite cluster
- Tailscale : container -> host natif (SSH natif)
- Wallos : supprimé