Aller au contenu
Infrastructure

Ce qui tourne chez moi

J’héberge chez moi l’essentiel de ce que j’utilise au quotidien. Ce n’est pas une vitrine : c’est ce qui tourne, avec les compromis que ça suppose. Cette page dit quoi, sur quoi, comment j’en garde la porte, et ce que les pannes m’ont appris.

La forme générale

La forme générale
DMZ
frontendCaddy · CrowdSec · TLS
Serveur
dockerdouze services applicatifs
downloaderzone isolée
Management
pvehyperviseur Proxmox
Storage
nassauvegardes Synology
Un hôte Proxmox, trois machines virtuelles aux rôles séparés, un NAS Synology pour les sauvegardes. Des VLAN séparent les usages, plus une zone Agents isolée conservée sans machine active. Un réseau maillé Headscale relie le tout sans rien exposer au passage.

La porte d’entrée

Tout ce qui vient d’Internet passe par une seule machine, frontend, et par un seul processus : Caddy. Il termine le TLS, renouvelle les certificats et relaie vers les services, qui n’ont aucune interface publique.

Devant lui, CrowdSec, en trois morceaux qui font trois choses distinctes :

ComposantCe qu’il fait
Moteur de détectionlit les journaux de Caddy et qualifie les comportements
Bouncer pare-feuapplique les décisions au niveau réseau, avant Caddy
AppSec / WAFinspecte le contenu des requêtes, pas seulement l’adresse d’origine
Le trajet d’une requêteTrajet d’une requête à travers la façadeUne requête venue d’Internet traverse successivement le bouncer pare-feu, Caddy qui termine le TLS, le filtre de périmètre qui répond 404 aux requêtes externes visant un service interne, l’inspection de contenu AppSec, puis la pose des en-têtes de sécurité avant le relais vers le service. En retour, les journaux de Caddy alimentent le moteur de détection CrowdSec, dont les décisions reviennent au bouncer : ce n’est pas une chaîne de filtres, c’est une boucle.InternetBouncer pare-feuécarte les adresses déjà sanctionnéesCaddytermine le TLS, relaieFiltre de périmètre404 si la requête vient de l’extérieurAppSec / WAFinspecte le contenu de la requêteEn-têtes de sécuritéposés avant le relaisLe serviceaucune interface publiquejournaux → moteur → décisions
Le schéma se lit de haut en bas, sauf la flèche qui remonte : c’est elle qui compte. Les journaux de Caddy alimentent le moteur de détection, dont les décisions reviennent alimenter le bouncer. Ce n’est pas une chaîne de filtres, c’est une boucle — ce qu’une requête a tenté hier change ce qui lui est permis aujourd’hui.

La distinction entre les deux derniers est celle qui m’a décidé. Un bouncer d’adresses bloque un attaquant connu ; il ne voit rien d’une requête d’apparence banale envoyée par une adresse propre. L’AppSec regarde ce que la requête demande. Les deux se complètent, aucun ne remplace l’autre.

Une seule instance de détection tourne, sur frontend. L’interface web hébergée sur docker consomme cette instance par une identité machine dédiée, plutôt que d’en démarrer une seconde : deux moteurs, ce serait deux jeux de règles à tenir à jour et deux occasions de croire qu’on est protégé par celui qui ne tourne plus.

Trois provenances, deux politiques

Le même Caddy sert trois publics, et c’est l’adresse d’origine de la requête qui décide de ce qu’elle a le droit de voir.

ProvenanceCe qui est joignable
Internetles seuls services publiés volontairement : ce site, les webhooks, les notifications derrière authentification
Réseau localtout, y compris les interfaces d’administration
Maillage Headscalemême chose que le réseau local, depuis n’importe où

Trois provenances, mais deux politiques seulement : le maillage n’est pas un troisième niveau, c’est le réseau local étendu à mes appareils où qu’ils soient. C’est tout l’intérêt d’un VPN maillé — il déplace la frontière au lieu d’ouvrir une porte de plus. Il n’y a donc aucun service exposé « juste pour y accéder de l’extérieur ».

