-
v1.5 Stable
released this | 22 commits to main since this release
Sécurité
- Secrets uniformisés en
600(chown debian:debian) — 7 fichiers étaient encore monde-lisibles (644/444:grafana_admin_password,grafana_oauth_secret,jellyfin_api_key,jellyfin_sso_secret,redis_password,redis_nextcloud_password,forgejo_git_token). N'importe quel compte local du serveur pouvait les lire. - Conséquence gérée par service (le mode
600seul ne suffit pas partout — dépend de l'UID/capabilities du process qui lit le secret) :redis/redis-nextcloud:cap_add: DAC_OVERRIDE(tournent en root, capability effective).jellyfin-sso-init:user: "0:0"+cap_drop: [ALL]+cap_add: [DAC_OVERRIDE](imagecurlimages/curlnon-root sans groupe secondaire root).forgejo_git_token.txt,grafana_admin_password.txt,grafana_oauth_secret.txt:640 debian:root(images renovate et grafana tournent en UID non-root mais GID 0/root, lecture via le bit groupe).
pids_limitajouté sur tous les services (protection fork-bomb) :512par défaut,1024surnextcloud/nextcloud-cron(previews imagemagick/ffmpeg) etpostgresql(marge au-delà demax_connections=200). Porté pardeploy.resources.limits.pids(le champ top-levelpids_limitfait planter la validation Compose v5.3.1 dès qu'undeploy.resourcesexiste déjà).grafana:read_only: trueajouté, pour cohérence avecloki/prometheus/node-exporter/docker-socket-proxy(même profilcap_drop: ALL).swagetwireguard:cap_drop: ALLajouté (ils tournaient jusqu'ici avec le jeu de capabilities Docker par défaut complet — 14 caps — en plus deNET_ADMIN, seuls services de la stack dans ce cas).cap_addréduit àNET_ADMIN+CHOWN/SETUID/SETGID/FOWNER/DAC_OVERRIDE(même pattern que qbittorrent/jellyfin). Trouvé lors d'un audit pentest : SWAG étant le seul point d'entrée HTTP(S) public de la stack, c'était la plus grosse surface de capabilities inutiles de toute la config (NET_RAW,SYS_CHROOT,SETUID/SETGIDen trop sur le service le plus exposé). Validé par cycledown && upcomplet + test fonctionnel (wg show, HTTPS sur 4 sous-domaines, certificat, bouncer CrowdSec).- Toute la stack validée par deux cycles
down && upcomplets (un après les fixes secrets/pids_limit, un après le durcissement swag/wireguard) + lecture intégrale des logs (pas de--tail) : 20/20 conteneurs healthy, aucune erreur de permission. - En-têtes de sécurité HTTP activés dans
swag/conf/nginx/ssl.conf(Referrer-Policy,X-Content-Type-Options,X-Frame-Options— seulStrict-Transport-Securityétait actif). Appliqué globalement aux 6 apps exposées (jellyfin, grafana, authentik, qbittorrent, nextcloud, forgejo).proxy_hide_headerajouté en complément : plusieurs apps (grafana →X-Frame-Options: deny, nextcloud →Referrer-Policy: no-referrer) fixent déjà leur propre valeur, etadd_headernginx s'additionne au lieu de remplacer → sansproxy_hide_header, le navigateur recevait deux valeurs différentes pour le même header (comportement non garanti). Vérifié header par header sur les 5 apps HTTP après coup : une seule valeur cohérente partout.⚠️ Limite connue, non corrigée : la redirection de login qBittorrent (
@goauthentik_proxy_signindansauthentik-server.conf) ne porte aucun de ces en-têtes — ce location a son propreadd_header Set-Cookie, ce qui annule par construction nginx l'héritage desadd_headerdu niveauserversur cette réponse précise. Impact limité (HSTS déjà mémorisé par le navigateur depuis une réponse précédente sur le domaine) ; corriger proprement demanderait de dupliquer les 3add_headerdans ce fichier partagé linuxserver.io, non fait pour éviter la dérive au prochain refresh de l'image SWAG. - 🔴
backup.sh: les archives générées étaient monde-lisibles (644) — trouvé lors de l'audit pentest : l'archive contientsecrets/en clair (confirmé par extraction directe d'un secret depuis l'archive sans droits particuliers), et ça concernait aussi bien les 3 archives locales que les 10 copies hors-site (cppréserve le mode). N'importe quel compte local pouvait donc lire tous les secrets de la stack, ce qui annulait le durcissement600/640fait plus haut. Fix :chmod 600 "$ARCHIVE"ajouté juste après la création dansbackup.sh, avant rotation et copie hors-site (donc la copie hérite aussi du bon mode). Archives existantes (3 locales + 10 hors-site) corrigées manuellement. Validé par un run réel complet debackup.sh: nouvelle archive et sa copie hors-site toutes deux en600sans intervention.Vérifié en même temps : les dossiers
.pre-restore_*(snapshots avant restauration) ne contiennent que.env(déjà600) etdocker-compose.yml, pas desecrets/— pas de fuite de ce côté. - 🔴🔴
nextcloud/data/(hors dépôt, données applicatives) : toute l'arborescence était en777— ~28 000 fichiers, code PHP compris (index.php,remote.php,occ...) etconfig.php(contenantsecret,passwordsalt, et le mot de passe PostgreSQL en clair), en lecture et écriture pour n'importe quel compte local. Sévérité la plus haute de toute la session : contrairement aux backups (lecture seule), ceci permettait à n'importe quel utilisateur local d'injecter du code PHP directement exécuté par Apache à la prochaine requête HTTP — un chemin quasi direct compte-local → RCE dans le conteneur Nextcloud. Ownership déjà correct (www-data:www-data) partout, seul le mode était en cause (probablement unchmod -R 777fait un jour pour débloquer un souci de permission au setup/restore, jamais resserré). Fix :find nextcloud/data/ -type f -exec chmod 640+-type d -exec chmod 750. Les 118 entrées encore vues en777après coup sont des liens symboliques (lrwxrwxrwx) — mode cosmétique sous Linux, sans incidence (le noyau applique les permissions du fichier cible, déjà à 640). Validé :occ files:scan --allsur les 4 comptes sans erreur, écriture réelle testée (création/lecture/suppression d'un fichier en tant quewww-data),status.phpet HTTPS toujours 200/302 comme attendu.Ce
777récursif semble spécifique à nextcloud, pas un pattern répété ailleurs — scan complet du reste du projet (find -perm -002) : 0 résultat en dehors des deux cas traités ici et ci-dessous. authentik/redis/appendonlydir/*.aof/*.rdben777— 2 fichiers de persistance Redis (AOF) trouvés world-writable, propriétaire déjà correct (root:root, le conteneur redis tournant en root). Passés en600. Testé avec unBGREWRITEAOFforcé : Redis a régénéré de nouveaux fichiers de génération (.2.*) en644par défaut (confirme que ce n'est pas Redis lui-même qui produit du777— origine du777initial non identifiée, probablement une manipulation manuelle antérieure) ; ces nouveaux fichiers resserrés en600aussi. Validé :PING,SET/GETréel,BGREWRITEAOF(aof_last_bgrewrite_status:ok), aucune erreur dans les logs redis.forgejo/data/gitea/conf/app.inien644— contient en clairPASSWD(mot de passe DB),SECRET_KEY,INTERNAL_TOKENetJWT_SECRET. N'importe quel compte local pouvait forger des tokens d'authentification Forgejo (JWT/internal) sans avoir besoin d'un compte. Passé en600(le processgitea webtourne en UID 1000, identique au propriétaire du fichier — pas besoin de capability). Validé par force-recreate + logs complets sans erreur + API forgejo fonctionnelle (auth par token, lecture des commits).jellyfin/config/data/jellyfin.dben644— base SQLite jellyfin (comptes locaux/hash de mots de passe potentiels, historique de visionnage). Passé en600par précaution (même schéma que forgejo : propriétaire déjà correct). Validé par force-recreate +/healthtoujoursHealthy+ HTTPS 302 (redirection SSO normale).
Testé et abandonné
pinDigests: true(Renovate) testé en dry-run puis en conditions réelles : épuise systématiquement le quota anonyme Docker Hub (100 req/h) à cause de l'appel de résolution de digest par image, et fait échouer tout le run (external-host-error), pas seulement les images concernées. Reverté. Pour l'activer un jour : authentifier Renovate auprès de Docker Hub (hostRules) ou le scoper aux registres hors Docker Hub (ghcr.io/lscr.io/codeberg.org), au prix de laisserpostgres/redis/nextcloud/etc. non pinnés.
Downloads
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
0 downloads
- Secrets uniformisés en