• v1.5 48aa5e41b1

    v1.5 Stable

    pepper 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 600 seul 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] (image curlimages/curl non-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_limit ajouté sur tous les services (protection fork-bomb) : 512 par défaut, 1024 sur nextcloud/nextcloud-cron (previews imagemagick/ffmpeg) et postgresql (marge au-delà de max_connections=200). Porté par deploy.resources.limits.pids (le champ top-level pids_limit fait planter la validation Compose v5.3.1 dès qu'un deploy.resources existe déjà).
    • grafana : read_only: true ajouté, pour cohérence avec loki/prometheus/node-exporter/docker-socket-proxy (même profil cap_drop: ALL).
    • swag et wireguard : cap_drop: ALL ajouté (ils tournaient jusqu'ici avec le jeu de capabilities Docker par défaut complet — 14 caps — en plus de NET_ADMIN, seuls services de la stack dans ce cas). cap_add ré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/SETGID en trop sur le service le plus exposé). Validé par cycle down && up complet + test fonctionnel (wg show, HTTPS sur 4 sous-domaines, certificat, bouncer CrowdSec).
    • Toute la stack validée par deux cycles down && up complets (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 — seul Strict-Transport-Security était actif). Appliqué globalement aux 6 apps exposées (jellyfin, grafana, authentik, qbittorrent, nextcloud, forgejo). proxy_hide_header ajouté en complément : plusieurs apps (grafana → X-Frame-Options: deny, nextcloud → Referrer-Policy: no-referrer) fixent déjà leur propre valeur, et add_header nginx s'additionne au lieu de remplacer → sans proxy_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_signin dans authentik-server.conf) ne porte aucun de ces en-têtes — ce location a son propre add_header Set-Cookie, ce qui annule par construction nginx l'héritage des add_header du niveau server sur 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 3 add_header dans 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 contient secrets/ 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 (cp préserve le mode). N'importe quel compte local pouvait donc lire tous les secrets de la stack, ce qui annulait le durcissement 600/640 fait plus haut. Fix : chmod 600 "$ARCHIVE" ajouté juste après la création dans backup.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 de backup.sh : nouvelle archive et sa copie hors-site toutes deux en 600 sans intervention.

      Vérifié en même temps : les dossiers .pre-restore_* (snapshots avant restauration) ne contiennent que .env (déjà 600) et docker-compose.yml, pas de secrets/ — pas de fuite de ce côté.

    • 🔴🔴 nextcloud/data/ (hors dépôt, données applicatives) : toute l'arborescence était en 777 — ~28 000 fichiers, code PHP compris (index.php, remote.php, occ...) et config.php (contenant secret, 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 un chmod -R 777 fait 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 en 777 aprè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 --all sur les 4 comptes sans erreur, écriture réelle testée (création/lecture/suppression d'un fichier en tant que www-data), status.php et HTTPS toujours 200/302 comme attendu.

      Ce 777 ré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/*.rdb en 777 — 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 en 600. Testé avec un BGREWRITEAOF forcé : Redis a régénéré de nouveaux fichiers de génération (.2.*) en 644 par défaut (confirme que ce n'est pas Redis lui-même qui produit du 777 — origine du 777 initial non identifiée, probablement une manipulation manuelle antérieure) ; ces nouveaux fichiers resserrés en 600 aussi. Validé : PING, SET/GET réel, BGREWRITEAOF (aof_last_bgrewrite_status:ok), aucune erreur dans les logs redis.
    • forgejo/data/gitea/conf/app.ini en 644 — contient en clair PASSWD (mot de passe DB), SECRET_KEY, INTERNAL_TOKEN et JWT_SECRET. N'importe quel compte local pouvait forger des tokens d'authentification Forgejo (JWT/internal) sans avoir besoin d'un compte. Passé en 600 (le process gitea web tourne 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.db en 644 — base SQLite jellyfin (comptes locaux/hash de mots de passe potentiels, historique de visionnage). Passé en 600 par précaution (même schéma que forgejo : propriétaire déjà correct). Validé par force-recreate + /health toujours Healthy + 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 laisser postgres/redis/nextcloud/etc. non pinnés.

    Downloads