Le filtre tient en un fragment de configuration, importé par chaque service interne :

(internal_only) {
    @public not remote_ip private_ranges 100.64.0.0/10
    respond @public 404
}

Le code retourné est un 404, pas un 403. Un 403 confirmerait que la ressource existe et qu’elle est protégée ; un 404 dit qu’il n’y a rien à cette adresse. La différence ne change rien pour moi et retire à un visiteur non invité la seule information qu’il aurait obtenue gratuitement : la liste de ce qui mérite d’être protégé.

Un certificat qui ne dit pas ce qu’il couvre

Répondre 404 depuis Internet ne sert à rien si la liste des services fuit ailleurs — et elle fuit par un canal auquel on ne pense pas : les journaux deCertificate Transparency. Chaque certificat TLS émis par une autorité publique y est inscrit, avec les noms qu’il couvre, et ces journaux sont consultables par n’importe qui. Demander un certificat pourun-service.exemple.net revient donc à publier l’existence deun-service, quand bien même il répondrait 404 à tout le monde.

D’où deux certificats en joker, un par domaine, plutôt qu’un par service. Ce qui paraît dans les journaux publics est *.exemple.net : le nombre de sous-domaines n’y figure pas, ni leurs noms.

Un joker ne s’obtient pas de la même manière qu’un certificat ordinaire. La vérification par HTTP suppose que l’autorité joigne le nom demandé — ce qui est impossible pour un joker, et ce qui exigerait d’exposer chaque service le temps du renouvellement. Il faut donc prouver qu’on contrôle le domaine par le DNS : l’autorité demande de publier un enregistrement précis dans la zone, et Caddy le crée par l’API du registrar, puis le retire.

C’est le seul point où l’infrastructure dépend d’un service extérieur, et il demande une clé d’API qui peut écrire dans la zone DNS — donc une clé qui vaut le domaine entier si elle fuit. Le compromis est assumé : sans elle, chaque nom de service serait publié à chaque renouvellement, tous les deux mois.

Ce que le Caddyfile ne répète pas

La configuration est écrite en fragments réutilisables plutôt qu’en blocs recopiés. Un service interne se déclare en trois lignes qui disent son intention :

import internal_only
import public_security_headers
reverse_proxy …

L’intérêt n’est pas d’écrire moins. Il est qu’une correction se fasse à un seul endroit. Quand j’ai durci les en-têtes de sécurité, tous les services publiés en ont bénéficié le même jour, sans que j’aie à me souvenir de la liste — et c’est précisément la liste qu’on oublie.

public_security_headers fait deux choses de nature différente. Il ajoute ce qu’un navigateur doit savoir : HSTS,nosniff, politique de référent, cadrage, restrictions de permissions. Et il retire Server, Via etX-Powered-By — trois en-têtes qui annoncent le logiciel et sa version à qui les demande. Ne rien dire sur ce qu’on fait tourner ne protège de rien à soi seul ; ça évite simplement de publier de quoi cibler.

La politique de sécurité du contenu, elle, reste par site : ce qu’une page a le droit de charger dépend de la page. Ce site-ci a la sienne, plus stricte, dont je raconte plus bas ce qu’elle m’a coûté.

Ce qui tourne

Sur la VM docker :

ServiceRôle
Homepagetableau de bord d’accueil
Paperless-ngxgestion documentaire
Homeboxinventaire personnel
FreshRSSlecteur de flux
Kavitabibliothèque numérique
Actual Budgetbudget personnel
Dockhandgestion des piles Docker
Uptime Kumasupervision et alertes
Prometheus + Grafanamétriques et tableaux de bord
CrowdSec (interface web)consulte le LAPI hébergé sur frontend
n8nautomatisation de traitements personnels
ntfynotifications push auto-hébergées

Ce qui m’a mordu

Ce que ça m’a appris

La partie la plus utile de cette infrastructure n’est pas ce qui tourne, mais ce qui signale quand ça s’arrête : une sonde qui détecte sans alerter, ou une alerte qui part sans jamais arriver, produit exactement le même silence qu’une panne inexistante.