Blog

Ce clienți au nevoie efectiv de un VM propriu? Un plan de izolare Docker pe niveluri.

Un VM pentru fiecare client este exagerat. Iată cum să decizi câtă izolare are nevoie fiecare tenant—și să automatizezi decizia.

Rezumat

Agențiile intră adesea în panică atunci când un client întreabă cât de izolate sunt cu adevărat datele lor față de ceilalți tenanți. Namespace-urile și cgroup-urile Docker oferă izolare reală, dar nu sunt același lucru cu o barieră hardware. În loc să rulezi fiecare client pe un VM—sau, mai rău, să tratezi fiecare client la fel—construiește un set mic de niveluri de izolare și potrivește fiecare client la unul în funcție de sensibilitatea datelor, încredere și conformitate. Un container securizat (non-root, fără capabilități, seccomp, sistem de fișiere read-only) acoperă majoritatea site-urilor; sarcinile reglementate sau ostile primesc un VM sau un hibrid container-în-VM. Acest articol oferă un flux decizional repetabil, un tabel comparativ și o privire onestă asupra momentului în care mai multă izolare este exagerată.

Ești în punctul dintr-un apel de vânzări în care noul client spune „suntem în domeniul sănătății, arată-mi că datele noastre sunt izolate de ceilalți clienți ai tăi” și ai prefera să vorbești despre orice altceva?

Aceasta este problema agenției: nu o singură implementare perfectă, ci aceeași implementare de încredere repetată pentru o duzină de clienți cu bugete diferite, profiluri de risc și cerințe de conformitate. Iată versiunea sinceră. Izolarea Docker este reală, dar este specifică. Namespace-urile oferă fiecărui container propria viziune asupra proceselor, rețelei și sistemului de fișiere; cgroup-urile limitează CPU, memoria și I/O-ul pe disc, astfel încât tenanții să nu se sufoce reciproc. Ceea ce nu primești este un zid hardware între container și kernel-ul gazdei. Dacă un atacator evadează din container, este în interiorul singurului kernel pe care îl ai. Restul acestui articol transformă acest fapt inconfortabil într-o decizie repetabilă: clasifică fiecare client după sensibilitatea datelor și încredere, aplică un profil de hardening de bază și apelează la un VM doar când costul unei breșe este mai mare decât costul VM-ului.

Stai, nu sunt containerele deja izolate?

Docker rulează pe namespace-uri și cgroup-uri Linux, iar aceste cuvinte fac treabă reală. Namespace-urile separă ID-urile de proces, stivele de rețea, punctele de montare și utilizatorii, astfel încât un proces dintr-un container să nu poată vedea tabela de procese a altuia. Cgroup-urile stabilesc limite: dă unui container 0,5 CPU, 512 MB de memorie și o greutate fixă de I/O pe disc, și exact asta primește. O buclă care rulează necontrolat într-un tenant este limitată în loc să doboare vecinul. Dacă nu ai configurat limite, ai sărit peste cel mai de bază lucru pentru care există cgroup-urile.

Consideră o aplicație PHP simplă în containerul A. Își vede propriul sistem de fișiere, propria interfață de rețea, propriul PID 1. Containerul B are la fel, dar o vedere diferită. Asta sunt namespace-urile. Acum pleacă și omite limita de memorie: containerul A poate umple RAM-ul gazdei și poate face containerul B să meargă încet. Exact asta există cgroup-urile să prevină. Dar două containere pot fi izolate unul de celălalt prin namespace-uri și pot totuși partaja kernel-ul gazdei, care este partea despre care este fiecare poveste de evadare din container. Un exploit care ajunge la kernel poate potențial ajunge la fiecare tenant de pe acea gazdă.

„Docker este izolat” este o frază pe jumătate adevărată. Versiunea corectă este „Docker izolează cu namespace-uri și cgroup-uri, iar o vulnerabilitate de kernel este raza de explozie.” Înainte să ai încredere într-un tenant să ruleze cod neîncrezător, stai cu acest gând un minut. Răspunsul nu este „nu folosi niciodată containere”—aceasta este panica ușoară. Răspunsul este un sistem de niveluri.

Deci de ce au unii clienți nevoie de mai mult decât namespace-uri?

Răspunsul onest este că izolarea nu este un întrerupător, ci un spectru. La un capăt ai un container complet partajat în care toată lumea este efectiv într-o singură aplicație. La celălalt capăt ai un VM separat pentru fiecare tenant cu propriul kernel. Majoritatea muncii de agenție trăiește în mijlocul inconfortabil, iar mijlocul nu este o alegere binară între „Docker este ok” și „rulează un VM pentru toți”.

