Blog

Ogni tenant dovrebbe avere la propria VM?

Scegli tra container per tenant, VM e configurazioni ibride con un framework decisionale basato sul rischio e i passaggi di hardening che rendono difendibile ogni opzione.

Riepilogo

L'hosting multi-tenant ti obbliga a scegliere quanto i tenant possano accedere l'uno all'altro. I container usano i namespace Linux e i cgroup per isolare processi e risorse, ma condividono il kernel dell'host. Le macchine virtuali aggiungono un confine a livello hardware, a costo di velocità e carico operativo. Un approccio ibrido—container all'interno di VM—può dare entrambi, ma raddoppia la superficie da patchare. Questo articolo ti guida attraverso una decisione basata sul rischio, un confronto fianco a fianco e i passaggi di hardening di Docker che contano anche all'interno di una VM. Alla fine, saprai quale modello di isolamento si adatta ai tuoi tenant e cosa configurare prima del lancio.

La tua app multi-tenant è quasi pronta. Hai un file Docker Compose che avvia uno stack per ogni cliente, ed è veloce. Poi un amico che gestisce un'azienda di hosting ti chiede: 'Stai dando a ogni tenant la sua VM?' Ti blocchi. Non avevi previsto quella domanda. Questo articolo ti offre un modo per rispondere oggi, senza un team di sicurezza. Lo fai da solo, quindi la decisione deve essere abbastanza semplice da difendere alle 2 di notte.

Smettila di cercare il modello 'migliore'. Inizia scrivendo cosa succede se il codice di un tenant prende il controllo del tuo host. Definisci il raggio d'esplosione prima di scegliere qualsiasi strumento. Questo esercizio ti dirà più di qualsiasi benchmark.

Il Kernel è il Coinquilino che Non Puoi Sfrattare

I container sono efficienti perché condividono il kernel dell'host. Questa condivisione è tutto il trucco e tutto il rischio. I namespace Linux danno a ogni container una propria visione di processi, rete e filesystem. I control group (cgroup) ti permettono di limitare CPU, memoria e I/O del disco così che un tenant non possa affamare gli altri. Ma nessuno dei due crea un muro hardware.

Pensa a un container come a un processo con un'ottima falsa identità. Crede di essere sulla propria macchina. Il kernel, tuttavia, è una copia di Linux in esecuzione sul tuo host. Se un tenant sfrutta una vulnerabilità del kernel, i namespace diventano solo metadati. Un attaccante che può chiamare funzioni del kernel può raggiungere altri namespace sullo stesso kernel. Questa è la fuga dal container di cui senti parlare.

Supponiamo che tu ospiti un piccolo strumento B2B con un container per cliente. Un cliente installa un plugin losco con un bug di esecuzione remota del codice. Con le impostazioni Docker predefinite, quel processo gira come root all'interno del container. Root in un container è comunque UID 0, e il kernel non distingue quel UID dal root dell'host a meno che non mappi esplicitamente gli utenti. L'attaccante può tentare di evadere, e il kernel condiviso è il suo bersaglio.

Il guasto non deve essere drammatico. Un singolo tenant che perde memoria può spingere l'host nello swap, rallentando ogni altro tenant. Senza limiti cgroup, un ciclo che si comporta male è un attacco alla disponibilità. Con essi, è un processo bloccato e un avviso.

Significa che i container non sono sicuri? No. Significa che devi trattare il kernel come una zona di fiducia condivisa. Prima di scegliere, scrivi una dichiarazione di rischio di un paragrafo: 'Se il container di un tenant viene compromesso, l'attaccante può accedere a: [elenco]. Il costo aziendale sarebbe: [importo o impatto].' Se quel paragrafo ti spaventa, non sei paranoico. Sei onesto.

Per un approfondimento sullo spettro dell'isolamento, dai container condivisi a stack completamente separati, consulta la nostra guida alla progettazione di un'architettura Docker multi-tenant.

Tre Modi per Affettarlo (Scegline uno Prima di Distribuire)

