• v1.6 c5d935421f

    v1.6 Stable

    pepper released this 2026-07-16 01:04:26 +00:00 | 20 commits to main since this release

    Sécurité

    • swag/conf/crowdsec/crowdsec-nginx-bouncer.conf en 644 — régénéré à chaque démarrage par le mod linuxserver/mods:swag-crowdsec, contient l'API_KEY du bouncer CrowdSec en clair, lisible par tout compte local. Découvert en creusant pourquoi un script custom-cont-init.d censé corriger ça au démarrage ne semblait avoir aucun effet.
    • 🔴 Bug latent découvert par ricochet : les scripts swag/conf/custom-cont-init.d/* n'ont jamais été exécutés depuis leur création — le script d'init du base-image LSIO (/etc/s6-overlay/s6-rc.d/init-custom-files/run) a le chemin SCRIPTS_DIR="/custom-cont-init.d" codé en dur (racine du conteneur), alors que le compose ne montait que ./swag/conf:/config → les scripts atterrissaient à /config/custom-cont-init.d, jamais scanné. Log confirmé : [custom-init] No custom files found, skipping.... Ça concernait aussi 60-fix-propagation (créé le 2026-07-04, jamais vraiment actif malgré le CHANGELOG antérieur qui le présentait comme fonctionnel). Fix en deux temps :
      1. Script 65-fix-crowdsec-perms ajouté (chmod 600 sur le conf bouncer, même pattern que 60-fix-propagation).
      2. Montage dédié /custom-cont-init.d:ro ajouté — mais un premier essai en le pointant sur ./swag/conf/custom-cont-init.d s'est révélé insuffisant : ce chemin restait aussi accessible en écriture via le mount /config (chevauchement, même inode des deux côtés), et une étape tardive du boot LSIO réinitialise silencieusement l'ownership à PUID:PGID sur tout /config — un chown root:root posé juste avant se faisait donc défaire après coup (le bandeau d'avertissement not owned by root du base-image disparaissait du log sans que la protection tienne réellement). Fix définitif : nouveau dossier swag/custom-cont-init.d/ hors de swag/conf/ (donc hors du mount RW), scripts copiés là en root:root 755, montage docker-compose.yml repointé dessus. Les fichiers originaux dans swag/conf/custom-cont-init.d/ renommés en .old (conservés, non exécutés) plutôt que supprimés.
        Validé : stat sur l'hôte confirme root:root maintenu après un boot complet (contrairement à la première tentative) ; logs complets sans bandeau d'avertissement ; les deux scripts exited 0 ; crowdsec-nginx-bouncer.conf repassé en 600 à chaque démarrage ; 20/20 conteneurs healthy ; whitelist qBittorrent retestée (nextcloud403, swag200, cf. entrée suivante) ; 6 sous-domaines HTTPS publics répondent normalement. Effet de bord observé et sans gravité : 60-fix-propagation relance désormais un certbot certonly complet (incluant les 120s d'attente DNS) à chaque redémarrage de swag — même date d'expiration de certificat obtenue à 3 reprises rapprochées lors des tests, donc pas de nouvelle émission distincte ni de risque de rate-limit Let's Encrypt observé, mais à surveiller si swag redémarre très fréquemment.
    • 🔴 qBittorrent : la whitelist de bypass d'authentification WebUI couvrait tout le réseau swag (172.22.0.0/24), pas seulement SWAGWebUI\AuthSubnetWhitelistEnabled=true + WebUI\AuthSubnetWhitelist=172.22.0.0/24 dispensait de login toute requête provenant de n'importe quelle IP de ce /24. Or 7 autres conteneurs partagent ce réseau (nextcloud, forgejo, grafana, jellyfin, authentik-server, wireguard, swag lui-même) — la protection Authentik (forward-auth @goauthentik_proxy_signin) n'étant appliquée que côté nginx dans SWAG, pas par qBittorrent, n'importe lequel de ces conteneurs compromis (RCE applicative) pouvait joindre directement l'IP de qBittorrent sur le réseau docker et administrer entièrement le client torrent sans aucune authentification — y compris sa fonctionnalité "exécuter une commande externe" (exécution de code dans le conteneur). Fix : IP fixe pour swag sur le réseau swag (ipv4_address: 172.22.0.5 dans docker-compose.yml, backup.sh ne fait pas tourner down/up en automatique donc pas de risque de dérive d'IP) + whitelist qBittorrent réduite à 172.22.0.5/32. Édition du fichier de conf faite pendant que le conteneur était arrêté (évite qu'il réécrive l'ancienne valeur en mémoire au shutdown). Validé par cycle down -v && up -d complet (20/20 conteneurs healthy) + test direct : requête depuis nextcloud vers l'API qBittorrent → 403 Forbidden (bloqué, comme attendu) ; même requête depuis swag200 (flux légitime intact) ; les 6 sous-domaines HTTPS publics répondent normalement (302 SSO ou 200). Seules anomalies de logs : résolutions DNS transitoires de forgejo au tout début du démarrage de renovate (auto-résolues, run Renovate terminé avec succès juste après) — sans rapport avec ce fix.
    • sshd_config : AllowUsers free debianAllowUsers debianfree ne correspond à aucun compte local (getent passwd free : absent). C'est un résidu de config (le nom coïncide avec le compte free utilisé côté serveur distant pour le montage sshfs hors-site de backup.sh, mais ce montage est sortant — ce serveur se connecte vers le distant en tant que free, aucun rapport avec les connexions SSH entrantes gérées par AllowUsers ici). Risque : dormant tant que free n'existe pas localement, mais si un compte free était recréé un jour sans qu'on y pense, il hériterait automatiquement de l'autorisation SSH. Atténué en plus par PasswordAuthentication no (clé obligatoire). Backup de sshd_config fait avant modif, syntaxe validée (sshd -t), rechargé à chaud (systemctl reload sshd, pas de coupure de session).
    Downloads
  • v1.2 21173cd3d3

    v1.2 Stable

    pepper released this 2026-06-27 03:44:08 +00:00 | 78 commits to main since this release

    Ajouté

    • Wiki complet (10 pages) : Services, Réseaux, SSO, BDD, Supervision, Sécurité, Sauvegardes, Exploitation, Secrets
    • Avatar du dépôt Forgejo (baleine Docker stylisée)
    • README enrichi avec badges shields.io, emojis et sections repliables <details>
    Downloads
  • v1.1 9192e18c98

    v1.1 Stable

    pepper released this 2026-06-27 03:44:08 +00:00 | 80 commits to main since this release

    Ajouté

    • Intégration CrowdSec IDS/IPS avec bouncer lua dans SWAG
    • hCaptcha comme provider de captcha (clés injectées via Docker secrets)
    • Script git-push.sh — contournement hairpin NAT via réseau Docker interne
    • Dépôt Forgejo pepper/docker-infra (privé)
    • Section Sécurité dans le README
    • Section Versionnement dans le README

    Modifié

    • .gitignore affiné : exclusion fail2ban.sqlite3, crowdsec/, secrets hCaptcha
    • Secrets Docker : ajout hcaptcha_site_key, hcaptcha_secret_key, crowdsec_api_key, forgejo_git_token
    Downloads
  • v1.0 1e7e6fb6f0

    v1.0 Stable

    pepper released this 2026-06-27 03:44:08 +00:00 | 89 commits to main since this release

    Stack initiale — 22 services

    Applications

    • Nextcloud 34 (cloud de fichiers)
    • Forgejo 10 (forge Git)
    • Jellyfin (médiathèque) + plugin SSO init
    • qBittorrent (client torrent)

    Identité & sécurité

    • Authentik SSO (OIDC natif pour Grafana, Forgejo, Jellyfin — forward-auth pour qBittorrent)
    • SWAG reverse proxy (Let's Encrypt DNS challenge Cloudflare)
    • WireGuard VPN

    Données

    • PostgreSQL 18 partagé (3 bases : authentik, forgejo, nextcloud)
    • Redis × 2 (Authentik AOF + Nextcloud LRU)

    Supervision

    • Prometheus + Loki + Alloy + Node-exporter + Grafana

    Infra

    • Watchtower (opt-in) + 2 socket-proxies Docker
    • 9 réseaux Docker cloisonnés (segmentation et moindre privilège)
    • Secrets via fichiers (/run/secrets/)
    • backup.sh / restore.sh avec rotation et copie hors-site
    Downloads
  • 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
  • v1.4 a176a9d784

    v1.4 Stable

    pepper released this | 63 commits to main since this release

    Modifié

    • Watchtower retiré — remplacé par Renovate (PRs hebdomadaires sur Forgejo, merge manuel)
    • renovate.json : 3 groupes de packages (stack critique / monitoring / infra), toutes les images Docker épinglées
    • 7 réseaux au lieu de 9 (suppression des réseaux watchtower et watchtower-proxy)
    • 20 services au lieu de 22 (–2 watchtower/watchtower-socket-proxy, +1 renovate, –1 forgejo-runner)
    • README mis à jour : badges (dont badge Renovate), section Mise à jour, table des réseaux, commandes quotidiennes

    Downloads
  • v1.3 bc782f5cdf

    v1.3 Stable

    pepper released this | 76 commits to main since this release

    Modifié

    • git-push.sh : token extrait dans une variable avant injection dans l'URL (SC2046)
    • backup.sh, restore.sh : remplacement de ls par find+sort pour la détection des archives (SC2012)
    Downloads