Container Hardening – Deo 1: Docker – bezbednost hosta i osnovna izolacija
Ovim tekstom počinje nova serija posvećena bezbednosti kontejnerizovanih radnih opterećenja — prvo Docker, zatim Kubernetes u nastavku serije. Kontejneri se često percipiraju kao inherentno izolovani i time bezbedni po difoltu — pull-uješ image, pokreneš ga, i on radi u sopstvenom, odvojenom prostoru. Ova percepcija je delimično tačna i delimično opasna: izolacija kontejnera je stvarna, ali nikad apsolutna, i podrazumevana Docker konfiguracija je optimizovana za praktičnost prvog korišćenja, ne za produkcionu bezbednost. Prvi deo pokriva ono što je fundamentalno — bezbednost samog Docker daemon-a i hosta na kojem radi.
Zašto host-nivo hardening dolazi prvi
Isti princip koji smo primenili na Hyper-V u ranijoj seriji na ovom blogu važi identično i ovde: kontejner nije virtuelna mašina sa sopstvenim jezgrom (kernel-om) — svi kontejneri na jednom hostu dele isti Linux kernel host mašine. To znači da je granica izolacije tanja nego kod pune virtuelizacije, i da kompromitacija Docker daemon-a ili hosta praktično poništava svaku izolaciju koju kontejneri međusobno imaju. Bezbednost hosta je gornja granica bezbednosti svega što se na njemu izvršava — princip koji se ponavlja kroz svaku seriju na ovom blogu, jer je fundamentalno tačan u svakom sloju infrastrukture.
Docker socket — najvažnija stvar koju treba razumeti u celoj seriji
Docker daemon (proces koji stvarno upravlja kontejnerima) se podrazumevano kontroliše preko Unix socket-a na putanji /var/run/docker.sock. Ovo je tehnički detalj koji zaslužuje detaljno objašnjenje, jer njegovo nerazumevanje je uzrok velikog broja stvarnih bezbednosnih incidenata: bilo šta što ima pristup pisanja na taj socket ima, u praksi, pun root pristup samom hostu — ne samo kontrolu nad kontejnerima.
Razlog je jednostavan kad se objasni: preko tog socket-a je moguće narediti Docker daemon-u da pokrene novi kontejner sa proizvoljnim podešavanjima — uključujući podešavanje koje montira ceo fajl sistem hosta (korenski direktorijum "/") unutar tog novog kontejnera. Kad se to uradi, granica između "unutar kontejnera" i "na hostu" praktično nestaje — proces unutar tog kontejnera može da čita, piše i menja bilo šta na hostu, jer mu je host fajl sistem direktno dostupan. Zbog toga je pristup Docker socket-u ekvivalentan root pristupu na hostu, bez obzira na to koliko "obično" izgleda proces koji taj pristup ima.
Šta treba uraditi
- Nikad ne montirati Docker socket (/var/run/docker.sock) unutar kontejnera, čak i kad neki alat ili tutorijal to predlaže kao "praktično rešenje" za kontejner koji treba da upravlja drugim kontejnerima — ovo je jedna od najčešćih, a najozbiljnijih grešaka u praksi
- Ako je alatu zaista, dokumentovano potreban pristup Docker API-ju (npr. alat za monitoring ili CI/CD koji pokreće build-ove), koristiti posrednički proxy koji ograničava koje tačno operacije taj alat sme da izvrši preko socket-a, umesto direktnog pristupa punom API-ju
- Ako se Docker daemon izlaže preko mreže (TCP, portovi 2375/2376) umesto lokalnog socket-a, obavezno koristiti uzajamnu TLS autentifikaciju (mTLS) — nešifrovan TCP pristup Docker daemon-u je ekvivalentan ostavljanju root pristupa hostu otvorenim ka mreži bez ikakve lozinke
- Ako mrežni pristup daemon-u uopšte nije potreban (najčešći slučaj), ne uključivati ga — koristiti lokalni Unix socket i, gde je udaljeno upravljanje zaista potrebno, SSH-bazirano povezivanje koje ponovo koristi već uspostavljene SSH ključeve i pristup opisan u ranijoj seriji o hardeningu Linux servera
Napomena o riziku: ovo nije podešavanje koje "možda malo povećava rizik" — nezaštićen pristup Docker socket-u ili daemon-u je direktan, potpun put do kompromitacije celog hosta, ne samo jednog kontejnera. Ako je bilo koji postojeći kontejner u infrastrukturi trenutno montira socket, tretirati to nalaz kao hitan prioritet za ispravku, ne kao stavku za "kad se nađe vremena".
Privileged mod — gotovo nikad stvarno potreban
Docker nudi opciju pokretanja kontejnera u privilegovanom modu (--privileged), koja kontejneru daje praktično sve Linux mogućnosti (capabilities) i uklanja veći deo standardnih cgroup ograničenja. Ovo efektivno poništava veliki deo izolacije koju kontejner inače pruža — privilegovan kontejner može da radi gotovo sve što bi mogao i proces direktno na hostu.
Ovaj mod postoji za retke, legitimne slučajeve (npr. kontejner koji upravlja samom Docker infrastrukturom, ili direktno pristupa hardveru hosta), ali se u praksi često koristi kao brzo rešenje kad neka aplikacija "ne radi" bez njega — umesto da se identifikuje tačna, uža dozvola koja aplikaciji zaista nedostaje.
Šta treba uraditi
- Tretirati zahtev za --privileged modom kao signal za detaljniju analizu, ne kao rutinsko rešenje — u velikoj većini slučajeva, aplikaciji je potrebna samo jedna ili dve konkretne dodatne mogućnosti, ne sve odjednom
- Umesto punog privilegovanog moda, koristiti selektivno dodavanje konkretnih Linux capabilities (--cap-add) uz istovremeno uklanjanje svih podrazumevanih (--cap-drop=ALL kao polazna tačka, zatim dodati samo ono što je zaista potrebno)
- Uključiti opciju --security-opt=no-new-privileges, koja sprečava proces unutar kontejnera da eskalira svoje privilegije čak i preko setuid izvršnih fajlova ili sličnih mehanizama — ovo je jednostavna, gotovo besplatna dodatna zaštita koja bi trebalo da bude podrazumevana za svaki kontejner
Rootless Docker — uklanjanje privilegije, ne samo ograničavanje
Podrazumevano, i sam Docker daemon i kontejneri koje pokreće rade sa root privilegijama na hostu. Ovo znači da ranjivost u samom daemon-u, ili uspešan "escape" iz kontejnera (izlazak iz izolacije nazad na host), automatski daje napadaču root pristup hostu, jer je upravo tim privilegijama daemon i radio.
Rootless Docker rešava ovaj problem na fundamentalnom nivou: sam daemon radi kao običan, neprivilegovan korisnik, koristeći Linux user namespace mehanizam da simulira root privilegije unutar kontejnera bez stvarnih root privilegija na hostu. Ako dođe do escape scenarija u rootless konfiguraciji, napadač izlazi u kontekst neprivilegovanog korisnika na hostu, ne u root kontekst — razlika koja u praksi znači razliku između ozbiljnog, ali ograničenog incidenta i potpune kompromitacije hosta.
Šta treba uraditi
- Za nove implementacije, razmotriti rootless Docker kao podrazumevani izbor, posebno na hostovima koji pokreću više kontejnera ili kontejnere iz manje pouzdanih izvora
- Gde rootless mod nije izvodljiv (neki mrežni ili storage scenariji imaju ograničenja u rootless konfiguraciji), koristiti user namespace remapping (userns-remap podešavanje u daemon.json) kao međukorak — ovo mapira root korisnika unutar kontejnera na neprivilegovanog korisnika na hostu, umanjujući, iako ne u potpunosti eliminišući, isti rizik
- U samom Dockerfile-u, koristiti USER direktivu da se aplikacija unutar kontejnera izvršava kao neprivilegovan korisnik, nezavisno od toga da li je sam daemon rootless — ovo je dodatan, komplementaran sloj zaštite, ne zamena za host-nivo meru
Napomena o riziku: prelazak na rootless Docker na postojećoj, aktivnoj infrastrukturi je netrivijalna migracija — menja se putanja do socket-a, neka mrežna podešavanja rade drugačije, i pojedini volume mount scenariji zahtevaju prilagođavanje. Ovo planirati kao poseban projekat sa testiranjem u odvojenom okruženju, ne kao brzu izmenu konfiguracije na produkcionom hostu. Za nove implementacije, ovaj trošak se izbegava u potpunosti postavljanjem rootless moda od samog početka.
Mrežna izolacija između kontejnera
Podrazumevana Docker mreža (bridge) dozvoljava svim kontejnerima na njoj da međusobno komuniciraju bez ograničenja — praktično, "svako sa svakim" pristup, čak i kad aplikacije koje ti kontejneri pokreću nemaju stvarnu poslovnu potrebu za tom komunikacijom. Ovo je isti obrazac koji smo kritikovali u seriji o Windows Server hardeningu za mrežnu segmentaciju uopšte: podrazumevano "sve dozvoljeno" umesto svesno definisanog, ograničenog pristupa.
Šta treba uraditi
- Kreirati odvojene, namenske Docker mreže za grupe kontejnera koje zaista treba da komuniciraju, umesto oslanjanja na podrazumevanu bridge mrežu za sve
- Onemogućiti međusobnu komunikaciju kontejnera na podrazumevanoj mreži (icc: false u daemon.json) kao polaznu, restriktivniju tačku, uz eksplicitno kreiranje mreža tamo gde je komunikacija zaista potrebna
- Za mreže koje ne treba da imaju izlaz ka internetu (npr. interna mreža za komunikaciju baze podataka i aplikacije), koristiti internal:true opciju, čime se ta komunikacija ograničava isključivo na kontejnere u toj mreži
- Vezivati (bind) portove kontejnera na localhost (127.0.0.1) umesto na sve interfejse (0.0.0.0), osim kad je spoljna dostupnost zaista namerna i potrebna — isti princip opisan u ranijoj seriji za servise na Linux serverima uopšte
Sledeći deo serije prelazi na bezbednost Docker image-a — od čega se kontejner gradi, kako proveriti da li sadrži poznate ranjivosti pre nego što uopšte dođe do pokretanja, i zašto je izbor baznog image-a jedna od najuticajnijih odluka u celom procesu.
Comments
Post a Comment