ApproccioBarriera di isolamentoIdeale quandoAvvertenza più difficile
Container per tenantNamespace del kernel + cgroupsMolti tenant piccoli, rischio basso per tenant, bisogno di densitàUn singolo exploit del kernel può rompere ogni tenant su quell'host
Una VM per tenantVirtualizzazione hypervisor/hardwareDati regolamentati, tenant ostili, alto valore per tenantPiù pesante, più lenta da provisioning, devi patchare un OS per tenant
Container all'interno di VMConfine VM attorno ai workload containerizzatiDensità più un guscio duro tra i gruppiCosti e overhead operativi quasi raddoppiano

Container per tenant. Questa è l'impostazione predefinita per la maggior parte dei fondatori di SaaS. Ogni tenant ha il proprio container o un piccolo stack Compose. Il provisioning è immediato, le immagini sono piccole, CI/CD è semplice. I limiti di risorse impediscono ai vicini rumorosi di mangiarsi il server. Il compromesso è il kernel condiviso. Se puoi mantenere i workload non privilegiati e patchare l'host regolarmente, spesso è la prima mossa giusta.

Non mettere due tenant nello stesso container. Ciò significa un kernel condiviso più un runtime condiviso più un filesystem condiviso. Se un tenant carica un file che crea un processo, l'altro tenant è già nella stessa tabella dei processi. Un container è la tua unità di isolamento; fai un tenant per container.

E il database? Se ogni tenant si connette a un'unica istanza MongoDB o PostgreSQL con le stesse credenziali, hai già aggiunto un enorme componente condiviso. Dai a ogni tenant credenziali separate e, idealmente, un database o schema separato. I container isolano l'app; il database è spesso la prima falla che un attaccante proverà.

Una VM per tenant. Dai a ogni tenant una macchina virtuale completa. L'hypervisor aggiunge un confine a livello hardware, che è esattamente ciò che un exploit del kernel deve attraversare per raggiungere l'host. Questo è importante per ambienti regolamentati o quando i tenant non sono attendibili. Il costo è densità e tempo. Ora gestisci una flotta di sistemi operativi, non solo container. Ogni VM richiede aggiornamenti, agenti di sicurezza e monitoraggio. Per un fondatore singolo, è un vero lavoro.

Schemi che funzionano a questo livello: usa infrastruttura come codice per creare una VM dalla stessa immagine di base, incorpora gli aggiornamenti in nuove immagini invece di patchare sistemi live e termina i workload che non riconosci. Tieni chiusa a internet la porta di gestione della VM.

Container all'interno di VM. Questo ibrido raramente viene discusso nei tutorial per principianti. Metti una piccola VM attorno a ogni tenant (o piccolo gruppo di tenant), poi esegui container all'interno di quella VM. La VM è un contenitore del raggio d'esplosione; i container sono solo unità distribuibili. Questo ti dà il confine netto della virtualizzazione e la riproducibilità delle immagini. Costa di più, perché paghi l'overhead della virtualizzazione e la flessibilità dei container, ma può essere il modello più sensato a lungo termine quando non puoi fidarti completamente dei tenant.

Un micro-esempio comune: un tenant esegue un'API Node e un worker in background. Invece di un unico grande container con entrambi i processi, usa una VM, poi due container con diversi limiti di risorse, una rete condivisa e nessuna esposizione diretta a internet per il worker. La VM fornisce il confine netto; i container forniscono struttura.

Quale dovresti scegliere? La tabella è la tua shortlist. Le prossime sezioni rendono concreta la decisione.

Se Scegli i Container, Fai Queste Sei Cose o Non Ne Vale la Pena

I container per tenant vanno bene se tratti ogni container come un potenziale attaccante. Questo inizia dalla configurazione, non dal pensiero positivo.

0. Limita le risorse prima di fidarti di chiunque. I cgroup sono un meccanismo di equità e una difesa di disponibilità. Imposta --memory e --cpus per container. Un tenant che perde memoria dovrebbe colpire il proprio limite, non quello del server. Non è un confine di sicurezza, ma un vicino rumoroso è un attacco senza una singola riga di codice. Un avvio pratico: --memory 512m --cpus 0.5. Per un processo worker, inizia più basso e scala.

