3 Sécurité
pepper edited this page 2026-07-16 15:54:23 +00:00

🛡 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/ en 600 (debian:debian), sauf 3 exceptions en 640 debian:root (forgejo_git_token, grafana_admin_password, grafana_oauth_secret) car les images correspondantes (renovate, grafana) tournent en UID non-root mais GID root.
  • backup.sh : les archives générées sont chmod 600 juste après création (elles contiennent secrets/ en clair) — avant, elles étaient 644, lisibles par tout compte local.
  • nextcloud/data/ (hors dépôt) : était en 777 récursif (code PHP + config.php avec 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é en 640/750.
  • authentik/redis/appendonlydir/*.aof : fichiers de persistance Redis trouvés en 777, resserrés en 600.
  • forgejo/data/gitea/conf/app.ini (contient SECRET_KEY/JWT_SECRET/mot de passe DB) et jellyfin/config/data/jellyfin.db : étaient en 644, passés en 600.
  • swag/conf/crowdsec/crowdsec-nginx-bouncer.conf (API_KEY CrowdSec en clair, régénéré à chaque démarrage par le mod swag-crowdsec) : passé en 600 via un script custom-cont-init.d dédié (65-fix-crowdsec-perms).
  • Bruit 400 sur POST /v1/usage-metrics (LAPI CrowdSec) : le metrics.lua du mod swag-crowdsec (régénéré à chaque démarrage, comme le conf bouncer ci-dessus) utilise cjson.array_mt — inexistant dans la lua-cjson embarquée, la bonne constante est cjson.empty_array_mt — ce qui sérialise feature_flags en {} au lieu de [], rejeté par le LAPI. Sans impact sur le blocage des IP, juste du bruit dans les logs. Corrigé par un script custom-cont-init.d de plus (70-fix-crowdsec-metrics, même pattern que 65-fix-crowdsec-perms : sed après le mod-init, avant nginx).

Conteneurs / capabilities

  • pids_limit (protection fork-bomb) sur tous les services : 512 par défaut, 1024 sur nextcloud/nextcloud-cron/postgresql.
  • cap_drop: ALL + cap_add minimal sur swag et wireguard (avant : capability set Docker par défaut complet + NET_ADMIN, sur les services les plus exposés du réseau).
  • read_only: true gé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-Options ajoutés dans swag/conf/nginx/ssl.conf (seul Strict-Transport-Security était actif avant) + proxy_hide_header pour é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\AuthSubnetWhitelist couvrait tout le réseau docker swag (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 pour swag (172.22.0.5) + whitelist réduite à 172.22.0.5/32.
  • sshd_config : entrée AllowUsers free debian nettoyée — free ne correspondait à aucun compte local (résidu de config, sans lien avec le compte distant free utilisé pour le montage sshfs hors-site de backup.sh).
  • Pare-feu iptables sur eno1 : DROP par défaut, seuls les ports réellement exposés sont ouverts (80/443 SWAG, 6881 qBittorrent, 51820/udp WireGuard, SSH sur port non standard).
  • sudo sans mot de passe pour debian (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 de swag/conf/ (donc du mount RW /config) ne suffit pas — LSIO réinitialise l'ownership PUID:PGID sur tout /config au boot, ce qui annule silencieusement un chown root:root posé sur le dossier de scripts. Le dossier a donc été déplacé hors de swag/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

  1. Le mod swag-crowdsec (lua) intercepte chaque requête dans nginx
  2. Il interroge la LAPI CrowdSec (port 8080 interne) pour l'IP source
  3. La LAPI répond : ban, captcha, ou rien (IP inconnue → passe)
  4. 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.conf est régénéré à chaque démarrage par le mod swag-crowdsec à partir des variables d'environnement (et repassé en 600 par 65-fix-crowdsec-perms). Ne pas l'éditer directement. Le metrics.lua du même mod est lui aussi régénéré à chaque démarrage et repatché par 70-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 HTTP
  • crowdsecurity/http-bf — brute-force HTTP générique
  • crowdsecurity/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