Wstęp

Konteneryzacja, Docker to jedna z najszybciej rozwijających się technologii w ostatnich latach. Popularność konteneryzowania w Dockerze bierze się nie tylko z faktu, że niezwykle łatwo jest zarządzać aplikacją, skalować ją i aktualizować, ale też z szybkości jaką oferują wdrożenia, zmiany, które robimy z użyciem tej technologii. Ogromną popularnością cieszy się publiczne miejsce, w którym najwięksi dostawcy oprogramowania i nie tylko mogą publikować gotowe obrazy z usługami takimi jak MySQL, Nginx, Apache, PHP i wiele więcej. Jednym kliknięciem możemy uruchomić na dowolnym serwerze kompletne środowisko developerskie czy produkcyjne.

To co jednak sprawia, że Docker staje się coraz bardziej popularny jest jednocześnie jednym z największych wyzwań dla osób zajmujących się bezpieczeństwem środowisk opartych o kontenery i Dockera. Nie jest tajemnicą, że gotowe obrazy są tworzone w taki sposób, aby zmaksymalizować dostępność we wszystkich środowiskach i pozwolić na bezproblemowe uruchomienie. Nie mniej nie więcej, nie znajdziemy w oficjalnych obrazach zbyt wiele o bezpieczeństwie, bo przecież ma działać wszędzie – tam gdzie bezpieczeństwo jest priorytetem i tam gdzie nikt nie ma o tym pojęcia. Obraz powinien być uniwersalny i po prostu działać, a reszta to odrębna sprawa.

W większości przypadków obrazy na dockerhubie posiadają swoją mini-dokumentację, dzięki której można właściwie jedną linijką uruchomić dowolny obraz dockera z dockerhuba. Problem w tym, że zwykle ta jedna linijka lub fragment pliku docker-compose.yml pokazuje, jak uruchomić kontener nasłuchujący bez żadnych zabezpieczeń na wszystkich interfejsach serwera.

W dzisiejszym artykule pokażę, jak w stosunkowo prosty sposób podnieść nieco poziom bezpieczeństwa uruchamianych serwisów i jak nie wpaść w pułapkę bezpieczeństwa z tym związaną.

Instalacja Docker’a.

Niezbędne pakiety:

apt-get remove docker docker-engine docker.io containerd runc

apt-get -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

Dodajemy klucz do repozytorium:

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add –

Dodajemy repozytorium:

add-apt-repository „deb [arch=amd64] https://download.docker.com/linux/debian  $(lsb_release -cs) stable”

Instalacja:

apt-get update

apt-get -y install docker-ce docker-ce-cli containerd.io

Przykładowy obraz Docker zaciągnięty z dockerhuba

Aby lepiej zobrazować o co chodzi w tym artykule posłużę się dowolnym, pierwszym z brzegu obrazem zaciągniętym z dockerhuba. Na pierwszy ogień pójdzie najpopularniejszy serwer www Apache2.

Link do obrazu: https://hub.docker.com/_/httpd

Aby uruchomić podstawowy obraz Apache2 wystarczy jedna linijka:

docker run -dit –name my-apache-app -p 8080:80 -v „$PWD”:/usr/local/apache2/htdocs/ httpd:2.4

Apache2 już nasłuchuje i jest gotowy do pracy.

Jak widać nasłuchuje on na wszystkich interfejsach na porcie 8080, a dodatkowo redirectuje port 8080 do portu 80 wewnątrz kontenera.

Być może Apache2, który z reguły i tak jest wystawiany publicznie to nie jest idealny przykład obrazujący problemy bezpieczeństwa, o których pisałem wyżej, ale chodzi o zasadę. Weźmy inny obraz, np MySQL:

Link do obrazu: https://hub.docker.com/_/mysql

Uruchomienie kontenera:

docker run –name mysql-server -e MYSQL_ROOT_PASSWORD=haslo -p 3306:3306 -d mysql:5.7.30

W tym przypadku port 3306 do usługi MySQL zostaje wystawiony podobnie jak port 8080 dla Apache2 na świat. W tym momencie z dowolnego serwera, komputera lub urządzenia na świecie możemy podłączyć się do portu 3306 gdzie będzie nasza baza danych. Sprawdźmy to telnetem z komputera.

Taka sytuacja już nie jest tak do końca pożądana jeśli ktoś ma świadomość zagrożeń jakie niesie wystawienie portu do usługi bazy danych na świat.

Jak się przed tym zabezpieczyć? Jak zabezpieczyć usługi uruchamiane w kontenerach, aby nie były dostępne na zewnątrz?

Zamykamy usługi kontenerowe do lokalnego środowiska

W poprzednim przykładzie uruchomiliśmy kontener z bazą MySQL za pomocą, tak zwanego one-linera, czyli prostej komendy do uruchamiania obrazów Dockera.

Uruchomienie standardowe:

docker run –name mysql-server -e MYSQL_ROOT_PASSWORD=haslo -p 3306:3306 -d mysql:5.7.30

Gdybyśmy nieco zmodyfikowali tą linijkę na przykład tak:

docker run –name mysql-server -e MYSQL_ROOT_PASSWORD=haslo -p 127.0.0.1:3306:3306 -d mysql:5.7.30

Co tutaj zmieniliśmy? Dodaliśmy do portu adres lokalny – 127.0.0.1. Dzięki temu nasza baza zostaje wystawiona tylko do adresu localhost i jest dostępna tylko z wewnątrz środowiska.

Próba połączenia się z portem 3306 do adresu zewnętrznego zakończy się niepowodzeniem:

