Blog
Eroarea încrederii: Cum configurația noastră Docker multi-tenant a scurs date (și cum am reparat-o)
Aflați cum configurația Docker naivă a unei echipe a dus la o scurgere de date între chiriași și strategia de izolare în straturi care a prevenit-o.
Rezumat
Containerele Docker nu sunt izolate implicit — ele partajează kernelul gazdei, iar fără o configurare deliberată, chiriașii se pot interfera reciproc. Acest articol parcurge un scenariu real în care un furnizor de hosting multi-tenant a descoperit că containerele clienților puteau accesa bazele de date ale celorlalți din cauza rețelei partajate și a setărilor de securitate slabe. Arătăm modificările pas cu pas care au reparat breșa: rețele definite de utilizator per-chiriaș, utilizatori non-root, capabilități retrase, sisteme de fișiere read-only și profiluri seccomp. O presupunere comună este că containerele oferă în mod inerent o izolare puternică; contestăm acest lucru explicând de ce VM-urile oferă încă o graniță mai dură și când să luați în considerare o abordare hibridă. Concluzia subliniază că izolarea este un exercițiu în straturi, nu o singură bifă.
Incidentul: Când containerele vorbesc prea mult
Ați configurat Docker pe o singură gazdă pentru a rula mai multe site-uri web ale clienților. Fiecare client are propriul container — un mediu curat și izolat, nu? Așa am crezut și noi. Până când un audit de securitate de rutină a relevat că containerul Clientului A citea socketul MySQL al containerului Clientului B pe aceeași gazdă. Ele partajau rețeaua implicită bridge. Mai rău, containerele rulau ca root, astfel că un atacator care compromitea unul putea manipula socketul Docker al gazdei sau sistemul de fișiere al altui container. Breșa nu a fost un exploit sofisticat; a fost o configurare greșită de bază. Date s-au scurs. Încrederea s-a dizolvat.
Scenariul eșecului nu este neobișnuit. Multe echipe presupun că namespace-urile și cgroup-urile Docker izolează automat chiriașii, dar subestimează câte portițe de scăpare rămân deschise implicit. Rețelele bridge implicite nu oferă izolare de rețea între containere. Rularea ca root oferă containerului mai multă putere decât este necesar. Și fără limite explicite de resurse, un vecin zgomotos poate lăsa pe alții fără CPU sau memorie.
Pasul 1: Nu mai partajați o singură rețea
Prima noastră reparație a fost să oferim fiecărui chiriaș propria sa rețea Docker definită de utilizator. Acest lucru împiedică containerele să se ajungă reciproc decât dacă le conectați explicit. Am creat un script care, pentru fiecare chiriaș, lansează o rețea dedicată și atașează containerul aplicației la aceasta. Containerul bazei de date locuiește în aceeași rețea a chiriașului, dar am adăugat și o rețea internă pentru comunicarea intra-chiriaș doar. Fără mai multă curiozitate între chiriași.
De asemenea, am izolat bazele de date rulându-le în containere separate pe aceeași rețea a chiriașului, folosind volume de date separate. Aceasta a asigurat că, chiar dacă un atacator pătrunde în containerul aplicației, nu poate mirosi traficul bazei de date de la un alt chiriaș.
Pentru o scufundare mai profundă în strategiile de izolare a rețelei, vedeți O listă de verificare practică a securității izolării Docker pentru hosting multi-tenant.
Pasul 2: Renunțați la privilegii inutile
Implicit, containerele Docker rulează cu un set limitat de capabilități Linux, dar tot au mai multe decât are nevoie majoritatea aplicațiilor. Containerele noastre rulau ca root, ceea ce permitea proceselor din interior să efectueze acțiuni precum montarea sistemelor de fișiere sau modificarea parametrilor kernelului. Am trecut la rularea aplicației ca utilizator non-root în interiorul containerului (folosind directiva USER în Dockerfile) și am retras toate capabilitățile, cu excepția celor absolut necesare. Pentru o aplicație web tipică, acestea ar putea fi doar NET_BIND_SERVICE (pentru legarea la porturi sub 1024) și CHOWN (pentru scrierea în directoare). Am adăugat și --security-opt no-new-privileges pentru a preveni escaladarea privilegiilor.
Acest singur pas a eliminat mulți vectori comuni de evadare din containere. Un atacator care compromite serverul web nu poate instala pachete, modifica binarele sistemului sau accesa socketul Docker al gazdei deoarece procesului îi lipsesc capabilitățile CAP_SYS_ADMIN sau CAP_DAC_OVERRIDE.
Pasul 3: Blocați sistemul de fișiere
Sistemele de fișiere în care se poate scrie sunt o suprafață comună de atac. Am făcut sistemul de fișiere rădăcină read-only (--read-only) pentru toate containerele, iar apoi am montat sisteme de fișiere temporare (tmpfs) pentru directoare care au nevoie de acces de scriere, cum ar fi /tmp și directorul cache al aplicației. Acest lucru împiedică un atacator să modifice codul aplicației sau să persiste binare malițioase.
În plus, am folosit opțiunea --mount a Docker pentru a monta bind directoare sensibile, cum ar fi socketul Docker, doar când este absolut necesar — și niciodată pe containerele de producție. Principiul: dacă containerul nu are nevoie să scrie într-o cale, faceți-o read-only.
Pasul 4: Aplicați profiluri seccomp și AppArmor
Profilurile seccomp implicite blochează deja multe apeluri de sistem periculoase, dar le-am personalizat și mai mult pentru a permite doar apelurile de sistem de care aplicația noastră are nevoie. Acesta este un compromis deoarece necesită profilarea aplicației. O abordare mai simplă este să folosiți profilul seccomp implicit al Docker și apoi să adăugați --security-opt seccomp=path/to/profile.json dacă aveți nevoie de reguli mai stricte. În mod similar, profilurile AppArmor pot restrânge procesele containerului la anumite căi de fișiere și capabilități. Am activat AppArmor și am folosit un profil personalizat care a restricționat accesul doar la directoarele de date ale aplicației.
Pentru un ghid cuprinzător al acestor pași de întărire, consultați Întărirea containerelor Docker pentru hosting multi-tenant: Un ghid de izolare pas cu pas.
Perspectiva contrară: Uneori aveți nevoie de VM-uri
Indiferent cât de întărite, containerele partajează kernelul gazdei. O vulnerabilitate a kernelului poate sparge toată izolarea deodată. De aceea multe platforme orientate spre securitate rulează containere în interiorul unor VM-uri ușoare — fiecare chiriaș primește propriul kernel. Acest lucru adaugă overhead, dar oferă o graniță la nivel hardware pe care containerele singure nu o pot oferi. Dacă chiriașii dumneavoastră gestionează date de card de credit sau înregistrări medicale, o abordare hibridă (containere în interiorul VM-urilor) poate fi alegerea corectă. Nu presupuneți că izolarea containerelor este suficientă pentru modelul dvs. de amenințare; evaluați sensibilitatea datelor și cerințele de reglementare.
Pentru o comparație mai profundă a nivelurilor de izolare, citiți Proiectarea unei arhitecturi Docker multi-tenant: Alegerea nivelului potrivit de izolare.
Concluzie: Izolarea este un stack, nu un comutator
Reparația nu a fost o singură modificare — a fost o stratificare: izolare de rețea, privilegii limitate, sisteme de fișiere read-only și filtrare a apelurilor de sistem. Chiar și atunci, am acceptat că izolarea perfectă este imposibilă cu containere care partajează kernelul. Pentru chiriașii noștri cu cea mai înaltă securitate, i-am mutat pe gazde dedicate. Lecția: nu aveți încredere în nicio setare implicită. Auditați configurația Docker ca și cum o breșă s-ar fi întâmplat deja. Momentul pentru a bloca este înainte de scurgere, nu după.


