Linux: Blokowanie dostępu do katalogów, plików i poleceń
W różnych sytuacjach następuje konieczność udzielenia dostępu do serwera zewnętrznym użytkownikom. Dobrym przykładem będzie utrzymywanie pewnej infrastruktury, na której inna firma ma za zadanie wdrożyć wybraną aplikację. Typowe aplikacje pisane na przykład w języku PHP w zakresie instalacji na docelowym środowisku nie wymagają pełnego dostępu do systemu jako root. Z reguły ich kod wystarczy umieścić w lokalizacji wskazanej w konfiguracji serwera WWW. Pliki z rozszerzeniem PHP będą przekazywane do interpretera tego języka, a rezultaty przedstawiane użytkownikowi. Podobnie działa to także w przypadku ASP.NET, gdzie jednak częściej używany jest system Windows i serwer IIS — w Linuxie odpowiednikiem jest projekt Mono, niestety nie wszystkie możliwości platformy .NET są wspierane.
Innym przypadkiem może być zwykłe udostępnienie wybranego katalogu klientowi, w ramach którego może pobierać czy przesyłać określone dane. Wtedy również nie ma żadnego uzasadnienia umożliwienie dostępu do całego systemu plików.
W systemach Linux możemy skutecznie izolować takich użytkowników od wrażliwej części środowiska. Przedstawione metody są ogólnie proste do realizacji. Chroot jest faktycznie bardziej zaawansowanym zagadnieniem, ale również ciekawym i w praktyce najmniej uciążliwym dla końcowego użytkownika.
SFTP
Korzystając z protokołu SFTP możemy bezpiecznie (szyfrowanie) pobierać i przesyłać dane do zdalnego hosta. SFTP jest bezpośrednio powiązany z protokołem SSH, który z kolei jest współczesnym (prekursorem był telnet) standardem w środowisku Linux. Używając SSH, zapewniamy zdalny dostęp do powłoki, a w konsekwencji możliwość wykonywania poleceń w systemie. Domyślnie autoryzujemy się z użyciem pary login i hasło, natomiast warto wykorzystywać w tym celu klucze.
W Linux usługi SFTP i SSH działają w ramach jednego daemon’a OpenSSH. Dlatego jeżeli zamierzamy ograniczyć dla danego użytkownika dostęp wyłącznie do SFTP i zablokować możliwość wykonywania poleceń przez SSH, musimy dokonać zmian w pliku /etc/ssh/sshd_config. Znajduje się w nim poniższy fragment:
Zastępujemy /usr/lib/openssh/sftp-server opcją internal-sftp, a następnie dopisujemy taką regułę (w tym przykładzie app to nazwa użytkownika, a ChrootDirectory wskazuje lokalizację katalogu domowego):
Pozwoli to na „zamknięcie” użytkownika w jego katalogu domowym, a także na zablokowanie dostępu SSH (dostęp możliwy wyłącznie z wykorzystaniem SFTP). Restartujemy usługę SSH:
Na koniec musimy pamiętać o zmianie własności katalogu domowego naszego użytkownika. Domyślnie będzie to app:app, a potrzebujemy uzyskać root:app. W innym przypadku przy próbie połączenia SFTP będziemy otrzymywać błąd Broken pipe. Zmiany dokonujemy poleceniem chown:
Można zauważyć, iż wprowadzone zmiany są poprawne. SFTP został ograniczony do katalogu domowego, natomiast połączenie SSH nie dojdzie do skutku — otrzymamy komunikat This service allows sftp connections only.
Pewna wada tego sposobu to brak możliwości zapisu przez użytkownika danych w katalogu / przy połączeniu SFTP. Powodem jest ustawienie własności na root:app. Ciężko to jakkolwiek obejść, ponieważ zmiana praw na 775 będzie ponownie powodować błąd Broken pipe. Użytkownik wciąż jednak posiada pełny dostęp do wszelkich podkatalogów, o ile nie zmienialiśmy własności na tym poziomie (w naszym przypadku może zapisywać do katalogu public_html).
Warto dodać, iż użytkownik cały czas może zalogować się “lokalnie” (tty), a inni użytkownicy mogą z użyciem polecenia su przelogować się na tego użytkownika. Rozwiązaniem jest przypisanie dla naszego użytkownika powłoki /sbin/nologin. Można to wykonać poleceniem usermod:
Próba przelogowania zakończy się teraz komunikatem This account is currently not available.
Opisana metoda jest świetnym wyjściem w sytuacji, gdy użytkownik nie potrzebuje dostępu do powłoki, a wyłącznie do przesyłania swoich plików czy odczytu udostępnionych w ten sposób zasobów. W innych przypadkach sprawdzą się podane w dalszej części alternatywy.
rbash
To specjalna wersja standardowej powłoki bash, która zawiera kilka ograniczeń, np. zablokowanie polecenia cd czy zmienną środowiskową SHELL w trybie read-only. Niestety nie działa dopełnianie poleceń bądź ścieżek (za pomocą klawisza Tab). W praktyce nie jest to osobny program, a tylko zwyczajny link symboliczny do powłoki bash właśnie o nazwie rbash:
Nieprzypadkowo o tym wspominam, ponieważ potrzebujemy usunąć ten symlink. Jest to związane z możliwym obejściem, gdy podczas połączenia SSH użytkownik poprzez parametr -t wskaże ścieżkę do innej powłoki (np. /bin/bash). Po nawiązaniu połączenia zostanie załadowana wskazana powłoka zamiast przypisanej dla niego rbash.
Wykonujemy więc polecenia:
Utworzyliśmy kopię pliku /usr/bin/bash, bo teraz dla każdej powłoki z pliku /etc/shells musimy poleceniem setfacl ustawić prawo tylko do odczytu dla blokowanego użytkownika:
Ten proces można zautomatyzować:
Na koniec przenosimy utworzoną wcześniej kopię do pliku /usr/bin/rbash:
Zmieniamy także domyślną powłokę dla naszego użytkownika:
Podane wyżej instrukcje są wystarczające, ale możemy spróbować ograniczyć dostępne polecenia wyłącznie do kilku wybranych. Aktualnie użytkownik ma dostęp do wszystkich poleceń (oprócz cd) ze ścieżek zdefiniowanych w zmiennej PATH. Ich listę możemy uzyskać poprzez wyświetlenie zawartości tej zmiennej:
Załóżmy, iż użytkownik potrzebuje jedynie poleceń, które bez przeszkód pozwolą mu na pracę w powłoce. Proponuję przyjąć następujące:
Aby osiągnąć zamierzony efekt, w pierwszej kolejności tworzymy osobny katalog przeznaczony na ww. binaria. Kopiujemy do niego odpowiednie pliki (wszystkie są w katalogu /usr/bin):
Ustawiamy właściciela plików .profile i .bashrc w katalogu domowym naszego użytkownika na użytkownika root.
Zawartość zmiennej PATH jest ustalana na podstawie pliku /etc/profile. W moim systemie Debian pierwsze linie wyglądają tak:
Użytkownik root ma więc możliwość uruchamiania poleceń z lokalizacji /usr/local/sbin i /sbin (to również jest symlink do /usr/local/sbin), a reszta ze ścieżek widocznych po instrukcji else. Chcemy, aby ci wszyscy pozostali użytkownicy (z wyłączeniem jednak tych znajdujących się w grupie sudo) mogli wykonywać tylko polecenia z /rbash_bin. W tym celu wystarczy dodać warunek do instrukcji if i zmienić przypisanie zmiennej PATH po instrukcji else:
Działanie jest skuteczne, co potwierdzają próby uruchomienia kilku niedozwolonych poleceń:
chroot
Tak jak wspomniałem, utworzenie chroot to najtrudniejszy z opisanych tutaj sposobów, ale (jak to często bywa) równocześnie najbardziej interesujący. Chroot można określić jako „system wewnątrz systemu”, w którym izolujemy użytkowników czy choćby poszczególne aplikacje (o czym można przeczytać w poniższym dodatku). Z poziomu chroot nie ma dostępu do całości systemu, co oznacza potrzebę zbudowania tego środowiska tak, aby spełniało swoją rolę. W przypadku prostej izolacji użytkowników kroki są z reguły identyczne, natomiast niektóre programy użytkowe (jak chociażby git) wymagają dedykowanych zasobów, które niekoniecznie dodaliśmy do chroot podczas jego tworzenia. Wszystko wymaga testowania i weryfikacji działania.
Na początku ustawiamy właściciela katalogu domowego izolowanego użytkownika na root:
Użytkownik nie będzie miał dostępu do systemu plików, a co za tym idzie — także do ważnych katalogów. Musimy obudować chroot’owane środowisko, a w naszym przypadku jest to katalog /home/users/app. W pierwszej kolejności tworzymy katalog dev i kilka specjalnych plików poleceniem mknod:
Następnie tworzymy katalog bin, do którego kopiujemy potrzebne polecenia. Ich lista jest dłuższa niż w poprzednim przykładzie, ponieważ niektóre są zależne od pozostałych.
Teraz dość żmudny krok, ponieważ potrzebujemy skopiować biblioteki, z których korzystają podane polecenia. Skorzystamy z polecenia ldd. Przykład dla /usr/bin/bash:
Wylistowane zostały biblioteki używane przez powłokę bash wraz z ich lokalizacją. Te właśnie pliki musimy odtworzyć w naszym chroot dla wszystkich z podanych wyżej poleceń w katalogu lib. Wiele z nich jest wspólnych. Katalog lib64 można utworzyć na końcu, ponieważ ostatecznie i tak będzie zawierał tylko ten jeden plik ld-linux-x86-64.so.2.
W naszym przypadku po zakończeniu kopiowania plików katalogi lib i lib64 zawierają poniższe biblioteki:
W tym momencie chroot w swojej podstawowej formie jest gotowy. Niestety nie zadziałają edytory tekstu (przykładowo nano przy próbie uruchomienia zwróci błąd Error opening terminal: xterm-256color), a w samej powłoce bash nie zadziała skrót Ctrl+L czyszczący konsolę czy nie zobaczymy efektów użycia klawisza Backspace usuwającego wpisane znaki. Rozwiązaniem będzie utworzenie katalogu usr/share, do którego skopiujemy katalog /usr/share/terminfo oraz skopiowanie /lib/terminfo do naszego katalogu lib.
Poza tym nie działa rozwiązywanie nazw. Wystarczy jednak utworzyć katalog etc i skopiować do niego plik /etc/resolv.conf oraz skopiować bibliotekę libnss_dns.so.2 do lib/x86_64-linux-gnu:
Nie możliwe jest także używanie polecenia git, które przykładowo przy próbie sklonowania repozytorium zwraca:
Rozwiązanie tych problemów jest trochę bardziej rozbudowane:
Aby użytkownik był ograniczony po połączeniu z serwerem, niezbędne są zmiany w pliku /etc/ssh/sshd_config. Podobnie jak w przypadku konfiguracji SFTP w linii:
zmieniamy /usr/lib/openssh/sftp-server na internal-sftp oraz dodajemy:
Użytkownik został zamknięty w swojej izolowanej od pełnego systemu części. Ma dostęp wyłącznie do wybranych poleceń, nie ma możliwości chociażby odczytu danych nienależących do niego.
Dodatek: Apache w chroot
Na koniec chciałbym przedstawić proces izolacji serwera WWW Apache w środowisku chroot. Każda aplikacja uruchomiona w chroot nie ma dostępu do całości systemu operacyjnego, co zwiększa ogólne bezpieczeństwo. Chroot ma też zastosowanie, gdy różne aplikacje wymagają różnych wersji określonych zależności (dependency hell) — wtedy te aplikacje uruchamiamy w osobnych chroot, dzięki czemu rozwiązuje się problem niekompatybilności. Chociaż akurat w tej kwestii Docker będzie lepszym wyborem, przede wszystkim pod kątem zwykłej wygody korzystania.
Wyjątkowo trudnym i złożonym zadaniem byłoby manualne kopiowanie wszelkich bibliotek czy katalogów, których wymaga działający serwer Apache. Wykorzystamy więc narzędzie debootstrap, które pobierze najnowszą wersję systemu Debian (bookworm). Postaramy się w jak największym stopniu zminimalizować to środowisko przy zachowaniu względnego komfortu zarządzania serwerem Apache z poziomu systemu.
Debootstrap można zainstalować z repozytorium lub pobrać plik DEB i wykonać instalację z użyciem dpkg (w repozytorium Debiana jest najnowsza wersja 1.0.128):
Tworzymy “minimalny” obraz systemu:
Do katalogu /home/users/web zostaną pobrane odpowiednie dane. Wykonujemy chroot do niego (sudo chroot /home/users/web), po czym instalujemy Apache i PHP wraz z potrzebnymi rozszerzeniami. W razie potrzeby aktywujemy też niezbędne moduły Apache.
Naszym celem jest taka konfiguracja, aby do wprowadzania zmian w virtual hostach nie było potrzeby każdorazowego wchodzenia do tworzonego chroot. Dlatego też powinniśmy przenieść katalogi etc/apache2 i etc/php przykładowo do katalogu /etc w naszym systemie.
Z katalogu etc usuwamy całą pozostałą zawartość oprócz podkatalogu init.d. Ponownie zamierzamy ograniczyć dostępne w chroot polecenia, stąd przenosimy także plik bin/php8.2. Tę samą czynność stosujemy do plików apache* z katalogu sbin.
Chroot nie ma dostępu do danych na poziomie systemu operacyjnego. Dlatego jeżeli jakiekolwiek zasoby muszą być dostępne w chroot (np. klucz i certyfikat SSL czy wszelkie konfiguracje) musimy je zamontować poleceniem mount. Linki symboliczne nie zadziałają z oczywistego powodu.
Dodatkowo do etc musimy skopiować pewne pliki i katalog /etc/ssl:
Zmieniamy katalog domowy naszego użytkownika na wybrany z chroot. W tym przykładzie używam po prostu /app:
Przenosimy katalog domowy z oryginalnej lokalizacji i podobnie jak poprzednio zmieniamy właściciela na root.
Zarządzanie usługą Apache (start, stop, reload, restart) nie będzie możliwy bez katalogu /proc w chroot. Wystarczy podmontować ten katalog z naszego systemu:
Bez dodania wpisów do pliku /etc/fstab po restarcie systemu wszystkie ustawione punkty montowania nie będą uwzględniane i będziemy zmuszeni wykonać te operacje ponownie. Nasze wpisy /etc/fstab mogą wyglądać w ten sposób:
Na tym etapie możemy przystąpić do minimalizacji tworzonego środowiska. Jak można zobaczyć, tych plików i katalogów w / jest zdecydowanie zbyt dużo.
Zajmiemy się najpierw katalogami z binariami. Ograniczmy dostępne polecenia do tych najbardziej przydatnych i niezbędnych w zwykłej pracy. Usuwamy symlink bin i sbin oraz katalog usr/bin:
Tworzymy zwykły katalog bin (bez żadnego symlinku) i kopiujemy do niego konieczne polecenia:
Może nie wszystkie z nich wydają się absolutnie użyteczne dla zwykłego użytkownika, ale bez nich nie będziemy w stanie poprawnie zarządzać usługą Apache. Nie zapominamy też o pliku binarnym php, który wcześniej przenieśliśmy do tymczasowego miejsca. Z kolei pliki apache* przenosimy do usr/sbin.
Wykonujemy dalsze oczyszczanie:
Tworzymy pusty katalog root. Niezbędny będzie ponadto katalog dev, gdzie analogicznie jak poprzednio tworzymy pliki specjalne:
Polecenie git ponownie będzie potrzebować pewnych katalogów i biblioteki:
systemctl nie działa w chroot, a wywoływanie service apache2 <akcja> i tak odwołuje się do skryptu apache2ctl, który używa systemctl. W tym skrypcie (usr/sbin/apache2ctl) widnieje poniższy fragment:
Wewnątrz bloku case wystarczy przypisać false do need_systemd i ta prosta modyfikacja rozwiązuje opisaną trudność.
Zresztą w samym katalogu usr/sbin znajduje się 144 plików binarnych. Możemy je wszystkie (oprócz tych zaczynających się od a2, apache i service) usunąć. Dodatkowo w katalogach usr i var będzie kilka pustych katalogów nadających się właśnie do usunięcia. Nie trzeba tego wykonywać manualnie, można posłużyć się poleceniem find:
Prawdopodobnie po tych wszystkich czynnościach pozostaną nam nieaktualne dowiązania symboliczne lib32 i libx32. Można je bezpiecznie usunąć.
Bardzo istotna kwestia to zarządzanie usługą. Najprostszym podejściem będzie napisanie dwóch prostych skryptów — start.sh i reload.sh — które odpowiednio uruchomią Apache po restarcie systemu i przeładują konfigurację (co jest ważne po działaniu logrotate, w innym wypadku Apache będzie próbował zapisywać logi do nieistniejących plików). Skrypty można umieścić choćby w katalogu głównym chroot (u nas to /home/users/web).
Chroot jest gotowy, pozostaje zadbać o automatyczny start usługi oraz rotowanie logów. Trzeba też dodać konfigurację virtual host. Sprawę uruchamiania załatwi zwykłe zadanie cron (dla użytkownika root i, co oczywiste, dodane w naszym systemie):
Konfiguracja logrotate może ograniczyć się do:
Zmianami w serwerze Apache możemy zarządzać standardowo poprzez pliki w katalogu /etc/apache2 (który wcześniej przenieśliśmy z chroot). Wymagane będzie dopisanie ServerName localhost do pliku /etc/apache2/apache2.conf. Konfiguracja SSH jest analogiczna jak przy zwykłym chroot dla użytkownika.
Uruchomienie Apache z PHP w chroot ma tę dobrą adekwatność, iż cały kod wykonywany jest na poziomie chroot. Widać to choćby na przykładzie ścieżek. Spoza chroot wiemy, iż pliki strony znajdują się w lokalizacji /home/users/web/app/public_html/app.test.local. Tymczasem aplikacja nie jest (i nie musi) być tego świadoma:
Samo środowisko zostało dość dokładnie zminimalizowane: