Skip to content

Latest commit

 

History

History
402 lines (311 loc) · 17.6 KB

File metadata and controls

402 lines (311 loc) · 17.6 KB

docker-socket-proxy

English | Français

Docker Scout report

Proxy HTTP minimaliste et sécurisé devant le socket Docker. Les clients sont associés à un profil par leur adresse IP, découverte depuis leurs labels Docker ; tout accès qui n'est pas explicitement accordé est refusé.

Le projet permet de limiter les familles d'API Docker, puis de restreindre les opérations à certains conteneurs. Il évite ainsi de monter directement /var/run/docker.sock dans une application.

Images de production

cerede2000/docker-socket-proxy:latest
ghcr.io/cerede2000/docker-socket-proxy:latest

La première référence est publiée sur Docker Hub ; la seconde sur GitHub Container Registry. Les deux images sont multi-architecture (linux/amd64 et linux/arm64), construites avec Go et exécutées sans privilèges dans distroless/static-debian13:nonroot.

latest suit main. Chaque release Git vX.Y.Z publie également les tags Docker immuables X.Y.Z et X.Y sur les deux registres.

La branche integration publie uniquement le tag mutable integration. Elle ne remplace jamais latest ni un tag de release.

Notes de mise à niveau

La release 1.2.0 renforce plusieurs permissions et peut nécessiter une adaptation des profils. L'inspection d'un conteneur demande allow_inspect: true ; la création d'une session exec demande à la fois exec: true et post: true ; enfin, les profils à portée limitée ne peuvent plus effectuer d'écritures globales sur les images, volumes ou réseaux. Les options de profil CLI inconnues sont rejetées afin qu'une faute de frappe ne produise pas silencieusement une politique inattendue.

Consultez la procédure de migration complète dans CHANGELOG.md avant de remplacer une image 1.1.2 ou antérieure, puis épinglez le tag immuable 1.2.0 plutôt que latest.

L'image publiée est analysée en continu par Docker Scout. Le rapport est lié ici plutôt que figé dans le README : son résultat suit les mises à jour des vulnérabilités et de l'image.

Ce qui le différencie

Les socket proxies classiques limitent principalement les familles d'endpoints Docker. Celui-ci ajoute deux niveaux de contrôle complémentaires :

  1. Le droit est attribué au client par profil, automatiquement via son label Docker (socketproxy.role).
  2. Le droit est ensuite limité à la cible : tous les conteneurs, allowlist, blacklist, refus explicite, ou accès strictement en lecture seule par conteneur.

Un outil peut donc disposer d'un accès Docker étendu lorsque c'est nécessaire (Portainer, un opérateur), tandis que Traefik Manager peut uniquement consulter et redémarrer traefik. Le contrôle nom/ID est maintenu en cache et s'applique aux listes, événements et appels directs.

Principes de sécurité

  • Aucun droit n'est accordé implicitement.
  • Seuls les conteneurs portant socketproxy.role (ou l'alias socketproxy.service) et correspondant à un profil sont autorisés.
  • Le proxy ne retient que les IP partagées avec ses propres réseaux Docker.
  • Les listes, les événements et les opérations ciblant un conteneur respectent la même portée.
  • Le cache interne nom / ID de conteneur évite une requête Docker supplémentaire pour les vérifications usuelles.
  • Le contrôle local /version est accepté sans profil uniquement depuis l'interface loopback. N'utilisez pas le réseau hôte et ne publiez pas le port 2375.
  • Pour les profils à portée limitée, les événements Docker non liés à un conteneur sont volontairement omis car ils ne peuvent pas être rattachés sûrement à une cible autorisée.

Démarrage rapide

Créez un fichier profiles.yml, puis lancez le proxy. Le montage :ro empêche seulement de remplacer le fichier socket Unix ; il ne rend pas les appels à l'API Docker accessibles en lecture seule. La politique du proxy constitue la barrière de sécurité.

services:
  docker-socket-proxy:
    image: cerede2000/docker-socket-proxy:latest
    container_name: docker-socket-proxy
    user: "1000:998" # adapter à l'UID:GID pouvant lire le socket sur l'hôte
    read_only: true
    cap_drop: [ALL]
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./profiles.yml:/config/profiles.yml:ro
    networks:
      - socketproxy
    restart: unless-stopped

networks:
  socketproxy:
    internal: true

Le compte configuré avec user doit avoir accès au socket Docker de l'hôte. Vérifiez son UID et son GID avec stat -c '%u:%g' /var/run/docker.sock sous Linux, puis adaptez la valeur. Sur certaines installations, il est préférable de construire une image dérivée qui crée un groupe portant le GID réel du socket.

Associer un client à un profil

Ajoutez l'un des deux labels suivants au conteneur client :

labels:
  socketproxy.role: mon-profil
  # ou : socketproxy.service: mon-profil

Le profil est rechargé automatiquement lorsque profiles.yml est modifié. La découverte des IP intervient au démarrage, lors des événements Docker pertinents, et périodiquement.

Exemple complet : Traefik et Traefik Manager

Cet exemple sépare les rôles : Traefik peut lire les informations nécessaires au provider Docker ; Traefik Manager peut seulement consulter et redémarrer le conteneur traefik. Il ne peut ni agir sur les autres conteneurs ni lancer d'exec.

compose.yml :

services:
  docker-socket-proxy:
    image: cerede2000/docker-socket-proxy:latest
    container_name: docker-socket-proxy
    user: "1000:998" # à adapter à l'hôte
    read_only: true
    cap_drop: [ALL]
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./profiles.yml:/config/profiles.yml:ro
    networks: [socketproxy]
    restart: unless-stopped

  traefik:
    image: traefik:v3
    container_name: traefik
    command:
      - --providers.docker=true
      - --providers.docker.endpoint=tcp://docker-socket-proxy:2375
      - --providers.docker.exposedbydefault=false
    labels:
      socketproxy.role: traefik
    networks: [socketproxy, frontend]
    restart: unless-stopped

  traefik-manager:
    image: ghcr.io/chr0nzz/traefik-manager:latest
    container_name: traefik-manager
    environment:
      DOCKER_HOST: tcp://docker-socket-proxy:2375
      RESTART_METHOD: proxy
      TRAEFIK_CONTAINER: traefik
    labels:
      socketproxy.role: traefik-manager
    networks: [socketproxy, frontend]
    restart: unless-stopped

networks:
  socketproxy:
    internal: true
  frontend:
    external: true

profiles.yml :

traefik:
  ping: true
  version: true
  containers: true
  allow_inspect: true
  networks: true
  events: true
  session: true

traefik-manager:
  ping: true
  version: true
  containers: true
  allow_inspect: true
  post: true
  allow_restart: true
  container_scope: allowlist
  allowed_containers:
    - traefik

container_name: traefik et allowed_containers: [traefik] doivent correspondre exactement. Le profil traefik-manager ne permet pas start ou stop : seul POST /containers/traefik/restart est admis.

Configuration du proxy

Variable Défaut Description
DOCKER_SOCKET_PATH /var/run/docker.sock Chemin du socket Docker à joindre
PROXY_PORT 2375 Port d'écoute et port du healthcheck intégré
PROXY_LISTEN Adresse d'écoute complète ; prioritaire sur PROXY_PORT
SOCKETPROXY_PROFILE_FILE /config/profiles.yml Fichier YAML des profils
DISCOVER_INTERVAL 30s Période de redécouverte des conteneurs ; durée Go (15s) ou nombre de secondes (15)
EVENT_DEBOUNCE_DELAY 100ms Délai de regroupement des événements Docker ; durée Go ou nombre de millisecondes

Les arguments suivants sont disponibles et prioritaires sur les variables correspondantes : --listen, --socket, --profiles, --discover-interval et --debounce-delay.

Le healthcheck appelle http://127.0.0.1:$PROXY_PORT/version. Si --listen ou PROXY_LISTEN utilise un autre port, renseignez PROXY_PORT avec ce même port.

Options de ligne de commande par profil

Les profils peuvent aussi être définis dans la commande du conteneur. Le format est --<profil>.<option>=<valeur> ou --proxy-<profil>.<option>=<valeur>.

command:
  - --traefik.ping=1
  - --traefik.containers=1
  - --traefik-manager.container_scope=allowlist
  - --traefik-manager.allowed_containers=traefik

Les listes CLI acceptent des noms séparés par des virgules. container_rule accepte nom:deny ou nom:readonly. Le YAML reste préférable pour les configurations maintenues dans le temps.

Référence des profils

Chaque famille est désactivée par défaut. Une valeur YAML booléenne (true/false) est recommandée.

Option Famille d'API Docker autorisée
ping /_ping
version /version
info /info
events ou event /events
auth /auth
build /build
commit /commit
configs /configs
containers /containers (famille générale ; les sous-routes sensibles restent contrôlées séparément)
distribution /distribution
exec /exec
images /images
networks /networks
nodes /nodes
plugins /plugins
secrets /secrets
services /services
session /session
swarm /swarm
system /system
tasks /tasks
volumes /volumes

Les écritures génériques (POST, PUT, PATCH, DELETE) restent interdites même lorsqu'une famille est activée, sauf si post: true est ajouté. Les actions ciblées de cycle de vie sont indépendantes de ce droit large et peuvent être accordées avec post: false.

Option conteneur Route Nécessite post
allow_archive /containers/{id}/archive GET/HEAD : non ; PUT : oui
allow_changes /containers/{id}/changes non
allow_export /containers/{id}/export non
allow_inspect /containers/{id}/json non
allow_logs /containers/{id}/logs non
allow_top /containers/{id}/top non
allow_start /containers/{id}/start non
allow_stop /containers/{id}/stop non
allow_restart /containers/{id}/restart non
allow_pause /containers/{id}/pause non
allow_unpause /containers/{id}/unpause non
allow_kill /containers/{id}/kill non

Toutes ces options valent false par défaut. allow_restarts reste un alias de allow_restart ; contrairement au commutateur groupé de LinuxServer, il n'accorde pas implicitement stop ou kill. Ces droits doivent être explicitement ajoutés.

La création d'une session exec via POST /containers/{id}/exec exige les trois droits explicites containers: true, exec: true et post: true. allow_all n'active jamais exec.

GET /containers/{id}/stats reste volontairement inclus dans le droit général de lecture containers. Cette route expose la télémétrie d'exécution, respecte la portée de conteneurs configurée et ne possède pas de commutateur allow_stats distinct.

allow_all: true est un raccourci groupé mais limité à la portée pour toutes les options allow_* du tableau. Ce n'est volontairement pas un droit Docker global : il n'active ni containers, ni exec, ni post, ni une autre famille d'API et ne contourne pas les portées de conteneurs. L'envoi d'une archive et les autres écritures génériques nécessitent donc toujours post: true. Traitez-le comme un droit à fort impact : export peut lire tout le système de fichiers du conteneur et la lecture d'archive peut exposer n'importe quel fichier de la cible.

Exemple minimal limité au cycle de vie :

container-operator:
  ping: true
  version: true
  containers: true
  post: false
  allow_start: true
  allow_stop: true
  allow_restart: true
  allow_pause: true
  allow_unpause: true

Tous les contrôles ciblés des conteneurs, sans activer les autres familles Docker :

container-manager:
  containers: true
  post: true
  allow_all: true

apirewrite force une version d'API Docker pour un profil, par exemple apirewrite: "1.53".

Portée des conteneurs

Les noms sont les noms Docker sans le préfixe /. Les règles s'appliquent aux listes, événements, inspections, logs, statistiques, exec, opérations réseau et actions ciblées.

Limites de la portée

La portée conteneur s'applique uniquement lorsqu'une requête Docker peut être rattachée à un conteneur. Pour un profil limité, les opérations globales sur les conteneurs (create, prune) et les écritures destructrices sur les images, volumes ou réseaux non ciblés sont refusées. Les lectures des familles globales images, volumes et networks ne sont pas filtrées par conteneur. Évitez d'accorder ces familles avec post: true sauf si le client administre réellement tout l'hôte.

Accès large : all

all est la valeur par défaut. Le profil conserve ses droits sur tous les conteneurs ; utilisez une règle deny pour retirer une cible critique.

portainer:
  ping: true
  version: true
  containers: true
  allow_inspect: true
  images: true
  networks: true
  post: true
  allow_start: true
  allow_stop: true
  allow_restart: true
  container_scope: all
  container_rules:
    - name: docker-socket-proxy
      access: deny

Accès minimal : allowlist

Les conteneurs absents de allowed_containers sont invisibles et inaccessibles.

traefik-manager:
  containers: true
  allow_inspect: true
  post: true
  allow_restart: true
  container_scope: allowlist
  allowed_containers:
    - traefik

Accès large avec exclusions : blacklist

Les conteneurs de blocked_containers sont invisibles et toute opération les visant est refusée.

dockhand:
  ping: true
  containers: true
  allow_inspect: true
  events: true
  post: true
  allow_start: true
  allow_stop: true
  allow_restart: true
  container_scope: blacklist
  blocked_containers:
    - docker-socket-proxy

Exceptions par conteneur : container_rules

container_rules est prioritaire sur la portée. deny masque totalement la cible. readonly laisse visibles les listes et événements et autorise uniquement inspect, logs, stats, top et changes ; les actions, exec, les archives et attach sont refusés.

dockhand:
  containers: true
  allow_inspect: true
  events: true
  post: true
  allow_start: true
  allow_stop: true
  allow_restart: true
  container_scope: blacklist
  blocked_containers:
    - docker-socket-proxy
  container_rules:
    - name: dockman
      access: readonly

Une cible ne peut pas figurer à la fois dans blocked_containers et container_rules. Les valeurs d'accès admises sont deny et readonly. container_scope: all ne peut pas contenir de liste blanche ou noire ; allowlist ne peut pas contenir blocked_containers et blacklist ne peut pas contenir allowed_containers.

Lorsqu'une portée est active (allowlist, blacklist ou une règle nominative), les opérations globales create et prune sont refusées afin de ne pas contourner la restriction par conteneur.

Exploitation

Le proxy écrit dans ses journaux la découverte des rôles et les refus. Un client sans rôle, avec un rôle inconnu, ou ne partageant aucun réseau avec le proxy reçoit 403 Forbidden.

Réseau et TLS

Le proxy est conçu pour une communication locale au moteur Docker :

client Docker -- HTTP privé --> docker-socket-proxy -- socket Unix --> dockerd
  • La liaison entre le proxy et Docker utilise DOCKER_SOCKET_PATH (par défaut /var/run/docker.sock). Elle ne traverse pas le réseau et n'utilise donc pas TLS, certificat ou autorité de certification (CA).
  • Le proxy n'ouvre pas de connexion HTTPS sortante et ne prend pas en charge un moteur Docker distant configuré par DOCKER_HOST=tcp://….
  • Le port 2375 est volontairement en HTTP clair : il doit rester accessible uniquement depuis un réseau Docker interne, partagé avec les clients autorisés. N'ajoutez pas de ports: et ne l'exposez jamais par Traefik, un load balancer ou Internet.
  • internal: true réduit l'exposition du réseau, mais tout conteneur qui y est attaché reste un client potentiel : n'y raccordez que le proxy et les services qui ont réellement besoin de l'API Docker.

Cette approche est la même que celle des proxies de référence Tecnativa et LinuxServer : le contrôle d'accès réseau et le filtrage d'API remplacent une terminaison TLS sur un port qui ne doit pas être publié. Si un besoin inter-hôtes apparaît, déployez un proxy local par hôte plutôt que d'étendre ce port : l'association client/profil de ce projet repose sur les réseaux Docker locaux.

La runtime distroless/static-debian13:nonroot est adaptée à ce modèle : le binaire Go est compilé avec CGO_ENABLED=0, sans dépendance à glibc, OpenSSL ni magasin de CA. Ajouter des CA ne renforcerait pas cette configuration ; elles ne deviendraient nécessaires qu'avec une future fonctionnalité HTTPS sortante ou mTLS.

Développement

go test -race ./...
go vet ./...

Licence

Distribué sous licence MIT.