La stack, en détail

Tout est infrastructure-as-code dans un seul dépôt : cluster/ pour les briques transverses, apps/ pour les applications. Un git clone + un make reconstruisent l'ensemble.

L'architecture

Trois Raspberry Pi à la maison, une bordure qui n'accepte rien d'entrant, et le reste organisé en couches. Ce qui n'est pas dans le cluster y est autant à sa place que ce qui y est : un résolveur DNS et un coffre de secrets ne peuvent pas dépendre de la chose qu'ils servent.

Architecture de l’infrastructure zebbox.net Le trafic public arrive par une bordure qui termine le TLS et filtre, puis descend dans un tunnel établi vers l’extérieur par la maison : aucun port n’est ouvert. À la maison, trois Raspberry Pi forment un cluster de trois managers, organisé en couches : ingress actif/actif, identité, applications flottantes, bases répliquées à bascule automatique, observabilité et sauvegarde. À côté du cluster : un stockage objet distant pour le site et les sauvegardes chiffrées, un coffre de secrets jamais lu à l’exécution, une CI publique sans clé d’écriture, et le DNS du réseau local, délibérément hébergé hors du cluster. INTERNET PUBLIC visiteurs, moteurs — et le bruit de fond des scanners HTTPS LA BORDURE TLS terminé ici filtrage bots + WAF sortie du tunnel tunnel sortant c’est la maison qui appelle : aucun port ouvert, aucune adresse à découvrir côté box À LA MAISON — TROIS MANAGERS, QUORUM 2 SUR 3 rpi4 rpi51 rpi52 perdre un nœud ne coupe ni le site, ni les données — six exercices l’ont vérifié Ingress actif / actif tunnel ×2 reverse-proxy ×3 pages d’erreur IPS + WAF Identité devant le privé SSO + 2FA ×2 sessions répliquées OIDC pour les apps Applications toutes flottantes vitrine app métier automatisation jeu multi-mondes Données bascule automatique 2 membres par base routeur de leader élection (quorum 3) Observabilité les trois, reliées métriques logs traces alertes push Sauvegarde chiffrée, hors-site agents de snapshot dumps applicatifs contrôle quotidien les snapshots partent chiffrés vers le stockage objet — la clé ne quitte jamais la maison AILLEURS ce qui, délibérément, ne vit pas dans le cluster Stockage objet la vitrine que vous lisez,les logs et les traces,les sauvegardes chiffrées Coffre de secrets la source de vérité, renduepar stack au déploiement —jamais lue à l’exécution CI publique construit le site,ne détient aucune cléd’écriture DNS du réseau local hors cluster, par principe :un résolveur ne peut pasdépendre du cluster qu’il sert.Géré ici, hébergé ailleurs. Ce schéma dit l’architecture, pas la cible : ni adresse, ni port, ni version, ni nom d’hôte d’administration.
Les briques sont nommées par leur rôle ; la liste des logiciels est juste en dessous. Savoir ce qui tourne ici n’ouvre aucune porte — savoir et sur quel port, si.

Le chemin d'une requête

La même infrastructure, vue depuis une requête qui la traverse — et les trois endroits où elle peut s'arrêter avant d'atteindre l'application.

Le chemin d’une requête à travers l’infrastructure Une requête HTTPS est reçue par la bordure Cloudflare qui termine le TLS et filtre le trafic, puis descend par le tunnel jusqu’au point d’entrée du cluster. Elle traverse ensuite, dans cet ordre : la pose des en-têtes de sécurité, la couche de pages d’erreur, le filtrage IPS et WAF, le rétablissement du schéma https, l’authentification unique avec second facteur si l’application est privée, le réveil de l’application si elle dort, puis l’application elle-même. Trois sorties anticipées : adresse bannie, visiteur non authentifié — l’application n’est alors pas réveillée — et application endormie qui affiche une page d’attente. Requête HTTPS un visiteur, un moteur, ou un scanner 01 Bordure : TLS, WAF, anti-bot le certificat est présenté ici 02 Tunnel → point d’entrée établi depuis la maison, jamais vers elle 03 En-têtes de sécurité posés en premier — voir la note 04 Pages d’erreur remplacent la réponse en cas de 4xx / 5xx 05 IPS + WAF réputation d’adresse, puis règles WAF 06 Schéma réel rétabli l’app apprend qu’on est bien en https 07 SSO + 2FA si l’application est privée 08 Réveil si l’application dort à zéro réplique 09 L’application enfin 10 Adresse bannie, requête hostile coupé ici, et la réponse resteune page soignée — pas une tracede pile Pas authentifié renvoyé au portail. L’applicationn’est même pas réveillée : on nedépense rien pour un inconnu Application endormie page d’attente, puis réveilet rejeu de la requête Le TLS s’arrête à la bordure le saut suivant vit sur le réseauprivé du cluster — d’où un HSTSexplicitement forcé, sinon perdu Pourquoi si tôt ? la couche d’erreur, juste après,REMPLACE la réponse — en-têtescompris. Posés après elle, ilsdisparaîtraient de chaque paged’erreur. Posés avant, ilssurvivent à tout. L’ordre n’est pas décoratif : c’est lui qui décide de ce qui survit à une erreur, et de ce qu’on refuse de dépenser pour un inconnu.

Les choix qui comptent

Zéro port ouvert

Aucune redirection de port sur la box : tout le trafic entrant passe par un tunnel sortant Cloudflare. La surface d'attaque réseau côté maison est nulle.

Scale-to-zero

Les apps peu sollicitées descendent à 0 réplica et se réveillent à la première requête autorisée. RAM économisée sur du matériel modeste, avec une page d'attente soignée.

SSO d'abord, réveil ensuite

Authelia s'exécute avant Sablier : une requête non authentifiée est renvoyée au portail sans même réveiller l'application. On ne dépense des ressources que pour de vrais utilisateurs.

Reconstruisible

Tout est versionné et déployable en ssh + docker stack deploy, sans dépendre d'une UI. Un plan de reprise existe pour repartir d'un dépôt git et des sauvegardes.

Déploiement continu, secrets cloisonnés

Cette vitrine se reconstruit sur git push : GitHub Actions build, puis n8n déploie vers R2 et vide le cache — sans coupure (rolling). Les clés d'écriture R2 restent côté homelab (Docker secrets), jamais sur GitHub.

On ne s'alerte pas de sa propre panne

Tunnel, bases, services : tout part en notification push si ça défaille. Mais pas d'alerte « le canal de notif est mort » — elle transiterait par ce même canal. Un système ne surveille pas sa propre alerte ; c'est le rôle d'un veilleur externe. On préfère connaître ses angles morts plutôt que s'offrir une fausse assurance.

Le résolveur ne vit pas dans le cluster qu'il sert

Le DNS du LAN (AdGuard) est géré par le cluster — config versionnée, métriques, alertes, accès SSO — mais il tourne dehors, sur sa propre machine. Le mettre dans le Swarm créerait une dépendance circulaire : un déplacement de nœud couperait le DNS de toute la maison, et donc les outils pour le réparer. On intègre la gestion, pas le moteur.

Pas de point unique de défaillance

Les 3 Pi sont managers (quorum raft 2/3), l'ingress tourne en actif/actif, les bases sont en cluster Patroni (failover automatique) et chaque app flotte entre les nœuds — jusqu'à la couche d'authentification (Authelia + sessions) répliquée. Perdre un nœud — à chaud ou par crash — ne coupe ni le site, ni le pilotage, ni les données : validé par une matrice de 6 drills (sortie propre + crash sur chacun des 3 nœuds). Les Pi restent nomades, déplaçables un par un.

La source de vérité des secrets ne vit pas dans le cluster

Bitwarden Secrets Manager est le master : chaque stack reçoit ses secrets sous forme de fichier rendu, le contrat (les noms, jamais les valeurs) est versionné en git. Le runtime ne dépend jamais de Bitwarden — seuls la rotation et le disaster recovery y touchent. Perdre le disque du manager, c'est un make secrets-render ; faire tourner une clé, trois commandes.

Le matériel

Le détail des briques

Orchestration

Docker Swarm3 nœuds Pi (NVMe), tous managers — quorum raft 2/3, tolère la perte d'un nœud
PortainerUI + API (pilotée par le déploiement)

Ingress / réseau

Traefikv3 — actif/actif sur les 3 managers ; reverse-proxy, métriques + traces OTLP
Cloudflare Tunnelexposition publique, 0 port ouvert — cloudflared en HA
Sablierscale-to-zero + page d'attente
error-pagespages 4xx/5xx soignées, globales
AdGuard HomeDNS du LAN (hors-cluster, par principe) — filtrage pub/malware, DoH chiffré ; piloté en config-as-code, observé, en SSO

Sécurité / identité

CrowdSecIPS + WAF (CRS) — réputation d'IP + AppSec
AutheliaSSO + 2FA (forwardAuth & OIDC) — entièrement HA : 2 répliques + sessions Valkey Sentinel
Vaultwardengestionnaire de mots de passe (Postgres HA, clé en secret)

Applications

Stickers Managerl'app métier (nginx + php-fpm)
nginx-s3-gatewaysert cette vitrine depuis un bucket R2
n8nautomatisation — pilote le déploiement continu
OR_AI (oria)jeu 4X spatial multi-mondes — un conteneur par monde, état en mémoire + boucle temps-réel

Données / sauvegarde

PostgreSQL + Patroniune base dédiée par app, chacune en cluster 2 nœuds — failover automatique (etcd + HAProxy)
restic → Cloudflare R2backups chiffrés hors-site, 2 agents
Backrestorchestration des sauvegardes

Secrets

Bitwarden Secrets Managerla source de vérité (~70 secrets) — plus aucun mot de passe en clair dans les .env
bws + secrets-sync.shrendu par stack en .env.secrets (0600) ; rotation = 3 commandes ; comparaisons md5, jamais une valeur affichée
Docker secretsles credentials runtime (clés RSA, tokens de jobs) — mirrorés dans Bitwarden pour la reprise

Observabilité

Prometheusmétriques cluster, apps & bases — un exporter par cible
Loki + Alloyagrégation des logs (global shipper)
Tempo + OTel Collectortraces distribuées (service map)
Grafanadashboards par service (n8n, Postgres, tunnel…) + suivi des releases + alertes + pont métriques↔logs↔traces
ntfynotifications push — alertes Grafana, déploiements & nouvelles releases

CI/CD & IaC

GitHub Actionsbuild du site → artefact (jamais de clé R2)
n8n + Docker secretsdéploiement vers R2 + flush, secrets côté homelab
Makefile + scriptsssh + docker stack deploy ; secrets rendus depuis Bitwarden (make secrets-render)
Gittout versionné — github.com/zebby76/zebbox.net

Le code de cette infrastructure est public : github.com/zebby76/zebbox.net