Aller au contenu
Conteneurs & Orchestration medium

Sécuriser l'accès au socket Docker avec un socket-proxy

9 min de lecture

logo docker

Beaucoup d'outils réclament /var/run/docker.sock : superviseurs, reverse-proxies à découverte automatique, dashboards, runners de CI. Or donner le socket, c'est donner un accès root à l'hôte. Le réflexe courant, monter le socket en :ro, ne protège pas : le :ro porte sur le fichier, pas sur l'API, et les écritures passent quand même. La vraie solution est un docker-socket-proxy qui filtre l'API et n'autorise que la lecture.

Ce guide déploie ce proxy, y branche un outil via DOCKER_HOST, prouve que l'écriture est bloquée, puis montre comment n'ouvrir que le strict nécessaire. Public : intermédiaire à avancé. Les commandes sont testées.

  • Comprendre pourquoi docker.sock:ro n'empêche pas les écritures.
  • Déployer un docker-socket-proxy en lecture seule.
  • Brancher un outil dessus via DOCKER_HOST.
  • Restreindre l'accès aux endpoints strictement nécessaires.

Le socket Docker est un canal de communication avec le démon, qui tourne en root. On s'y connecte et on échange des requêtes HTTP : un POST /containers/{id}/stop emprunte le même canal qu'un GET /containers/json. Le flag :ro empêche seulement de remplacer le fichier socket, il n'inspecte pas le contenu des requêtes.

La preuve : montons le socket en :ro et tentons une écriture.

Fenêtre de terminal
docker run -d --name cible nginx:alpine
CID=$(docker inspect -f '{{.Id}}' cible)
# POST d'arrêt, à travers un socket monté en :ro
docker run --rm --user 0 -v /var/run/docker.sock:/var/run/docker.sock:ro \
curlimages/curl -s -o /dev/null -w "%{http_code}\n" \
-X POST --unix-socket /var/run/docker.sock "http://localhost/v1.44/containers/$CID/stop"
# 204
docker ps -a --format '{{.Names}} {{.Status}}' | grep cible
# cible Exited (0)

HTTP 204, conteneur arrêté à travers un socket monté en read-only. Le :ro n'a rien empêché : tout outil ayant le socket monté a le pouvoir d'agir, :ro ou non.

Il faut donc un composant qui inspecte l'API et refuse les endpoints d'écriture. C'est exactement le rôle du proxy.

Le proxy le plus répandu est tecnativa/docker-socket-proxy : il se place devant le socket, expose un endpoint TCP, active uniquement les groupes d'endpoints que vous déclarez, et refuse tout POST par défaut.

  1. Déclarer le proxy en n'activant que les groupes de lecture utiles. Ici de quoi alimenter un outil de monitoring ou une découverte de conteneurs.

    services:
    socketproxy:
    image: tecnativa/docker-socket-proxy:latest
    environment:
    - CONTAINERS=1
    - INFO=1
    - VERSION=1
    - EVENTS=1
    - IMAGES=1
    - NETWORKS=1
    - PING=1
    # POST non défini => 0 => toute écriture renvoie 403
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    networks: [proxynet]
    restart: unless-stopped
    networks: { proxynet: {} }
  2. Démarrer et vérifier que la lecture répond.

    Fenêtre de terminal
    docker compose up -d socketproxy
    docker compose exec socketproxy wget -qO- http://localhost:2375/version | head -c 80
    # {"Platform":{"Name":"Docker Engine..."} -> lecture OK

L'outil ne monte plus le socket : il pointe sur le proxy via DOCKER_HOST. La plupart des outils Docker (SDK Go/Python) honorent cette variable.

monitoring:
image: votre/outil:latest
environment:
- DOCKER_HOST=tcp://socketproxy:2375 # jamais le vrai socket
networks: [proxynet]
depends_on: [socketproxy]

L'outil découvre alors les conteneurs à travers le proxy, sans jamais accéder au socket réel. J'ai vérifié avec un superviseur : l'auto-découverte de la stack fonctionne à l'identique.

Le test qui compte : la lecture passe, l'écriture est refusée.

Fenêtre de terminal
# Lecture : autorisée
docker compose exec socketproxy wget -qO- -S http://localhost:2375/version 2>&1 | grep HTTP
# HTTP/1.1 200 OK
# Écriture : refusée
CID=$(docker inspect -f '{{.Id}}' un-conteneur)
docker compose exec socketproxy \
wget -qO- -S --post-data='' "http://localhost:2375/containers/$CID/stop" 2>&1 | grep HTTP
# HTTP/1.1 403 Forbidden

200 en lecture, 403 en écriture. Le read-only n'est plus une promesse de l'outil, c'est une règle imposée par le proxy. Même si l'outil était compromis, il ne pourrait pas agir sur vos conteneurs.

Chaque variable active un groupe d'endpoints. Le principe de moindre privilège impose de n'activer que ce dont l'outil a réellement besoin.

VariableOuvrePour quel usage
CONTAINERSliste et inspection des conteneursmonitoring, découverte
IMAGESliste des imagesdétection de mises à jour
NETWORKSréseauxdécouverte (Traefik)
EVENTSflux d'évènementsréaction temps réel
INFO, VERSION, PINGmétadonnées et santébase

Le proxy, lui, monte le vrai socket : c'est le composant de confiance, root-equivalent. Il faut donc le traiter comme tel.

  • Réseau dédié : placez le proxy et ses clients sur un réseau interne (proxynet), sans port publié sur l'hôte. Le 2375 ne doit jamais être exposé au réseau.
  • Durcissement : read_only: true, security_opt: [no-new-privileges:true], cap_drop: [ALL] sur le conteneur proxy.
  • Un proxy par besoin : plutôt qu'un proxy tout-ouvert partagé, un proxy restreint par outil limite le rayon d'impact.
  • Proxy durci : wollomatic/socket-proxy va plus loin (filtrage par regex des chemins, TLS mutuel, allowlist stricte). À considérer pour un usage sensible.
  • Mode rootless : le Docker rootless déplace le démon hors de root, ce qui réduit l'enjeu à la source. Complémentaire du proxy.
  • L'outil ne voit rien : le groupe d'endpoints nécessaire n'est pas activé (souvent CONTAINERS=1 manquant), ou DOCKER_HOST ne pointe pas sur le proxy.
  • 403 sur une action légitime : l'outil tente une écriture ; décidez si vous l'autorisez via un ALLOW_* précis, ou si l'action n'a rien à faire là.
  • DOCKER_HOST ignoré : certains outils utilisent un chemin de socket codé en dur ; vérifiez leur doc, beaucoup acceptent un tcp:// ou un DOCKER_GID.
  • Le proxy ne démarre pas : il lui faut le vrai socket monté (/var/run/docker.sock:ro) et le réseau partagé avec ses clients.
  • docker.sock:ro n'empêche aucune écriture : le :ro porte sur le fichier, pas sur l'API.
  • Un docker-socket-proxy filtre l'API : GET autorisés, POST en 403.
  • Les outils se branchent via DOCKER_HOST=tcp://socketproxy:2375, sans le vrai socket.
  • N'activez que les groupes d'endpoints nécessaires ; POST/ALLOW_* rouvrent l'écriture.
  • Le proxy est le composant de confiance : réseau interne, pas de port publié, conteneur durci.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn