Docker socket in sicurezza: proxy, rootless e privilegi minimi
Come ridurre il rischio di /var/run/docker.sock con un proxy a privilegi minimi, Docker rootless, segmentazione e controlli operativi verificabili.
Montare /var/run/docker.sock dentro un container è una scorciatoia comune. Portainer, reverse proxy, strumenti di aggiornamento e dashboard possono così rilevare o amministrare gli altri workload. Il problema è che quel socket non è una normale API applicativa: chi lo controlla può chiedere al daemon di creare container privilegiati, montare il filesystem dell'host e, di fatto, ottenere il controllo della macchina.
In un homelab il rischio viene spesso accettato perché l'ambiente è piccolo. È proprio qui che conviene intervenire: una singola istanza compromessa può contenere password, backup, DNS interno e accessi VPN. L'obiettivo non è eliminare Docker, ma ridurre superficie esposta e conseguenze di un incidente.
Perché il socket equivale quasi a root
Il daemon Docker tradizionale viene eseguito con privilegi elevati. Un client autorizzato può creare un container con una bind mount di /, avviarlo con privilegi estesi oppure leggere variabili e configurazioni di altri container. Essere nel gruppo docker sull'host comporta quindi privilegi assimilabili a root.
La prima regola è semplice: non montare il socket se l'applicazione non ne ha una necessità documentata. Un servizio web, un database o un'applicazione multimediale non dovrebbero vederlo. Anche il mount in sola lettura non risolve il problema: :ro limita il file nel namespace del container, ma non rende di sola lettura le operazioni accettate dall'API Docker.
Per individuare i mount esistenti si può usare:
docker inspect $(docker ps -q) --format '{{.Name}} {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}{{.Source}} -> {{.Destination}}{{end}}{{end}}'
Il risultato va confrontato con la configurazione Compose e con la funzione reale di ogni servizio.
Opzione 1: eliminare l'accesso
È la misura migliore. Molti container richiedono il socket solo per funzionalità accessorie, come la scoperta automatica o il riavvio da interfaccia. Si può sostituire l'autodiscovery con una configurazione statica, delegare il deployment alla pipeline CI oppure eseguire comandi amministrativi via SSH da una macchina controllata.
Per gli aggiornamenti automatici bisogna valutare anche il rischio operativo: aggiornare indiscriminatamente immagini in produzione può introdurre regressioni. Una pipeline che esegue pull, test di salute e rollback è spesso più sicura di un container con controllo permanente del daemon.
Opzione 2: interporre un socket proxy
Quando l'API è indispensabile, un proxy permette di esporre solo gruppi di endpoint. Il progetto Tecnativa/docker-socket-proxy usa HAProxy e variabili d'ambiente per autorizzare operazioni come lettura di container, reti o eventi. Il socket reale resta montato soltanto nel proxy; l'applicazione usa una connessione TCP sulla rete Docker privata.
Un esempio Compose per un servizio che deve osservare i container, ma non crearli o cancellarli, è:
services:
socket-proxy:
image: tecnativa/docker-socket-proxy:latest
restart: unless-stopped
environment:
CONTAINERS: 1
EVENTS: 1
INFO: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- docker-api
monitor:
image: example/monitor:stable
environment:
DOCKER_HOST: tcp://socket-proxy:2375
networks:
- docker-api
depends_on:
- socket-proxy
networks:
docker-api:
internal: true
La rete internal impedisce l'esposizione diretta del proxy verso la LAN. Non bisogna pubblicare la porta 2375 sull'host: è traffico non cifrato e, se il proxy fosse configurato male, diventerebbe un punto di ingresso critico.
Il principio è negare tutto e abilitare soltanto ciò che compare nei log come necessario. POST=0 è importante per i client di sola osservazione, perché blocca in modo generale le richieste che modificano lo stato. Alcune applicazioni richiedono però endpoint più potenti: prima di abilitarli, bisogna capire quali azioni diventano possibili.
Il proxy riduce il rischio, ma non crea una separazione assoluta. Concedere accesso a immagini, exec, volumi o creazione di container può ancora consentire una compromissione completa. Va inoltre fissata una versione dell'immagine, verificato il changelog e pianificato l'aggiornamento, invece di affidarsi stabilmente a latest.
Opzione 3: Docker in modalità rootless
Docker rootless esegue daemon e container senza un daemon root, usando user namespace. In caso di fuga dal container o abuso dell'API, l'attaccante incontra un confine aggiuntivo: opera con l'UID dell'utente dedicato, non direttamente come root sull'host.
È una scelta valida per workload compatibili, ma non è trasparente in ogni scenario. Porte privilegiate, networking, cgroup, storage driver e accesso a dispositivi possono richiedere configurazioni specifiche. Prima della migrazione bisogna testare backup, limiti di risorse, riavvio al boot e persistenza dei volumi. Per un nodo nuovo è spesso più semplice partire rootless che convertire un host già carico di servizi.
Rootless non sostituisce il minimo privilegio: consegnare il relativo socket a un'applicazione compromessa permette comunque di controllare tutti i container e i dati appartenenti a quell'utente.
Hardening complementare
Il socket è solo una parte del modello di sicurezza. Per ogni stack conviene applicare controlli coerenti:
- eseguire il processo con utente non root quando l'immagine lo supporta;
- impostare filesystem root in sola lettura e aggiungere volumi scrivibili solo dove servono;
- rimuovere capability Linux con
cap_drop: [ALL], riaggiungendo le sole necessarie; - abilitare
no-new-privilegese mantenere i profili seccomp/AppArmor; - separare frontend, backend e rete di gestione;
- non inserire segreti nel file Compose o nelle variabili visibili via inspect;
- limitare chi può accedere al gruppo
dockere proteggere le chiavi SSH; - aggiornare Engine, plugin Compose, immagini e sistema operativo con una finestra controllata;
- raccogliere log e allarmi sui container privilegiati e sui mount sensibili.
Un controllo periodico può cercare configurazioni ad alto rischio:
docker ps -q | xargs -r docker inspect --format '{{.Name}} privileged={{.HostConfig.Privileged}} pid={{.HostConfig.PidMode}} network={{.HostConfig.NetworkMode}}'
Va verificato anche l'ascolto del daemon. docker context ls e la configurazione systemd aiutano a scoprire endpoint TCP inattesi. Se l'amministrazione remota è necessaria, la documentazione Docker raccomanda SSH o TLS: non si deve aprire una API non autenticata su 0.0.0.0.
Checklist di adozione
- Inventariare tutti i container che montano il socket.
- Rimuovere il mount dove è soltanto una comodità.
- Definire gli endpoint minimi richiesti da ciascun client.
- Inserire un socket proxy su una rete interna dedicata.
- Bloccare le richieste di modifica quando il client deve solo leggere.
- Valutare rootless per i nuovi nodi e per i workload compatibili.
- Testare riavvio, restore e aggiornamenti prima del passaggio in produzione.
- Ripetere l'audit dopo ogni modifica allo stack.
La decisione pratica segue una gerarchia netta: nessun socket è meglio di un proxy; un proxy ristretto è meglio del socket diretto; rootless aggiunge difesa in profondità, ma non autorizza a ignorare i privilegi. In una piccola infrastruttura questa disciplina costa poco e impedisce che la compromissione di una dashboard diventi immediatamente la compromissione dell'intero host.
Fonti
- Docker Docs, Docker daemon attack surface: https://docs.docker.com/engine/security/
- Docker Docs, Rootless mode: https://docs.docker.com/engine/security/rootless/
- Tecnativa, docker-socket-proxy: https://github.com/Tecnativa/docker-socket-proxy