-
v1.6 Stable
released this
2026-07-16 01:04:26 +00:00 | 20 commits to main since this releaseSécurité
swag/conf/crowdsec/crowdsec-nginx-bouncer.confen644— régénéré à chaque démarrage par le modlinuxserver/mods:swag-crowdsec, contient l'API_KEY du bouncer CrowdSec en clair, lisible par tout compte local. Découvert en creusant pourquoi un scriptcustom-cont-init.dcensé 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 cheminSCRIPTS_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 aussi60-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 :- Script
65-fix-crowdsec-permsajouté (chmod 600sur le conf bouncer, même pattern que60-fix-propagation). - Montage dédié
/custom-cont-init.d:roajouté — mais un premier essai en le pointant sur./swag/conf/custom-cont-init.ds'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:PGIDsur tout/config— unchown root:rootposé juste avant se faisait donc défaire après coup (le bandeau d'avertissementnot owned by rootdu base-image disparaissait du log sans que la protection tienne réellement). Fix définitif : nouveau dossierswag/custom-cont-init.d/hors deswag/conf/(donc hors du mount RW), scripts copiés là enroot:root 755, montagedocker-compose.ymlrepointé dessus. Les fichiers originaux dansswag/conf/custom-cont-init.d/renommés en.old(conservés, non exécutés) plutôt que supprimés.
Validé :statsur l'hôte confirmeroot:rootmaintenu après un boot complet (contrairement à la première tentative) ; logs complets sans bandeau d'avertissement ; les deux scriptsexited 0;crowdsec-nginx-bouncer.confrepassé en600à chaque démarrage ; 20/20 conteneurs healthy ; whitelist qBittorrent retestée (nextcloud→403,swag→200, cf. entrée suivante) ; 6 sous-domaines HTTPS publics répondent normalement. Effet de bord observé et sans gravité :60-fix-propagationrelance désormais uncertbot certonlycomplet (incluant les 120s d'attente DNS) à chaque redémarrage deswag— 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 siswagredémarre très fréquemment.
- Script
- 🔴
qBittorrent: la whitelist de bypass d'authentification WebUI couvrait tout le réseauswag(172.22.0.0/24), pas seulement SWAG —WebUI\AuthSubnetWhitelistEnabled=true+WebUI\AuthSubnetWhitelist=172.22.0.0/24dispensait 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,swaglui-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 pourswagsur le réseauswag(ipv4_address: 172.22.0.5dansdocker-compose.yml, backup.sh ne fait pas tournerdown/upen 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 cycledown -v && up -dcomplet (20/20 conteneurs healthy) + test direct : requête depuisnextcloudvers l'API qBittorrent →403 Forbidden(bloqué, comme attendu) ; même requête depuisswag→200(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 deforgejoau tout début du démarrage derenovate(auto-résolues, run Renovate terminé avec succès juste après) — sans rapport avec ce fix. sshd_config:AllowUsers free debian→AllowUsers debian—freene correspond à aucun compte local (getent passwd free: absent). C'est un résidu de config (le nom coïncide avec le comptefreeutilisé côté serveur distant pour le montage sshfs hors-site debackup.sh, mais ce montage est sortant — ce serveur se connecte vers le distant en tant quefree, aucun rapport avec les connexions SSH entrantes gérées parAllowUsersici). Risque : dormant tant quefreen'existe pas localement, mais si un comptefreeétait recréé un jour sans qu'on y pense, il hériterait automatiquement de l'autorisation SSH. Atténué en plus parPasswordAuthentication no(clé obligatoire). Backup desshd_configfait avant modif, syntaxe validée (sshd -t), rechargé à chaud (systemctl reload sshd, pas de coupure de session).
Downloads
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
0 downloads
-
v1.2 Stable
released this
2026-06-27 03:44:08 +00:00 | 78 commits to main since this releaseAjouté
- 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
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
0 downloads
-
v1.1 Stable
released this
2026-06-27 03:44:08 +00:00 | 80 commits to main since this releaseAjouté
- 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é
.gitignoreaffiné : exclusionfail2ban.sqlite3,crowdsec/, secrets hCaptcha- Secrets Docker : ajout
hcaptcha_site_key,hcaptcha_secret_key,crowdsec_api_key,forgejo_git_token
Downloads
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
0 downloads
-
v1.0 Stable
released this
2026-06-27 03:44:08 +00:00 | 89 commits to main since this releaseStack 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.shavec rotation et copie hors-site
Downloads
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
0 downloads
-
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
-
v1.4 Stable
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
watchtoweretwatchtower-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
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
0 downloads
-
v1.3 Stable
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 delsparfind+sortpour la détection des archives (SC2012)
Downloads
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
0 downloads