1. Esegui come utente non root. Non permettere mai al processo del container di usare UID 0 a meno che non sia assolutamente necessario. Imposta un utente nel Dockerfile e passa --user come ulteriore protezione. Un exploit che gira come utente non privilegiato ha molti meno percorsi verso il kernel. Nel tuo Dockerfile, crea un utente: RUN useradd -u 10001 app e USER app. Non saltare questo passaggio per risparmiare tempo.

2. Elimina ogni capacità di cui non hai bisogno. Le capacità Linux dividono il potere di root in piccoli pezzi. La maggior parte delle app web non ne ha quasi bisogno. Inizia con --cap-drop=ALL e aggiungi solo ciò che sai di doverti servire. Un container senza CAP_SYS_ADMIN è molto più difficile da usare per trucchi con i namespace. Se la tua app cerca di legare una porta privilegiata, eseguila su una porta alta e metti un proxy davanti invece di concedere NET_BIND_SERVICE.

3. Rendi il filesystem di sola lettura. La tua app non dovrebbe scrivere nel proprio layer del container. Monta un tmpfs per lo stato. Un attaccante che non può scrivere su disco ha molta più difficoltà a piantare persistenza. Un'app PHP compromessa che cerca di scrivere una webshell fallirà quando il filesystem root è di sola lettura. Puoi montare un volume nominato per una directory scrivibile di cui la tua app ha realmente bisogno.

4. Applica seccomp e AppArmor o SELinux. Questi mandano le syscall rischiose nel cestino. Docker include un profilo seccomp predefinito; usalo. Aggiungi un profilo AppArmor per un altro strato. Non devi padroneggiare ogni syscall. Devi negare ciò che un normale web worker non richiede mai. Non eseguire mai con --privileged. Questa flag disabilita quasi ogni difesa che hai appena impostato.

5. Segmenta la rete. Non dare a ogni container una rotta verso ogni altro container. Rifiuto predefinito, poi apri solo le porte di cui hai bisogno. Un container database compromesso non dovrebbe poter scansionare il tuo pannello di amministrazione. Se i tenant sono in reti separate, una violazione in una rete non può diffondersi lateralmente.

Un avvio pratico:

docker run --user 10001 --cap-drop=ALL --security-opt no-new-privileges --read-only --tmpfs /tmp:rw,size=64M --security-opt seccomp=default.json --memory 512m --cpus 0.5 myimage

Metti le stesse flag in un file Compose e applicale a ogni tenant. Non è completo, ma è un default molto più robusto di quello che docker run ti dà subito.

Per una guida più approfondita, usa la nostra guida passo-passo all'hardening per container Docker in hosting multi-tenant.

L'isolamento avanzato dei container di Docker è l'eccezione che dovresti conoscere

Se esegui all'interno di un ambiente Docker gestito, cerca l'isolamento avanzato dei container di Docker (ECI). Utilizza l'isolamento dei namespace utente e un runtime container sicuro sotto il cofano. Root all'interno di un container mappa a un utente non privilegiato sull'host, quindi anche un container che gira come root non ottiene i privilegi root dell'host. Inoltre blocca capacità e syscall pericolose per impostazione predefinita. Non è qualcosa che puoi ricreare con alcune flag su Docker vanilla. Se la tua piattaforma lo supporta, attivalo. Non elimina la necessità di utenti non root e limiti di risorse, ma cambia la matematica del rischio.

Puoi avvicinarti in parte con il rimappaggio dei namespace utente (userns-remap) nel demone Docker. Non è completo come un runtime sicuro, ma è meglio di niente. Se lo usi, verifica che la mappatura UID funzioni prima di fidarti.

La Fallacia della VM: Passare alle Macchine Virtuali Non È Hardening

Ecco la parte contrarian, ed è la parte che la maggior parte delle persone salta. Se passi a una VM per tenant e poi distribuisci i tuoi normali container al suo interno, non hai rimosso il tuo problema di sicurezza dei container. Hai aggiunto una gabbia più grande. La fuga dal container funziona ancora; l'attaccante atterra nella VM invece che sull'host. Questo è un vero miglioramento, ma hai comunque bisogno dei sei passaggi.

L'altra trappola è assumere che la VM stessa sia sicura. Un'immagine predefinita con una password SSH debole, pacchetti di base non patchati o una porta di gestione aperta è un regalo. Il confine dell'hypervisor conta solo se l'ospite è indurito e aggiornato. Altrimenti, la tua 'VM sicura' è una via più veloce verso la compromissione perché ti senti al sicuro e smetti di controllare.

Ciò che una VM ti dà è un raggio d'esplosione riducibile. Il disastro di un tenant rimane in una VM. Ciò che ti costa è il tuo tempo. Diventi l'amministratore di sistema per tanti sistemi operativi quanti sono i tuoi tenant. Se sei un fondatore singolo che spedisce un prodotto, chiediti se hai le ore per patchare e monitorare una flotta. Se sì, una VM per tenant può essere la scelta giusta. Se no, i container con un forte hardening potrebbero essere più onesti.

Ricorda anche che il tuo host hypervisor è un bersaglio critico. Un hypervisor compromesso può vedere tutti gli ospiti. Patcha l'host, non solo gli ospiti. La VM non ti esonera dal patchare l'host; aumenta la posta in gioco se lo salti.

Un'avvertenza sull'ibrido: non dare per scontato che i container all'interno di una VM ti diano 'due livelli di sicurezza' gratis. La VM aggiunge un confine; il container ha comunque bisogno di non-root, capacità e seccomp. Altrimenti il primo livello è forte quanto il container più debole.

Quattro Domande che Risolvono il Dibattito in Dieci Minuti

Non ottimizzare in astratto. Fatti queste quattro domande in ordine. Scrivi le risposte.

1. A cosa ha accesso il mio tenant? Se un tenant può raggiungere solo la propria app web e il proprio database, i container per tenant con regole di rete rigide sono difendibili. Se i dati di un tenant sono regolamentati o finanziariamente sensibili, spostati verso le VM.

2. Quanto mi costerebbe la compromissione di un tenant? Somma clienti persi, esposizione legale e fiducia. Se il numero è più grande del costo di gestire le VM, spendi i soldi. Se no, i container sono una scelta razionale.

3. Quanti tenant ho e quanto pagano? Molti piccoli abbonati: la densità dei container conta. Un paio di grandi account: dai a ciascuno una VM e fattura di conseguenza. I tenant che ti pagano meno di un caffè non dovrebbero richiedere ciascuno un OS da gestire.

4. Posso patchare le cose secondo un programma? I container condividono un unico kernel host, quindi patchare l'host protegge tutti. Le VM moltiplicano i tuoi obiettivi di patch. Se sai che salterai gli aggiornamenti, scegli l'architettura con meno parti mobili e default più rigidi.

Le tue risposte si raggrupperanno. Due o più risposte orientate alla VM significano che non dovresti usare di default i container per tenant. Tre o più risposte orientate ai container significano che le VM sono premature. Un risultato controintuitivo: un tenant a basso reddito con accesso a dati sensibili ha comunque bisogno della VM, perché il costo normativo non ha nulla a che fare con quanto paga.

Rilascia il Minimo di Cui Ti Fidi, Poi Guadagna Più Isolamento

La tua prima architettura non deve essere quella definitiva. Inizia con la configurazione più rigorosa che puoi realmente mantenere, poi aggiungi isolamento man mano che la tua base di tenant lo giustifica. Per la maggior parte degli operatori singoli, ciò significa container per tenant con non-root, capacità limitate, filesystem di sola lettura, seccomp e segmentazione di rete. Per tenant regolamentati o di alto valore, passa direttamente a una VM per tenant, con container solo come livello di impacchettamento all'interno.

Qualunque cosa tu scelga, scrivi la decisione e rivalutala trimestralmente. Quando riceverai la prima domanda 'dovremmo spostare questo tenant su una VM?', avrai una risposta e avrai la checklist a supporto. Questo è ciò che l'isolamento significa davvero: un compromesso che gestisci, non una tecnologia che compri.

Prima del lancio, esamina la nostra checklist pratica di sicurezza dell'isolamento Docker: trasforma queste decisioni in un elenco che puoi verificare prima di mostrare una pagina a un cliente.

Sources (5)