Ceea ce împinge un client la dreapta nu este mărimea lor. Sunt patru întrebări:

  • Stochează date reglementate? Înregistrări medicale, detalii de card de plată, orice ar numi un regulator sensibil.
  • O breșă pe tenantul lor are o cale realistă către un alt tenant? Dacă pot rula cod arbitrar, da.
  • Ai încredere în codul și oamenii care îl implementează? Un client care angajează cel mai ieftin freelancer nu are același nivel de încredere ca un client a cărui echipă de dezvoltare o cunoști.
  • Contractul lor spune „dedicat”, „izolat” sau „privat”? Dacă da, ai promis deja un nivel; singura treabă acum este să alegi cel potrivit.

Dacă nu poți răspunde încă la aceste întrebări, pune clientul într-un nivel de bază și notează ipotezele. Nu este un audit de securitate; este o verificare de bun-simț pe care o repeți la fiecare integrare.

Cum decid per client fără să rulez un audit de securitate de fiecare dată?

Fă un tabel mic și angajează-te. Nu ai nevoie de o matrice cu patruzeci de celule. Patru niveluri vor acoperi aproape fiecare client pe care îl vede o agenție.

Poziția clientuluiCe îi separă de faptFolosește când
Tier 1: Aplicație/container partajatDoar logica aplicațieiUtilități interne, date cu risc scăzut, proiecte în care toată lumea este explicit într-un singur sistem de autentificare
Tier 2: Aceeași gazdă, containere separateNamespace-uri și cgroup-uriMajoritatea site-urilor de marketing, formulare de contact, fără date sensibile
Tier 3: Container securizatTier 2 + non-root, fără capabilități, seccomp, root read-only, segmentare de rețeaE-commerce, date cu caracter personal (PII), cod personalizat în care nu ai încredere totală
Tier 4: VM per tenantHypervisor și un kernel separatSănătate, finanțe, documente de conformitate, cod neîncrezător, vecini zgomotoși

Iată cum se desfășoară în practică. Un client brutar cu un formular de contact și un link Instagram merge la Tier 2: un container pe o gazdă partajată, rețea Docker implicită, limite de resurse, treabă terminată. Un magazin online care stochează nume de clienți, adrese și redirecționări de plată merge la Tier 3: aceeași gazdă partajată, dar containerul rulează ca utilizator non-root, nu are capabilități suplimentare de kernel, folosește un profil seccomp și expune doar portul 443. Un portal medical de admitere care stochează informații de sănătate protejate merge la Tier 4: un VM per tenant, deoarece costul unei breșe nu este „o vom curăța noi” ci „nu putem arăta clientului că l-am luat în serios”.

Tot trucul este că nu regândești arhitectura pentru fiecare client. Alegi un rând dintr-un tabel pe care l-ai stabilit deja. Așa poate o agenție de cinci persoane să ruleze o sută de site-uri fără o sută de obsesii separate de securitate. Înseamnă, de asemenea, că următorul client nu primește un răspuns care depinde de care membru al echipei a răspuns la telefon. Pentru dezbaterea mai profundă asupra arhitecturii din spatele acestor alegeri, acest ghid despre proiectarea nivelurilor de izolare multi-tenant acoperă compromisurile în detaliu.

Cum arată de fapt un container securizat?

Să nu mai spunem „securizat” și să fim concreți. Iată ce înseamnă Tier 3 pentru un client tipic WordPress sau PHP.

În primul rând, schimbă utilizatorul. Majoritatea imaginilor oficiale rulează încă ca root implicit; în Dockerfile-ul tău, creează un utilizator non-root și rulează aplicația cu acel utilizator. Asta elimină imediat cea mai comună cale prin care un compromis al containerului devine un compromis al gazdei. În al doilea rând, elimină capabilitățile de care nu ai nevoie. Rulează cu --cap-drop ALL și adaugă înapoi doar una, de obicei NET_BIND_SERVICE pentru ca aplicația să poată asculta pe portul 80. Doar asta este o schimbare mai mare decât se așteaptă majoritatea oamenilor. În al treilea rând, fă sistemul de fișiere root read-only cu --read-only și montează directoarele care trebuie să fie scrise (încărcări, directorul de date al bazei de date) ca volume sau tmpfs. În al patrulea rând, aplică un profil seccomp și, dacă gazda ta îl suportă, AppArmor sau SELinux. În cele din urmă, pune containerul pe o rețea Docker dedicată și expune doar porturile care trebuie să fie accesibile.

Hai să trecem printr-un exemplu WordPress. Imaginea de bază probabil rulează ca root, așa că adaugi un pas useradd și o directivă USER. Rulezi containerul cu o limită de memorie și o limită de CPU, astfel încât un vârf de trafic de plugin-uri să nu rănească vecinul. Montezi /var/www/html/wp-content/uploads ca volum cu drepturi de scriere. Setezi --read-only. Îl atașezi la o rețea care nu are niciun flag --privileged în apropiere. Rezultatul este un container care obișnuia să fie „un site WordPress” și acum este „un site WordPress care se întâmplă să fie mai securizat decât majoritatea serverelor virtuale private”.

