Aller au contenu
Ilan Oudor Étudiant en Cybersécurité
Tous les articles
4 min de lecture

J'ai donné les clés de la machine à mon reverse proxy

Traefik, l'auto-discovery Docker, et une ligne de configuration recopiée de la doc officielle qui ouvrait une escalade de privilèges complète vers l'hôte.

DockerTraefikReverse proxy

Sur mon homelab, ma méthode était simple et un peu honteuse : un service, un port. Jellyfin sur 8096, Grafana sur 3000, et une redirection de port par service sur la box.

Ça marche. Ça ne passe pas à l'échelle, et surtout chaque port ouvert est un service exposé en direct, avec sa propre gestion du TLS (ou son absence) et ses propres failles.

Un reverse proxy inverse la logique : un seul point d'entrée en 443, qui lit le nom de domaine demandé et route vers le bon conteneur en interne.

Routage par nom de domaine à travers un point d'entrée unique Client navigateur 443 (TLS) Traefik routage par Host terminaison TLS vault.exemple.fr conteneur vaultwarden:80 docs.exemple.fr conteneur outline:3000 status.exemple.fr conteneur uptime-kuma:3001 Un seul port ouvert, quel que soit le nombre de services.
Traefik lit l'en-tête Host et route vers le bon conteneur, sans qu'aucun port applicatif ne soit publié.

Pourquoi Traefik et pas Nginx

Nginx aurait fait le boulot. Mais dans un environnement où les services sont des conteneurs qu'on déploie et redéploie sans arrêt, écrire un bloc server à la main à chaque fois devient pénible, et on finit par oublier d'en mettre un à jour.

Traefik lit sa configuration dans les labels des conteneurs. Le service déclare lui-même comment il veut être exposé :

services:
  outline:
    image: outlinewiki/outline
    networks: [proxy]
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.outline.rule=Host(`docs.exemple.fr`)"
      - "traefik.http.routers.outline.entrypoints=websecure"
      - "traefik.http.routers.outline.tls.certresolver=letsencrypt"
      - "traefik.http.services.outline.loadbalancer.server.port=3000"

Aucun port publié vers l'hôte, aucune ligne à toucher dans le pare-feu. Le conteneur rejoint le réseau proxy, Traefik le voit apparaître, et le service est en HTTPS.

C'est élégant. Et c'est là que je me suis planté.

La ligne du problème

Pour lire les labels, Traefik a besoin de parler au démon Docker. La doc officielle donne la solution évidente :

volumes:
  - /var/run/docker.sock:/var/run/docker.sock

Je l'ai copiée sans réfléchir. Ça marchait, je suis passé à la suite.

C'est l'encadrant qui a tiqué en relisant mon compose. Sa question : si quelqu'un exécute du code dans ce conteneur, il peut faire quoi ?

Je n'avais pas la réponse. On l'a cherchée ensemble, et elle est brutale. Le socket Docker donne un contrôle total sur l'hôte. Qui peut parler au démon Docker peut lancer un conteneur privilégié, monter le système de fichiers de la machine hôte, et sortir de l'isolation.

Autrement dit, j'avais donné les clés de la machine au seul composant directement exposé sur Internet. Une faille dans Traefik, et on passait de la DMZ à l'hôte en une commande.

La correction

Deux choses.

D'abord un proxy de socket. Au lieu du socket brut, Traefik parle à un intermédiaire qui ne laisse passer que les appels de lecture dont il a besoin :

socket-proxy:
  image: tecnativa/docker-socket-proxy
  environment:
    CONTAINERS: 1      # autorisé : lister et inspecter
    NETWORKS: 1
    POST: 0            # refusé : toute écriture
    EXEC: 0
    IMAGES: 0

Ensuite, deux instances de Traefik au lieu d'une. Une en DMZ, une en zone serveur. Celle qui est exposée sur Internet ne connaît que les conteneurs de la DMZ, et n'a aucune visibilité sur les services internes.

Ce que ça m'a appris

J'avais appliqué une configuration issue de la documentation officielle. Elle fonctionnait parfaitement. Et elle introduisait une escalade de privilèges complète.

La doc d'un outil explique comment le faire marcher. Elle explique rarement comment le faire marcher sans se tirer une balle dans le pied, parce que ce n'est pas son objectif. Son objectif, c'est que tu réussisses ton premier démarrage.

Depuis, j'ai un réflexe systématique : dès qu'un exemple de config comporte un montage de volume, une capacité ou un accès réseau particulier, je me demande ce qu'un attaquant en ferait s'il contrôlait le conteneur. Pour le socket Docker, la réponse est courte. Tout.

C'est aussi ce qui m'a poussé à scanner les images avec Trivy, mais j'en reparlerai.