Nie musi być dla wszystkich. Izolowanie dostępu w środowiskach Linux
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ę.
01.09.2023 | aktual.: 19.10.2023 11:00
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.
Dalsza część artykułu pod materiałem wideo
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śli 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:
# override default of no subsystems
Subsystem sftp /usr/lib/openssh/sftp-server
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):
Match Group app
ChrootDirectory /home/users/app
ForceCommand internal-sftp
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:
sudo systemctl restart 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:
sudo chown root:app /home/users/app
Można zauważyć, że 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ć, że 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:
sudo usermod -s /sbin/nologin app
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:
ls -la /usr/bin/rbash
lrwxrwxrwx 1 root root 4 Mar 27 2022 /usr/bin/rbash -> bash
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:
sudo unlink /usr/bin/rbash
sudo cp /usr/bin/bash .
Ten proces można zautomatyzować:
powloki=$(grep -v '^#' /etc/shells)
while read -r powloka; do
sudo setfacl -m u:app:r "$powloka"
done <<< "$powloki"
Na koniec przenosimy utworzoną wcześniej kopię do pliku /usr/bin/rbash:
sudo mv bash /usr/bin/rbash
Zmieniamy także domyślną powłokę dla naszego użytkownika:
sudo usermod -s /usr/bin/rbash app
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:
echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
Załóżmy, że użytkownik potrzebuje jedynie poleceń, które bez przeszkód pozwolą mu na pracę w powłoce. Proponuję przyjąć następujące:
cp, mv, mkdir, mysql, mysqldump, rm, rmdir, git, nano, ls, ln, cat, tee, pwd, grep, sed, head, less, tail, more, wget, curl, touch, dircolors
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):
sudo mkdir /rbash_bin
sudo cp /usr/bin/{cp,mv,mkdir,mysql,mysqldump,rm,rmdir,git,nano,ls,ln,cat,tee,pwd,grep,sed,head,less,tail,more,wget,curl,dircolors,touch} /rbash_bin
Ustawiamy właściciela plików .profile i .bashrc w katalogu domowym naszego użytkownika na użytkownika root.
sudo chown root: /home/users/app/{.profile,.bashrc}
Zawartość zmiennej PATH jest ustalana na podstawie pliku /etc/profile. W moim systemie Debian pierwsze linie wyglądają tak:
# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
if [ "$(id -u)" -eq 0 ]; then
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
else
PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
fi
export PATH
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:
if groups|grep -q sudo || [ "$(id -u)" -eq 0 ]; then
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
else
PATH="/rbash_bin"
fi
export PATH
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 nawet 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:
sudo chown root: /home/users/app
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:
sudo mkdir dev
sudo mknod -m 666 dev/null c 1 3
sudo mknod -m 666 dev/tty c 5 0
sudo mknod -m 666 dev/zero c 1 5
sudo mknod -m 666 dev/random c 1 8
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.
sudo mkdir bin
sudo cp /bin/{bash,cp,mv,mkdir,mysql,mysqldump,rm,rmdir,git,nano,ls,ln,unlink,cat,tee,pwd,grep,sed,head,less,tail,more,wget,curl,tar,tr,dirname,wc,perl,tput,basename,touch} bin
Teraz dość żmudny krok, ponieważ potrzebujemy skopiować biblioteki, z których korzystają podane polecenia. Skorzystamy z polecenia ldd.
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 każdego 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.
sudo mkdir -p usr/share
sudo cp -r /usr/share/terminfo usr/share
sudo cp -r /lib/terminfo 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:
sudo mkdir etc
sudo cp /etc/resolv.conf etc
sudo cp /usr/lib/x86_64-linux-gnu/libnss_dns.so.2 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:
warning: templates not found in /usr/share/git-core/templates
git: 'remote-https' is not a git command. See 'git --help'.
Rozwiązanie tych problemów jest trochę bardziej rozbudowane:
sudo cp -r /usr/share/git-core usr/share
sudo mkdir usr/lib
sudo cp -r /usr/lib/git-core usr/lib
sudo cp /usr/lib/x86_64-linux-gnu/libcurl.so.4 lib/x86_64-linux-gnu
sudo cp /usr/lib/x86_64-linux-gnu/libcurl.so.4.7.0 lib/x86_64-linux-gnu
sudo cp /usr/lib/x86_64-linux-gnu/libcurl-gnutls.so.4 lib/x86_64-linux-gnu
sudo cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 lib/x86_64-linux-gnu
sudo cp /usr/lib/x86_64-linux-gnu/libexpat.so.1.6.12 lib/x86_64-linux-gnu
sudo cp /usr/lib/x86_64-linux-gnu/libexpatw.so.1.6.12 lib/x86_64-linux-gnu
sudo cp -r /etc/ssl etc
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:
Subsystem sftp /usr/lib/openssh/sftp-server
zmieniamy /usr/lib/openssh/sftp-server na internal-sftp oraz dodajemy:
Match Group app
ChrootDirectory /home/users/app
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 ręczne 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):
wget https://deb.debian.org/debian/pool/main/d/debootstrap/debootstrap_1.0.128+nmu2_all.deb
sudo dpkg -i debootstrap_1.0.128+nmu2_all.deb
Tworzymy "minimalny" obraz systemu:
sudo debootstrap --variant=minbase bookworm /home/users/web
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.
sudo mv etc/apache2 /etc
sudo mv etc/php /etc
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śli 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.
sudo mkdir ssl
sudo mount --bind /ssl ssl
cd etc
sudo mkdir {apache2,php}
sudo mount --bind /etc/apache2 apache2
sudo mount --bind /etc/php php
Dodatkowo do etc musimy skopiować pewne pliki i katalog /etc/ssl:
sudo cp /etc/{resolv.conf,passwd,group,mime.types} etc
sudo cp -r /etc/ssl etc
Zmieniamy katalog domowy naszego użytkownika na wybrany z chroot. W tym przykładzie używam po prostu /app:
sudo usermod -d /app 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:
sudo mount -t proc /proc proc
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:
/ssl /home/users/web/ssl none bind
/etc/apache2 /home/users/web/etc/apache2 none bind
/etc/php /home/users/web/etc/php none bind
proc /home/users/web/proc proc defaults,hidepid=2 0 0
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:
sudo unlink bin
sudo unlink sbin
sudo rm -rf usr/bin
Tworzymy zwykły katalog bin (bez żadnego symlinku) i kopiujemy do niego konieczne polecenia:
sudo cp /bin/{bash,sh,env,echo,id,mktemp,expr,chmod,chown,readlink,sleep,cp,mv,mkdir,mysql,mysqldump,rm,rmdir,git,nano,ls,ln,unlink,cat,tee,pwd,grep,egrep,sed,head,less,tail,more,wget,curl,tar,unzip,tr,dirname,wc,perl,tput,basename,touch,awk,pidof,ps} bin
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:
sudo rm -rf {boot,dev,home,media,mnt,opt,root,srv,sys}
Tworzymy pusty katalog root. Niezbędny będzie ponadto katalog dev, gdzie analogicznie jak poprzednio tworzymy pliki specjalne:
sudo mknod -m 666 dev/null c 1 3
sudo mknod -m 666 dev/tty c 5 0
sudo mknod -m 666 dev/zero c 1 5
sudo mknod -m 666 dev/random c 1 8
sudo mknod -m 666 dev/urandom c 1 9
Polecenie git ponownie będzie potrzebować pewnych katalogów i biblioteki:
sudo cp -r /usr/share/git-core usr/share
sudo cp -r /usr/lib/git-core usr/lib
sudo cp /usr/lib/x86_64-linux-gnu/libcurl-gnutls.so.4 lib/x86_64-linux-gnu
sudo cp /usr/lib/x86_64-linux-gnu/libldap_r-2.4.so.2 lib/x86_64-linux-gnu
sudo cp /usr/lib/x86_64-linux-gnu/liblber-2.4.so.2 lib/x86_64-linux-gnu
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:
need_systemd=false
if [ -z "$APACHE_STARTED_BY_SYSTEMD" ] ; then
case "$(readlink -f /proc/1/exe)" in
*systemd*)
need_systemd=true
;;
*)
;;
esac
fi
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ć ręcznie, można posłużyć się poleceniem find:
sudo find -maxdepth 1 -type d -empty -delete
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ć nawet w katalogu głównym chroot (u nas to /home/users/web).
start.sh
#!/bin/bash
/etc/init.d/apache2 start
reload.sh
#!/bin/bash
/etc/init.d/apache2 reload
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):
@reboot /usr/sbin/chroot /home/users/web "./start.sh"
Konfiguracja logrotate może ograniczyć się do:
/home/users/web/*/logs/*.log {
rotate 14
daily
compress
postrotate
/usr/sbin/chroot /home/users/web "./reload.sh"
endscript
}
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ą właściwość, że cały kod wykonywany jest na poziomie chroot. Widać to nawet na przykładzie ścieżek. Spoza chroot wiemy, że 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:
Michał Giza