No dobrze, ale co z komunikacją między kontenerami, skoro adres IP kontenera zwykle jest w podsieci 172? Tym zajmują się wewnętrzne mechanizmy dockera. Rutują wewnętrznie ruch między kontenerami i komunikacja po wewnętrznym adresie IP powinna być możliwa. Sprawdźmy zatem, jak to działa?

Mamy już uruchomionego Apache2 oraz przed momentem uruchamialiśmy na localhoście usługę MySQL. Adres IP przypisany do tej usługi w systemie to: 172.17.0.3.

Wchodzimy do kontenera z serwerem WWW i próbujemy podłączyć się do bazy danych.

Jak widać komunikacja jest możliwa, mimo zamknięcia usługi z zewnątrz i ograniczenia jej tylko do localhost. W ten sposób możemy zamykać wszystkie usługi kontenerowe i spokojnie wewnętrznie się z nimi łączyć, a z zewnątrz pozostaną niedostępne.

Wyłączamy ipv6

Z reguły niewiele osób i firm korzysta jeszcze z ipv6. Docker po uruchomieniu standardowego kontenera binduje na wszystkich możliwych interfejsach w tym dla protokołu ipv6. W sytuacji, w której nie możemy ograniczyć wszystkich usług tylko do adresów lokalnych, mamy firewalla (zwykle tylko na ipv4), warto wyłączyć protokół ipv6, o którym zwykle mniej doświadczeni zapominają, pozostawiając otwartą furtkę na cyberzagrożenia.

Jak w prosty sposób wyłączyć ipv6 w systemie Linux?

Najprościej i zarazem najskuteczniej pozbędziemy się ipv6 po prostu dodając flagę do bootloadera i przebudowując go.

nano /etc/default/grub

Dopisujemy: ipv6.disable=1 do obu opcji Grub’a.

GRUB_CMDLINE_LINUX_DEFAULT=”ipv6.disable=1 pozostałe_wpisy”

GRUB_CMDLINE_LINUX=”ipv6.disable=1″

Zapisujemy i przebudowujemy bootloadera.

update-grub

Restartujemy serwer. To obowiązkowe, aby załadowane zostały zmiany.

reboot

Jak wyglądało to przed wyłączeniem ipv6?

Jak po wyłączeniu ipv6?

Jak widać nie ma już obsługi ipv6 i Docker też na niego nie binduje.

Firewall

Zbudowanie skutecznego firewalla na serwerze, tam gdzie mamy Dockera może nie być tak proste jak się wydaje zwłaszcza tam gdzie chcemy cokolwiek udostępniać na zewnątrz. Dlaczego? Docker podczas startu demona oraz później kontenerów dokłada do iptables własne regułki, które z reguły są ładowana na górze stosu. Iptables w tym momencie nawet jeśli mamy już przygotowanego własnego firewalla i własny zestaw reguł prawdopodobnie nie będzie do końca skuteczny.

Zajrzyjmy do dokumentacji Dockera, do rozdziały traktującego o iptables: https://docs.docker.com/network/iptables/

Znajdziemy tam wpis, który powinien rozwiać wszystkie wątpliwości co do stosowania iptables (a pamiętajmy, że inne nakładki na firewalla linux i tak pod spodem dodają regułki do iptables, więc zachowanie będzie podobne).

“It is not possible to completely prevent Docker from creating iptables rules, and creating them after-the-fact is extremely involved and beyond the scope of these instructions”

Z dokumentacji oficjalnej dowiadujemy się, że choć można dodać wpis informujący, aby Docker nie manipulował przy iptables, nie da się kompletnie wyeliminować tego zjawiska. Docker do komunikacji między sobą i z usługami wymaga ustawienia regułek komunikacyjnych. To powoduje, że takie zabezpieczenie nie będzie w pełni skuteczne.

Rozwiązaniem tutaj może być przede wszystkim firewall nadrzędny. To znaczy, hosty na których utrzymujemy dockera, powinny być schowane, bez dostępu do sieci zewnętrznej i zewnętrznego interfejsu. Na froncie powinien stać load balanser, proxy lub inny sprzętowy firewall. Innym pomysłem na rozwiązanie tego jest firewall dostarczany przez dostawcę serwera, chmury lub node’a. Wielu dostawców, jak na przykład DigitalOcean, Azure, AWS dostarcza swojego firewalla, którego można włączyć i skonfigurować nadrzędnie nad instancją serwera. Taki firewall działa niezależnie i nie będzie modyfikowany ustawieniami Dockera. Podobne usługi oferują niektóre DataCenter przy wykupieniu serwera VPS czy dedykowanego. Taki nadrzędny firewall jest najlepszym i najbardziej rozsądnym rozwiązaniem, także dlatego, że z reguły dostawcy za jego uruchomienie nie doliczają dodatkowych opłat.

Podsumowanie

Jak widać, w stosunkowo prosty, niekoniecznie skomplikowany sposób możemy spokojnie podnieść przynajmniej o dwa poziomy bezpieczeństwo usług uruchomionych przy pomocy najszybciej rozwijającej się technologii kontenerowej jaką jest docker. Tak jak wspomniałem na początku, obrazy gotowe są głównie tworzone pod kątem działania wszędzie i na wszystkich możliwych konfiguracjach (no może na jak największej ich liczbie), dlatego nie są zbyt skoncentrowane na bezpieczeństwie bo też trudno określić twórcy, co akurat kto ma w systemie i na jakich portach chce pracować.

Da się jednak stosunkowo niewielkim kosztem pracy ograniczyć ryzyko wystawienia niepotrzebnie kontenera na świat, na przykład z bazą danych, jak to zobrazowaliśmy na dzisiejszym przykładzie.