Dacă a face totul manual pare fragil, există o cale de mijloc mai ușoară: Enhanced Container Isolation de la Docker, care folosește izolarea namespace-urilor de utilizator și un runtime de container securizat. Este o scurtătură legitimă, dar nu este un permis gratuit pentru a sări peste non-root sau eliminarea capabilităților. Tenantul are totuși nevoie de o imagine rezonabilă. Diferența este că suprafața de atac orientată spre kernel devine mai mică fără să devii expert seccomp peste noapte. Dacă vrei secvența exactă pentru un singur tenant, ghidul pas cu pas de securizare pentru izolare transformă această secțiune în comenzi de copiat-lipit.

Când mă opresc din stratificare și pur și simplu le dau un VM?

Aici este partea contrară: mai multă izolare nu este automat mai bine. VM-urile oferă izolare la nivel de hardware, un kernel separat și o suprafață de atac mult mai mică dacă kernel-ul oaspeților cade. Exact asta se așteaptă clienții din domeniul sănătății și finanțelor când spun „vrem să fim izolați”. Dar fiecare VM adaugă costuri de patch-uri, backup și calcul, și multiplică munca de a menține o flotă actualizată. Dacă pui fiecare client pe VM pentru că un client ți-a spus odată că Docker îl sperie, ai cumpărat teatru de siguranță cu bani reali.

Un VM este răspunsul corect atunci când riscul per tenant este mai mare decât costul operațional al unui VM per tenant. Asta înseamnă date reglementate, cerințe scrise de conformitate, cod terț neîncrezător sau un client care are nevoie să fie îndepărtat un vecin zgomotos. Este, de asemenea, răspunsul corect atunci când contractul clientului promite literalmente un mediu dedicat, pentru că „container” nu este ceea ce își imaginează când semnează „dedicat”.

Dar un VM nu scuza un container neglijent. O capcană comună este să pui clientul într-un VM și apoi să sari peste securizare pentru că „VM-ul îi protejează”. VM-ul protejează gazda de tenant, nu tenantul de propria imagine proastă. În continuare vrei non-root, capabilități eliminate și seccomp în acel VM. Abordarea hibridă—containere într-un VM—este adesea punctul dulce: VM-ul oferă granița pentru conversațiile de conformitate, iar containerul îți oferă fluxul de lucru de implementare pe care îl cunoști deja. Există o versiune mai lungă a acelei dezbateri în Ar trebui fiecare tenant să aibă propriul VM?, dar răspunsul scurt este că VM-ul este pentru contract, nu pentru frică.

Cum fac acest lucru repetabil pentru fiecare client?

Îl faci repetabil transformând sistemul de niveluri într-un șablon, nu într-o amintire. Păstrează un director de fișiere Compose, unul pentru fiecare nivel: tier2-baseline, tier3-locked, tier4-vm-hybrid. Când apare un client nou, copiază șablonul, schimbă variabilele de mediu și știi deja forma izolării înainte să fi scris o linie de infrastructură nouă.

Apoi notează decizia. Nu un raport de securitate de 400 de pagini, ci un paragraf scurt în depozitul clientului: ce date stochează, la ce nivel sunt, de ce și ce i-ar muta la un nivel superior. Acel paragraf valorează mai mult decât o sută de reguli de firewall, pentru că este lucrul pe care îl poți arăta următorului auditor sau următorului client îngrijorat. De asemenea, te împiedică să fii nevoit să-ți amintești de ce brutăria a primit Tier 2 și magazinul de comerț electronic a primit Tier 3 după ce apelul de vânzări original s-a estompat.

Automatizează verificările plictisitoare. Fă-ți CI-ul să scaneze fiecare imagine de client și să eșueze o build dacă rulează ca root, dacă are toate capabilitățile sau dacă încearcă să publice un alt port decât cele permise de nivel. Nimic din toate acestea nu este exotic; este doar să te asiguri că șablonul nu este rupt accidental de un dezvoltator bine intenționat. Dacă oricum construiești fluxul de lucru de găzduire din jur, articolul despre strategii de găzduire Docker pregătite pentru producție acoperă partea care vine după ce containerele sunt definite.

Nimic din toate acestea nu este glamoros. Niciun articol de blog nu va face „izolarea tenantilor” să sune la fel de captivant ca o diagramă de arhitectură greenfield. Dar aceasta este diferența dintre o agenție care răspunde „cât de izolați suntem?” cu un „total” cu degetele încrucișate și una care poate arăta un nivel, o configurație și un motiv. Containerele nu sunt un zid magic. VM-urile nu sunt un glonț magic. Un sistem de niveluri este doar o decizie pe care o scrii și o reutilizezi—și pentru o agenție, repetabilul este tot jocul.

Sources (5)