Table of contents
🛡 Sécurité
Dernière mise à jour : 2026-07-16. Le détail exhaustif (dates, commandes de validation, logs) est dans
CHANGELOG.mdà la racine du dépôt — cette page en donne la synthèse pour s'orienter rapidement.
Durcissement de la stack — synthèse
Permissions & secrets
- Tous les fichiers sous
secrets/en600(debian:debian), sauf 3 exceptions en640 debian:root(forgejo_git_token,grafana_admin_password,grafana_oauth_secret) car les images correspondantes (renovate, grafana) tournent en UID non-root mais GIDroot. backup.sh: les archives générées sontchmod 600juste après création (elles contiennentsecrets/en clair) — avant, elles étaient644, lisibles par tout compte local.nextcloud/data/(hors dépôt) : était en777récursif (code PHP +config.phpavec les credentials DB) — faille la plus sévère trouvée à ce jour (RCE quasi directe via écriture de PHP par n'importe quel compte local). Corrigé en640/750.authentik/redis/appendonlydir/*.aof: fichiers de persistance Redis trouvés en777, resserrés en600.forgejo/data/gitea/conf/app.ini(contientSECRET_KEY/JWT_SECRET/mot de passe DB) etjellyfin/config/data/jellyfin.db: étaient en644, passés en600.swag/conf/crowdsec/crowdsec-nginx-bouncer.conf(API_KEY CrowdSec en clair, régénéré à chaque démarrage par le modswag-crowdsec) : passé en600via un scriptcustom-cont-init.ddédié (65-fix-crowdsec-perms).- Bruit
400surPOST /v1/usage-metrics(LAPI CrowdSec) : lemetrics.luadu modswag-crowdsec(régénéré à chaque démarrage, comme le conf bouncer ci-dessus) utilisecjson.array_mt— inexistant dans lalua-cjsonembarquée, la bonne constante estcjson.empty_array_mt— ce qui sérialisefeature_flagsen{}au lieu de[], rejeté par le LAPI. Sans impact sur le blocage des IP, juste du bruit dans les logs. Corrigé par un scriptcustom-cont-init.dde plus (70-fix-crowdsec-metrics, même pattern que65-fix-crowdsec-perms:sedaprès le mod-init, avant nginx).
Conteneurs / capabilities
pids_limit(protection fork-bomb) sur tous les services :512par défaut,1024surnextcloud/nextcloud-cron/postgresql.cap_drop: ALL+cap_addminimal surswagetwireguard(avant : capability set Docker par défaut complet +NET_ADMIN, sur les services les plus exposés du réseau).read_only: truegénéralisé (redis,redis-nextcloud,grafana,loki,prometheus,node-exporter,docker-socket-proxy).docker-socket-proxy: accès à l'API Docker en lecture seule uniquement (POST=0), scope limité aux endpoints nécessaires à Alloy.
En-têtes HTTP (SWAG)
Referrer-Policy,X-Content-Type-Options,X-Frame-Optionsajoutés dansswag/conf/nginx/ssl.conf(seulStrict-Transport-Securityétait actif avant) +proxy_hide_headerpour éviter les doublons quand une app (grafana, nextcloud) fixe déjà sa propre valeur.- Limite connue : la page de redirection de login qBittorrent (
@goauthentik_proxy_signin) ne porte pas ces en-têtes (limitation du fichier partagé linuxserver.io, impact limité).
Réseau / accès
- Whitelist d'authentification qBittorrent trop large :
WebUI\AuthSubnetWhitelistcouvrait tout le réseau dockerswag(172.22.0.0/24, 8 conteneurs) au lieu de la seule IP de SWAG — n'importe quel autre conteneur du réseau (nextcloud, forgejo, grafana, jellyfin...) pouvait, si compromis, administrer qBittorrent sans authentification, y compris exécuter des commandes arbitraires via sa fonctionnalité "run external program". Corrigé : IP fixe pourswag(172.22.0.5) + whitelist réduite à172.22.0.5/32. sshd_config: entréeAllowUsers free debiannettoyée —freene correspondait à aucun compte local (résidu de config, sans lien avec le compte distantfreeutilisé pour le montage sshfs hors-site debackup.sh).- Pare-feu iptables sur
eno1:DROPpar défaut, seuls les ports réellement exposés sont ouverts (80/443 SWAG, 6881 qBittorrent, 51820/udp WireGuard, SSH sur port non standard). sudosans mot de passe pourdebian(NOPASSWD:ALL, généré par cloud-init) : accepté tel quel — serveur mono-admin, accès SSH par clé uniquement (PasswordAuthentication no), donc pas de second facteur à contourner de toute façon.
Bug latent découvert et corrigé
- Les scripts
custom-cont-init.d/de SWAG n'ont jamais été exécutés depuis leur création : le base-image LSIO cherche les scripts dans/custom-cont-init.d(racine du conteneur), pas/config/custom-cont-init.d. Un montage dédié a été ajouté, avec un piège : le pointer sur un sous-dossier deswag/conf/(donc du mount RW/config) ne suffit pas — LSIO réinitialise l'ownershipPUID:PGIDsur tout/configau boot, ce qui annule silencieusement unchown root:rootposé sur le dossier de scripts. Le dossier a donc été déplacé hors deswag/conf/(swag/custom-cont-init.d/,root:root).
CrowdSec + hCaptcha
Architecture
Internet → SWAG (bouncer lua) → décision CrowdSec LAPI
↑
logs nginx ────────┤
events Docker ─────┘ (authentik-server)
Comment ça fonctionne
- Le mod swag-crowdsec (lua) intercepte chaque requête dans nginx
- Il interroge la LAPI CrowdSec (port 8080 interne) pour l'IP source
- La LAPI répond :
ban,captcha, ou rien (IP inconnue → passe) - En parallèle, CrowdSec analyse les logs nginx et les événements Docker en temps réel
Remédiation
| Décision | Comportement SWAG |
|---|---|
ban |
403 immédiat — IP bloquée |
captcha |
Page hCaptcha servie avant accès |
| (aucune) | Requête proxifiée normalement |
hCaptcha
Provider : hCaptcha (sans tracking Google).
Les clés sont injectées via Docker secrets dans SWAG :
# docker-compose.yml → service swag
environment:
- CROWDSEC_CAPTCHA_PROVIDER=hcaptcha
- FILE__CROWDSEC_SITE_KEY=/run/secrets/hcaptcha_site_key
- FILE__CROWDSEC_SECRET_KEY=/run/secrets/hcaptcha_secret_key
secrets:
- hcaptcha_site_key
- hcaptcha_secret_key
Le fichier
swag/conf/crowdsec/crowdsec-nginx-bouncer.confest régénéré à chaque démarrage par le modswag-crowdsecà partir des variables d'environnement (et repassé en600par65-fix-crowdsec-perms). Ne pas l'éditer directement. Lemetrics.luadu même mod est lui aussi régénéré à chaque démarrage et repatché par70-fix-crowdsec-metrics(cf. synthèse ci-dessus).
Renouveler les clés hCaptcha :
# 1. Mettre à jour les fichiers secrets
echo "nouvelle_site_key" > secrets/hcaptcha_site_key.txt
echo "nouveau_secret_key" > secrets/hcaptcha_secret_key.txt
# 2. Recréer SWAG pour recharger les secrets
sudo docker compose up -d --force-recreate swag
Collections CrowdSec actives
# Lister les collections installées
sudo docker exec crowdsec cscli collections list
Collections recommandées (installées) :
crowdsecurity/nginx— parsing logs nginx + scénarios HTTPcrowdsecurity/http-bf— brute-force HTTP génériquecrowdsecurity/base-http-scenarios— scénarios de base
Commandes de gestion
# ── Décisions en cours ──────────────────────────────────────────────────────
sudo docker exec crowdsec cscli decisions list
# ── Débannir une IP ─────────────────────────────────────────────────────────
sudo docker exec crowdsec cscli decisions delete --ip 1.2.3.4
# ── Bannir manuellement une IP ──────────────────────────────────────────────
sudo docker exec crowdsec cscli decisions add --ip 1.2.3.4 --duration 24h --reason "test"
# ── Alertes sur une IP ──────────────────────────────────────────────────────
sudo docker exec crowdsec cscli alerts list --ip 1.2.3.4
# ── État du bouncer SWAG ────────────────────────────────────────────────────
sudo docker exec crowdsec cscli bouncers list
# ── Métriques ───────────────────────────────────────────────────────────────
sudo docker exec crowdsec cscli metrics
# ── MAJ des hub (collections, parsers, scénarios) ───────────────────────────
sudo docker exec crowdsec cscli hub update && sudo docker exec crowdsec cscli hub upgrade
Whitelister une IP
Créer ./crowdsec/config/parsers/s02-enrich/whitelist.yaml :
name: crowdsecurity/whitelists
description: "IPs de confiance"
whitelist:
reason: "réseau local / VPN"
ip:
- "192.168.1.0/24"
- "10.13.13.0/24"
Puis redémarrer CrowdSec : sudo docker compose restart crowdsec
Dépannage bouncer
# Vérifier que le bouncer est enregistré
sudo docker exec crowdsec cscli bouncers list
# Logs du bouncer (côté SWAG)
sudo docker compose logs swag | grep -i crowdsec
# Tester la LAPI manuellement
sudo docker exec crowdsec cscli lapi status