Blog

L'illusione della fiducia: come la nostra configurazione Docker multi-tenant ha causato una fuga di dati (e come l'abbiamo risolta)

Scopri come l'ingenua configurazione Docker di un team ha portato a una fuga di dati tra tenant e la strategia di isolamento a strati che l'ha prevenuta.

Riepilogo

I container Docker non sono isolati per impostazione predefinita: condividono il kernel host e, senza una configurazione deliberata, i tenant possono interferire tra loro. Questo articolo illustra uno scenario reale in cui un provider di hosting multi-tenant ha scoperto che i container dei clienti potevano accedere ai database reciproci a causa di reti condivise e impostazioni di sicurezza deboli. Mostriamo le modifiche passo dopo passo che hanno risolto la violazione: reti definite dall'utente per tenant, utenti non root, capacità rimosse, file system di sola lettura e profili seccomp. Un presupposto comune è che i container forniscano intrinsecamente un forte isolamento; lo contestiamo spiegando perché le macchine virtuali offrono ancora un confine più solido e quando considerare un approccio ibrido. La conclusione ribadisce che l'isolamento è un esercizio a strati, non una singola casella da spuntare.

L'incidente: quando i container parlano troppo

Hai configurato Docker su un singolo host per eseguire più siti web client. Ogni cliente ha il proprio container: un ambiente pulito e isolato, giusto? Questo è ciò che pensavamo. Fino a quando un audit di sicurezza di routine ha rivelato che il container del Cliente A stava leggendo il socket MySQL del container del Cliente B sullo stesso host. Condividevano la rete bridge predefinita. Peggio ancora, i container venivano eseguiti come root, quindi un attaccante che ne comprometteva uno poteva manomettere il socket Docker dell'host o il file system di un altro container. La violazione non era un exploit sofisticato; era una configurazione di base errata. Dati trapelati. Fiducia dissolta.

Lo scenario di fallimento non è raro. Molti team presumono che i namespace e i cgroups di Docker isolino automaticamente i tenant, ma sottovalutano quante vie di fuga rimangano aperte per impostazione predefinita. Le reti bridge predefinite non offrono alcun isolamento di rete tra i container. Eseguire come root dà al container più potere del necessario. E senza limiti espliciti delle risorse, un vicino rumoroso può privare gli altri di CPU o memoria.

Passo 1: Smetti di condividere una singola rete

La nostra prima correzione è stata quella di dare a ogni tenant la propria rete Docker definita dall'utente. Ciò impedisce ai container di raggiungersi a vicenda a meno che non li connetti esplicitamente. Abbiamo creato uno script che, per ogni tenant, crea una rete dedicata e collega il container dell'applicazione ad essa. Il container del database risiede nella stessa rete del tenant, ma abbiamo anche aggiunto una rete interna per la comunicazione solo intra-tenant. Niente più intrusioni tra tenant.

Abbiamo anche isolato i database eseguendoli in container separati sulla stessa rete del tenant, utilizzando volumi di dati separati. Ciò ha garantito che anche se un attaccante fosse entrato nel container dell'app, non avrebbe potuto intercettare il traffico del database di un altro tenant.

Per un approfondimento sulle strategie di isolamento di rete, consulta Una checklist pratica di sicurezza per l'isolamento Docker per hosting multi-tenant.

Passo 2: Rimuovi i privilegi non necessari

Per impostazione predefinita, i container Docker vengono eseguiti con un insieme limitato di capacità Linux, ma hanno comunque più di quanto la maggior parte delle applicazioni necessiti. I nostri container venivano eseguiti come root, il che consentiva ai processi interni di eseguire azioni come montare file system o modificare parametri del kernel. Siamo passati all'esecuzione dell'applicazione come utente non root all'interno del container (usando la direttiva USER nel Dockerfile) e abbiamo rimosso tutte le capacità tranne quelle assolutamente necessarie. Per una tipica app web, potrebbero essere solo NET_BIND_SERVICE (per il binding a porte inferiori a 1024) e CHOWN (per la scrittura nelle directory). Abbiamo anche aggiunto --security-opt no-new-privileges per prevenire l'escalation dei privilegi.

Questo singolo passo ha eliminato molti vettori comuni di fuga dai container. Un attaccante che compromette il server web non può installare pacchetti, modificare binari di sistema o accedere al socket Docker dell'host perché al processo mancano le capacità CAP_SYS_ADMIN o CAP_DAC_OVERRIDE.

Passo 3: Blocca il file system

I file system scrivibili sono una superficie d'attacco comune. Abbiamo reso il file system root di sola lettura (--read-only) per tutti i container, e poi montato file system temporanei (tmpfs) per le directory che necessitano di accesso in scrittura, come /tmp e la directory cache dell'applicazione. Ciò impedisce a un attaccante di modificare il codice dell'applicazione o di persistere binari malevoli.

Inoltre, abbiamo usato l'opzione --mount di Docker per montare in modalità bind directory sensibili come il socket Docker solo quando assolutamente necessario—e mai sui container di produzione. Il principio: se il container non ha bisogno di scrivere in un percorso, rendilo di sola lettura.

Passo 4: Applica profili Seccomp e AppArmor

I profili seccomp predefiniti bloccano già molte syscall pericolose, ma li abbiamo personalizzati ulteriormente per inserire nella whitelist solo le syscall di cui la nostra applicazione ha effettivamente bisogno. Questo è un compromesso perché richiede la profilazione dell'applicazione. Un approccio più semplice è utilizzare il profilo seccomp predefinito di Docker e poi aggiungere --security-opt seccomp=path/to/profile.json se sono necessarie regole più severe. Allo stesso modo, i profili AppArmor possono confinare i processi dei container a percorsi file e capacità specifici. Abbiamo abilitato AppArmor e utilizzato un profilo personalizzato che limitava l'accesso solo alle directory dei dati dell'applicazione.

Per una guida completa su questi passaggi di rafforzamento, consulta Hardening dei container Docker per hosting multi-tenant: una guida all'isolamento passo dopo passo.

La visione contraria: a volte servono le VM

Non importa quanto siano rafforzati, i container condividono il kernel dell'host. Una vulnerabilità del kernel può rompere tutto l'isolamento in una volta. Ecco perché molte piattaforme attente alla sicurezza eseguono container all'interno di macchine virtuali leggere: ogni tenant ha il proprio kernel. Questo aggiunge overhead ma fornisce un confine a livello hardware che i container da soli non possono. Se i tuoi tenant gestiscono dati di carte di credito o cartelle cliniche, un approccio ibrido (container all'interno di VM) potrebbe essere la scelta giusta. Non dare per scontato che l'isolamento dei container sia sufficiente per il tuo modello di minaccia; valuta la sensibilità dei dati e i requisiti normativi.

Per un confronto più approfondito dei livelli di isolamento, leggi Progettare un'architettura Docker multi-tenant: scegliere il giusto livello di isolamento.

Conclusione: l'isolamento è una pila, non un interruttore

La correzione non è stata un singolo cambiamento: è stata una stratificazione: isolamento di rete, privilegi limitati, file system di sola lettura e filtraggio delle syscall. Anche così, abbiamo accettato che l'isolamento perfetto è impossibile con container che condividono il kernel. Per i nostri tenant con i più alti requisiti di sicurezza, li abbiamo spostati su host dedicati. La lezione: non fidarti delle impostazioni predefinite. Controlla la tua configurazione Docker come se una violazione fosse già avvenuta. Il momento di bloccare è prima della fuga, non dopo.

Sources (5)