Jeśli aplikacja (np. serwer http) jest uruchamiana pod systemem operacyjnym, w szczególności np. z uprawnieniami roota, w przypadku umiejętnego wykorzystania błędu atakujący może uzyskać bardzo szerokie uprawnienia, m.in. nieograniczony dostęp do systemu plików. Włamywacze są bardzo łasi na wynajdowanie takich podatności.
W celu uniknięcia takich zagrożeń:
http
lub www
);W systemie Linux można uruchomić dowolny proces (i jego procesy potomne) ze zmienionym katalogiem głównym za pomocą polecenia chroot
(można to też zrobić wewnątrz programu za pomocą wywołania systemowego chroot()
). Uruchomiony w ten sposób proces nie może powrócić do rzeczywistego katalogu głównego w systemie plików. Można więc przygotować inny niż główny katalog zawierający pliki niezbędne dla aplikacji np. biblioteki czy pliki konfiguracyjne. Zwykle trzeba odwzorować część struktury katalogów i plików konfiguracyjnych, np. /etc, /usr/lib/, /var, /proc. Mechanizm chroot
może być też przydatny dla celów testowania oprogramowania lub np. zrealizowania środowiska programistycznego ze specyficznym zestawem bibliotek i narzędzi. Do zalet należy także zaliczyć bardzo łagodną krzywą uczenia się przy korzystaniu z tego rozwiązania i brak konieczności modyfikacji jądra systemu operacyjnego.
chroot
Jeśli docelowy katalog został przygotowany, można uruchomić proces, np.:
chroot /przygotowany_katalog /usr/sbin/apache2 -k start
chroot
Sam w sobie chroot
nie posiada mechanizmów limitowania zasobów używanych przez proces, użycie go nie zabezpiecza więc przed atakami DoS. (Ograniczenia takie można wprowadzić za to mechanizmem PAM).
Uruchomienie polecenia chroot
wymaga uprawnień roota.
Chroot
nie zmienia UID i GID procesu. Jeśli aplikacja sama nie zmieni UID, będzie wykonywana z prawami roota.
Istnieją sposoby, które pozwalają procesowi uruchomionemu z prawami roota, wyjście poza określony poleceniem chroot katalog -
opisano je w lekturze uzupełniajacej http://linux-vserver.org/Secure_chroot_Barrier.
Można przy okazji zacytować fragment listu Alana Coxa: "(...) chroot is not and never has been a security tool." :)
Jeżeli chcemy mimo wszystko przygotować katalog chroot
, możemy ułatwić sobie życie na dwa sposoby. Po pierwsze można przygotować minimalną instalację jakiejś dystrybucji, aby uniknąć zgadywania, które pliki są potrzebne do pracy naszego programu; dotyczy to zwłaszcza bibliotek korzystających z wielu plików pomocniczych. Trzeba jednak pamiętać, aby nie umieszczać w systemie zbędnych programów z bitem SUID, które mogłyby być wykorzystane do „ucieczki” z chroota
. Warto też pamiętać o poleceniu
mount --bind /proc /przygotowany_katalog/proc mount --rbind /dev /przygotowany_katalog/dev
pierwsza wersja powoduje, że pliki z systemu plików (lub jakiegoś katalogu z systemu plików) widocznego jako /proc będą też widoczne w katalogu /przygotowany_katalog/proc
. Nie dotyczy to jednak katalogów podmonotwanych wewnątrz proc, np. /proc/sys/fs/binfmt_misc
. Natomiast druga wersja obejmuje także katalogi podmontowane wewnątrz dowiązywanego katalogu.
Chociaż opanowanie podstawowej funkcjonalności chroot
jest łatwe, to jak zaznaczyliśmy powyżej, użycie chroot
do stworzenia szczelnie zamkniętego środowiska wykonawczego dedykowanego dla jednej aplikacji jest żmudne i wymaga sporego doświadczenia. Dlatego opracowane zostały bardziej kompleksowe rozwiązania, które pozwalają na lepsze opanowanie wskazanych trudności.
Naturalnym roszerzeniem chroota
jest objęcie ograniczeniami także innych zasobów niż system plików. W idealnym świecie uruchomiony w takim ulepszonym chroocie
– czyli kontenerze – program powinien zachowywać się tak, jakby był uruchomiony na maszynie wirtualnej na tym samym komputerze i z oddzielną kopią tego samego jądra. Jednak ponieważ naprawdę nie uruchamiamy nowego jądra, to nie występuje narzut spowodowany koniecznością obsługi wywołań systemowych przez dwa jądra, wirtualizacją urządzeń sprzętowych itp. Możemy także, jeżeli tego potrzebujemy, osłabić izolację w jakimś miejscu, aby programy bardziej efektywnie współpracowały. Ograniczeniem jest konieczność używania takiego samego jądra we wszystkich używanych kontenerach i systemie bazowym (choć to ograniczenie w ostatnich czasach zostało zupełnie zniwelowane).
W Linuksie implementacja kontenerów opiera się na uogólnieniu pojącia chroot
do pojęcia namespace. Są one następujące i pozwalają izolować:
Namespace Constant Isolates Cgroup CLONE_NEWCGROUP Cgroup root directory IPC CLONE_NEWIPC System V IPC, POSIX message queues Network CLONE_NEWNET Network devices, stacks, ports, etc. Mount CLONE_NEWNS Mount points PID CLONE_NEWPID Process IDs User CLONE_NEWUSER User and group IDs UTS CLONE_NEWUTS Hostname and NIS domain name
Tabelka pochodzi z man namespace(7). Tę stronę podręcznika systemowego należy przeczytać, a strony zależne przejrzeć. Należy także zapoznać się z możliwościami nakładania ograniczeń na procesy za pomocą grup cgroups(7).
Na podstawie tej infrastuktury zbudowanych jest kilka środowisk dających wygodne narzędzia do wirtualizacji, są to. np. Linux Containers (LXC), OpenVZ, Docker. Na tych zajęciach przyjrzymy się bliżej jako przykładowi temu ostatniemu rozwiązaniu.
Docker składa się z procesu serwera, który odpowiada za faktyczne tworzenie kontenerów, ich modyfikacje, itp. oraz klienta, którego funkcją jest zapewnianie możliwości korzystania z serwera. W celu skonfigurowania nowego kontenera, tworzymy w pustym katalogu plik o nazwie Dockerfile z zawartością odpowiadającą naszym potrzebom. Oto przykładowa zawartość:
FROM debian:buster # O ile chcemy, aby proces działał w środowisku Debian 10 MAINTAINER Kto To Wie RUN apt-get update && apt-get install -y apache2 EXPOSE 80 CMD apachectl -D FOREGROUND
pierwsza linia oznacza obraz systemu operacyjnego, z którego korzystamy na początku pracy. Docker ma centralny rejestr obrazów, z których można korzystać przy tworzeniu kontenerów, są w nim w szczególności podstawowe instalacje różnych dystrybucji. Jeśli chcemy uniezależnić się od twórców Dockera, można też uruchomić własny taki serwer.
Linia z wpisem MAINTAINER jest wymagana, ale ma charakter informacyjny. Następnie mamy (być może kilka) linii z poleceniami RUN. Opisują one polecenia potrzebne do przygotowania docelowej konfiguracji kontenera. W celu uniknięcia zbyt częstego wykonywania tych samych poleceń Docker zapamiętuje stan kontenera po każdej linijce z pliku Dockerfile w sposób analogiczny do migawek zwykłych maszyn wirtualnych.
Polecenie EXPOSE 80 udostępnia światu port 80. Polecenie CMD określa polecenie, które będzie służyło do faktycznego uruchomienia kontenera - w tym przypadku jest to serwer Apache. Gdy podany proces zakończy działanie Docker zatrzyma wszystkie procesy w kontenerze
W celu uruchomienia naszego kontenera wchodzimy do odpowiedniego katalogu z plikiem Dockerfile i wykonujemy polecenia:
docker build . docker container run <id_obrazu>
lub krócej
docker run <id_obrazu>
Polecenie docker build .
zbuduje obraz na podstawie pliku Dockerfile (w tym przypadku na Debianie 10, ale np. z dołączonymi naszymi plikami), polecenie docker run id_obrazu
uruchomi proces serwera Apache w nowym kontenerze w środowisku naszego obrazu.
Polecenie
docker images
lub
docker image ls
pozwala sprawdzić identyfikator (id) obrazu.
Jeśli chcemy szybko uruchomić proces w nowym kontenerze (w środowisku Debian 10) bez pisania pliku Dockerfile, możemy napisać
docker run -t -i debian:buster /bin/bash
Polecenie docker run
wykona wtedy kilka kroków:
docker pull debian:buster
Należy zwrócić uwagę, że istnieje duża różnica między docker run id_obrazu
, a docker start id_kontenera
. (W tym samym znaczeniu można wykonać docker container run id_obrazu
lub docker container start id_kontenera
). Polecenie Dockera run zawsze tworzy nowy kontener.
Kilka przykładów operacji na kontenerach:
docker container ls -a (listowanie wszystkich) docker container run <id_obrazu> (uruchamia proces w nowym kontenerze, w środowisku obrazu o danym id) docker container stop <id_kontenera> (zatrzymywanie kontenera) docker container start <id_kontenera> (uruchamianie zatrzymanego kontenera) docker container rm <id_kontenera> (usuwanie) docker container prune (usuwanie wszystkich zatrzymanych)
Można też oczywiście usuwać obrazy (docker image rm id_obrazu
lub docker image prune
-- usuwa wszystkie nieużywane). Tak jak było widać już wcześniej, np. polecenie docker stop
wykona to samo, co docker container stop
, ale nie zadziała docker prune,
trzeba użyć docker image prune
lub docker container prune
. Pierwsze usuwa obrazy, drugie kontenery. Warto sprawdzić jakie opcje są dostępne dla danego polecenia Dockera: docker --help
, docker container --help
, docker image --help
, docker container ls --help
itd.
Należy pamiętać, że obraz (image) i kontener (container) to nie to samo. Obraz daje możliwość utworzenia środowiska
dla procesu w kontenerze, środowiska opartego na konkretnym systemie operacyjnym (Debianie 9, Debianie 10, Ubuntu itp).
Można powiedzieć, iż kontenery korzystają z obrazów, z jednego obrazu może korzystać wiele kontenerów. Więcej informacji
na temat przechowywania danych, warstw z których składa się dockerowa pamięć nieulotna można znaleźć na https://docs.docker.com/storage/storagedriver/.
Docker oraz plik Dockerfile mają jeszcze wiele możliwości, których nie poznaliśmy. Co ciekawe, od wersji Docker Engine 19.03, istnieje możliwość pracy w trybie nie roota, w tej wersji cecha ta jest uważana za eksperymentalną. Do używania w tym trybie zalecana jest najnowsza wersja 20.10.
Warto też nadmienić, że chociaż początkowo korzystanie z Dockera pod Windows skazywało nas na korzystanie z systemów windowsowych wewnątrz kontenerów, a korzystanie z Linuksa z systemów linuksowych, to obecnie te ograniczenia zostały pokonane i pod Windows można korzystać z Linuksa i vice versa (choć w nieco bardziej skomplikowany sposób).
Pełna dokumentacja Dockera jest dostepna pod adresem https://docs.docker.com/get-started/overview/.
Zainstaluj dockera, pod Debianem 10 wystarczy:
apt-get install docker.io
docker pull debian:buster
docker run -i -t debian:buster bash -l
Jako ćwiczenie umieść w kontenerze pliki swojego rozwiązania zadania z tematu ACL i SUDO oraz program z obsługą PAM-a napisany na zajęciach z PAM-a. Uruchom w dokerze skrypt napisany z zadania z tematu ACL i SUDO i skompiluj program z PAM-em, a następnie uruchom skompilowany program. Odpowiedni Dockerfile prześlij na Moodle.