Docker Security #2
Skoro zdefiniowaliśmy sobie pewne płaszczyzny, na których możemy się spodziewać problemów to może czas na skonfrontowanie tego z ewentualnymi rozwiązaniami. Zacznijmy jednak od podstaw. W każdym środowisku, jakie by ono nie było, należy się kierować zasadą stawiania jak największej ilości murów. Jeżeli kontenery, to może w VM, zabezpieczone zaporami ogniowymi, oraz strzeżone pilnym okiem monitoringu wyczulonego na anomalie. Każdy z tych elementów pozwoli nam na zatrzymanie potencjalnego zadymiarza, burzyciela dyletanta przed namieszaniem w infrastrukturze i da nam chwile na reakcję. Taką barierą może być AppArmor, jest na pokładzie Ubuntu, ale żeby zacząć jej używać należy się mała konfiguracja:
[gdlr_core_code style=”light” ]
mkdir -p /etc/apparmor.d/containers/
nano /etc/apparmor.d/containers/docker-nginx
[/gdlr_core_code]
Za pomocą tych dwóch komend tworzymy niezbędny katalog dla AppArmor, a następnie plik konfiguracyjny, który wypełniamy treścią z tego linka:
[gdlr_core_code style=”light” ]
https://docs.docker.com/engine/security/apparmor/#nginx-example-profile
[/gdlr_core_code]
To jest przykładowy plik z zasadami dla Nginx’a, ale w internecie znajdziecie bardzo dużo przykładów dla innych usług. Pliki również można tworzyć samemu i do tego też dostępne jest mnóstwo materiałów. Jak już przekopiujecie treść, należy wykonać:
[gdlr_core_code style=”light” ]
apparmor_parser -r -W /etc/apparmor.d/containers/docker-nginx
docker container list
docker kill [id]
[/gdlr_core_code]
Pierwsza komenda każe AppArmor wczytać plik konfiguracyjny, druga, znana nam już wyświetla listę kontenerów, a trzecia ubija nasz kontener (na podstawie id), kolejnym krokiem jest:
[gdlr_core_code style=”light” ]
docker run –security-opt “apparmor=docker-nginx” -p 8080:80 nginx
docker exec -it [id] bash
root@[id]:~# ping 8.8.8.8
ping: Lacking privilege for raw socket.
root@[id]:/# top
bash: /usr/bin/top: Permission denied
root@[id]:~# touch ~/thing
touch: cannot touch ‘thing’: Permission denied
root@[id]:/# sh
bash: /bin/sh: Permission denied
root@[id]:/# dash
bash: /bin/dash: Permission denied
[/gdlr_core_code]
W pierwszej linii uruchomiliśmy kontener z wcześniej zdefiniowanym profilem AppArmor, a następnie weszliśmy do środka i wykonaliśmy kilka komend, które skończyły się planowanym niepowodzeniem.
Kolejnym elementem, na który trzeba zwrócić uwagę, jest separacja systemu plików kontenera od środowiska Hardware Land. Bardzo często zdarza się, że poza swoją przestrzenią dyskową zawartą w obrazie i warstwach na nim składowanych (np. warstwie danych) aplikacja chce uzyskać dostęp do zasobów poniżej, tzn. na Hoście. Mogą to być rzeczy najróżniejsze – pliki sesji, binarki, logi czy inne bezeceństwa. Dostęp taki realizowany jest przez punkty montowania. Stwórzmy jeden:
[gdlr_core_code style=”light” ]
touch /tmp/szatan.666
ls –alh /tmp/
docker container list
docker kill [id]
docker run -p 8080:80 -v /tmp:/tmp -d nginx
docker kill [id]
[/gdlr_core_code]
Na początku ubiliśmy nasz kontener, następnie stworzyliśmy plik w udziale /tmp, a folder ten podaliśmy jako punkt montowania podczas uruchamiania kontenera. Volie! Nasza aplikacja może sięgnąć do pliku szatan.666 i pobrać z niego dowolne diabelstwa. Warto jednak nadmienić, że może też pisać:
[gdlr_core_code style=”light” ]
docker exec -it [id] bash
ls –alh /tmp
rm /tmp/szatan.666
[/gdlr_core_code]
Ten przykład pokazuje jak łatwo można podczas tworzenia konfiguracji uruchomieniowej Dockera uzyskać dostęp do systemu Hardware Land… Nie muszę chyba mówić o tym co się wydarzy jak /tmp zmienione zostanie na /etc. Zabezpieczamy się przed tym za pomocą falgi tylko do odczytu:
[gdlr_core_code style=”light” ]
touch /tmp/szatan.666
docker run -p 8080:80 -v /tmp:/tmp:ro -d nginx
docker exec -it [id] bash
ls –alh /tmp
rm /tmp/szatan.666
[/gdlr_core_code]
Teraz dostaniemy informację, że nie możemy tego wykonać i o to nam chodziło. Pamiętajmy punkty montowania są niezwykle ważne, a ich ograniczenie albo chociaż limitowanie niezbędne. Pomóc mogą nam poniższe komendy, które staną się naprawdę przydatne jak będziemy mili dziesiątki lub setki kontenerów na hoście:
[gdlr_core_code style=”light” ]
docker inspect -f ‘{{ .Mounts }}’ [id]
docker ps –quiet –all | xargs docker inspect –format ‘{{ .Id }}: Propagation={{range $mnt := .Mounts}} {{json $mnt.Propagation}} {{end}}
[/gdlr_core_code]
Pierwsza przekazuje nam informację na temat tego czy dany kontener ma punkty montowania i jakie, druga natomiast przekopuje się przez wszystkie kontenery na hoście zwracając informacje, czy któryś z nich ma aktywne mounty.
Najważniejszą jednak rzeczą jest upewnienie się, że kontenery nie są uruchamiane jako root. Pozwoli to na zabezpieczenie się przed potencjalnym psotnikiem, huncwotem, rzezimieszkiem, który chciałby po rozrabiać w infrastrukturze poza kontenerem. Weźmy na warsztat następujący przykład:
[gdlr_core_code style=”light” ]
docker kill [id]
touch /tmp/szatan.666
chmod 600 /tmp/szatan.666
docker run -p 8080:80 -v /tmp:/tmp -it nginx bash
rm /tmp/szatan.666
[/gdlr_core_code]
Jak zwykle ubijamy nasz kontener, tworzymy plik w katalogu /tmp i nadajemy mu takie uprawnienia, które pozwalają na interakcję z nim tylko dla root’a. Uruchamiamy kontener mapując katalog /tmp jako punkt montowania i usuwamy plik. Ze względu, że nasz kontener został uruchomiony na poświadczeniach root’a udaje nam się ta sztuka. Spróbujmy jednak ustawić taką flagę podczas uruchamiania kontenera by był włączony na dedykowanym dla niego użytkowiku np.:
[gdlr_core_code style=”light” ]
touch /tmp/szatan.666 && chmod 600 /tmp/szatan.666
docker run -p 8080:80 -v /tmp:/tmp -it –user 10000:10000 nginx bash
rm /tmp/szatan.666
[/gdlr_core_code]
Na początku odtwarzamy plik, który wcześniej usunęliśmy, a potem przy pomocy flagi „–user 10000:10000” uruchamiamy nasz kontener na uprawnianiach użytkownika 10000 w grupie 10000. Usunięcie pliku mimo braku flagi tylko do odczytu się nie powiedzie, ponieważ użytkownik 10K i grupa 10K nie mają uprawnień root.
Kolejnym elementem są tak zwane „Kernel Capabilities”, które umożliwiają bardzo szczegółową kontrolę nad możliwościami, jakie daje konto root’a, dokładnie można je nieco przyciąć. Weźmy nasz serwer Nginx. Generalnie nie potrzebuje on wiele funkcji poza jedną „net_bind_service”, która pozwoli na dopięcie się do portu 80. Resztę zatem można zablokować, a robi się to tak:
[gdlr_core_code style=”light” ]
docker run -p 8080:80 -v /tmp:/tmp -it –cap-drop ALL –cap-add NET_BIND_SERVICES nginx bash
chown nobody /
[/gdlr_core_code]
Dzięki dodaniu flagi „–cap-drop ALL –cap-add NET_BIND_SERVICES” pozbywa się wszystkich uprawnień specjalnych roota, a co za tym idzie niby kontener jest uruchomiony na root, ale nie do końca działają z pełnym dostępem. Komenda chown się np. nie powiedzie, jak również wiele innych. Jeżeli chcemy dodać możliwość zmiany uprawnień musimy w –cap-add dodać parametr CHOWN. Listę podstawowych „Karnel Capabilites” możecie uzyskać tutaj:
[gdlr_core_code style=”light” ]
http://man7.org/linux/man-pages/man7/capabilities.7.html
[/gdlr_core_code]
Bardzo często zdarza się, że kontener, jak również i jego plik konfiguracyjny lub opcje uruchomienia, są przygotowywane przez dział programistyczny. Jest to pierwszy i najgrubszy błąd, który popełnia się na wstępie przygody z konteneryzacją. Nigdy, ale to przenigdy nie pozwalaj swoim developerom dostarczać ci kontenera. To tak jak byś brał od nich te obślizgłe, pełne niepotrzebnego softu i porno w cache przeglądarki maszyny wirtualne z aplikacją – zawsze za duże, zawsze niezaktualizowane i zawsze jakieś takie ciągnące rozkładem i katastrofą. Zatem nie, wielkie nie. Kontenery budujesz sam, a aplikację wraz z wymaganiami (przejrzanymi przez Ciebie) dostarcza dział programistyczny. Następnie ty jako odpowiedzialny za infrastrukturę decydujesz ile zasobów przyznać dla danego kontenera i jak monitorować, a robisz to tak:
[gdlr_core_code style=”light” ]
docker run –security-opt “apparmor=docker-nginx” –security-opt “no-new-privileges” –health-cmd “curl –fail http://localhost:8080 || exit 1” –health-interval 5s –health-timeout 3s –restart on-failure:5 -p 10.0.0.3:8080:80 -d –cpus 1 -m 128m –cpu-shares=1024 nginx
[/gdlr_core_code]
Komenda jak z lat 90 – cztery długości monitora 14” w 60Hz. Nie martw się tym jednak, wszystko można zapisywać w plikach konfiguracyjnych, które będą ci automatycznie dostarczane z repozytorium. Nie zaprzątajmy sobie jednak głowy w tym momencie, bo na tapecie mamy przełączniki „–health-cmd “curl –fail http://localhost:8080 || exit 1” –health-interval 5s –health-timeout 3s –restart on-failure:5”, które oznaczają, że chcemy monitorować od strony Docker Engine kontener poprzez komendę „curl –fail http://localhost:8080” i w przypadku, gdy się nie powiedzie mamy go restartować, jeżeli test nie przejdzie 5 razy. Sprawdzanie ma następować co 5 sekund, a timeout wynosi 3 sekundy. Dalej tniemy zasoby „–cpus 1 -m 128m –cpu-shares=1024” co w wolnym tłumaczeniu daje 1 procesor, 128 MB RAM i punkty dostępowe do procesora na poziomie 1024. Punkty dostępowe są wagami z jakimi współzawodniczące kontenery w obrębie jednego hosta będą otrzymywać zasoby. Parametry te pozwolą nam na ograniczanie zasobów poszczególnych kontenerów, jak również i zapewnią bardzo prosty system monitoringu i samoleczenia się infrastruktury.
Przebrnęliśmy przez pewne podstawy konfiguracyjne, ale zasad i dobrych praktyk jest o wiele więcej. Przecież tyle złego może się zdarzyć, gdy nie mamy aktualnej wersji kontenera, jak wymieszamy różne kontenery na jednym hoście, jak uruchamiamy je „z ręki”, a nie za pomocą plików konfiguracyjnych… Mam jednak nadzieję, że już teraz po przeczytaniu tego wprowadzenia masz poczucie, że fakt problemy są, ale jest również i wiele na nie rozwiązań. Gdy zagłębimy się jeszcze bardziej gwarantuje ci, że poczujesz lekką ulgę w kontekście bezpieczeństwa kontenerów. Temat jest długi i szeroki, i będziemy go rozwijać w kolejnych artykułach.
Artykuł został opublikowany na łamach IT Professional.
Poniżej znajdą Państwo linki do wcześniejszych artykułów:
Pingback: Docker Security #4 » Inleo - Infrastruktura IT, Konteneryzacja, Datacenter, Private Cloud, PM