Linux Hardening: systemd jako sandbox — jak zamknąć usługę w jej własnym świecie
Przez lata systemd kojarzył się administratorom głównie z jednym:
systemctl start nginx
systemctl stop nginx
systemctl restart nginx
Uruchom usługę. Zatrzymaj usługę. Sprawdź status.
Tymczasem współczesny systemd potrafi zrobić coś znacznie ciekawszego.
Może ograniczyć:
- system plików,
- dostęp do
/home, - dostęp do urządzeń,
- capabilities,
- namespace,
- rodziny adresów sieciowych,
- syscall’e,
- możliwość uzyskania nowych uprawnień,
- dostęp do części interfejsów kernela.
Czyli administrator może powiedzieć:
„Ta usługa ma działać, ale nie chcę, żeby mogła zobaczyć albo zmodyfikować połowę systemu.”
I nie trzeba od razu instalować kolejnego narzędzia.
Mechanizmy sandboxingu są już częścią systemd.
Dokumentacja systemd.exec opisuje zestaw opcji właśnie jako mechanizmy ograniczające ekspozycję systemu na procesy uruchamiane przez daną jednostkę.
To bardzo ważna zmiana sposobu myślenia.
Nie zabezpieczamy tylko serwera.
Zabezpieczamy każdą usługę osobno.
1. Najpierw przestańmy myśleć „serwer”
Wyobraź sobie typową maszynę:
Linux Server
│
├── nginx
├── sshd
├── php-fpm
├── PostgreSQL
├── Redis
├── Docker
├── monitoring
└── backup
Administrator może zabezpieczyć cały host.
Firewall.
AppArmor.
SELinux.
Aktualizacje.
SSH.
Ale nadal pozostaje jedno fundamentalne pytanie:
Co się stanie, jeżeli jedna z usług zostanie przejęta?
Jeżeli odpowiedź brzmi:
„Atakujący dostanie uprawnienia procesu.”
to nie jest jeszcze wystarczająco dobra odpowiedź.
Trzeba zapytać:
Jakie uprawnienia procesu są rzeczywiście potrzebne?
I tutaj zaczyna się sandboxing systemd.
2. Usługa nie potrzebuje całego systemu
Załóżmy prostą usługę:
myapp.service
Proces potrzebuje:
/opt/myapp
/var/lib/myapp
/var/log/myapp
Ale czy potrzebuje:
/home
/root
/boot
/etc/shadow
/dev
/sys
/proc
Oczywiście nie.
Jeżeli aplikacja ma podatność RCE, dlaczego pozwalać jej swobodnie poruszać się po systemie?
Możemy więc zmienić model:
HOST
|
+------------+------------+
| |
SYSTEM SERVICE
| |
cały filesystem ograniczony widok
| |
| /opt/myapp
| /var/lib/myapp
| /var/log/myapp
|
X
Usługa nadal działa na tym samym kernelu.
Ale jej widok świata jest mniejszy.
3. ProtectSystem= — pierwsza bardzo mocna blokada
Jedną z najciekawszych opcji jest:
ProtectSystem=strict
W tym trybie systemd montuje praktycznie całą hierarchię systemu plików jako read-only dla procesów jednostki, z wyjątkami dotyczącymi API filesystems takich jak /dev, /proc i /sys.
Dokumentacja systemd zaleca ProtectSystem= dla długotrwałych usług, o ile dana usługa nie musi modyfikować systemu operacyjnego.
Przykład:
[Service]
ProtectSystem=strict
Nagle proces, który został przejęty, nie może po prostu zrobić:
echo "..." >> /etc/some-config
czy zmienić plików systemowych.
To nie jest to samo co klasyczne:
chmod
Administrator nie zmienia właściciela pliku.
Zmienia widok systemu plików dla procesu.
4. Ale aplikacja musi gdzieś pisać
I tutaj zaczyna się ciekawsza część.
Jeżeli ustawimy:
ProtectSystem=strict
a aplikacja musi zapisywać:
/var/lib/myapp
możemy jawnie udostępnić ten katalog jako zapisywalny:
ReadWritePaths=/var/lib/myapp
Wtedy model wygląda tak:
filesystem
│
├── /etc READ ONLY
├── /usr READ ONLY
├── /boot READ ONLY
├── /home READ ONLY
│
└── /var/lib/myapp READ + WRITE
To jest dokładnie to, czego oczekujemy od least privilege.
Nie:
„Aplikacja może pisać.”
Tylko:
„Aplikacja może pisać tutaj.”
systemd.exec przewiduje również StateDirectory=, LogsDirectory= i podobne mechanizmy tworzenia katalogów przeznaczonych dla konkretnej usługi, które współpracują z ograniczeniami ProtectSystem=.
5. ProtectHome= — odetnij prywatne dane
Następna rzecz:
ProtectHome=yes
To bardzo interesujące szczególnie dla usług sieciowych.
Przy odpowiedniej konfiguracji /home, /root i /run/user mogą zostać ukryte przed procesem usługi. ProtectHome= może również działać w trybie read-only albo przez zamontowanie pustych filesystemów tymczasowych.
Wyobraźmy sobie przejęty serwer WWW.
Bez dodatkowej izolacji aplikacja może próbować:
/home/user/.ssh/
home/user/.config/
root/
Jeżeli aplikacja tego nie potrzebuje, nie powinna tego widzieć.
Konfiguracja:
[Service]
ProtectHome=yes
dodaje kolejną granicę.
To bardzo prosty przykład zasady:
dane użytkownika nie są potrzebne usłudze → usługa nie powinna ich widzieć.
6. PrivateTmp= — własne /tmp
/tmp jest wspólnym miejscem, z którego korzysta wiele procesów.
Dla części usług nie ma powodu, aby widziały dokładnie ten sam /tmp.
Możemy użyć:
PrivateTmp=yes
Wtedy systemd tworzy dla usługi prywatne przestrzenie tymczasowe.
Schemat:
HOST
│
├── /tmp
│
├── service A
│ └── własny /tmp
│
└── service B
└── własny /tmp
To ogranicza możliwość przypadkowej lub celowej interakcji przez pliki tymczasowe.
Nie rozwiązuje wszystkich problemów związanych z IPC.
Ale usuwa jedną klasę niepotrzebnej wspólnej przestrzeni.
7. NoNewPrivileges=yes
To jedna z najważniejszych opcji.
NoNewPrivileges=yes
Jej sens jest prosty:
proces i jego potomkowie nie mogą zdobywać nowych uprawnień poprzez execve(), np. przez setuid/setgid albo filesystem capabilities.
Czyli:
proces
|
| execve()
v
program uprzywilejowany
nie powinien automatycznie powodować przejścia do nowego poziomu uprawnień.
To szczególnie interesujące w połączeniu z innymi mechanizmami.
Na przykład:
[Service]
User=myapp
NoNewPrivileges=yes
Teraz mamy:
myapp
|
+-- zwykły UID
|
+-- brak zdobywania nowych privilegiów
To nie zastępuje pozostałych ograniczeń.
Ale bardzo dobrze je uzupełnia.
8. PrivateDevices=yes
Co robi typowy serwis webowy z urządzeniami:
/dev/sda
/dev/mem
/dev/kvm
/dev/input/*
Praktycznie nic.
Dlaczego więc miałby mieć do nich dostęp?
Możemy zastosować:
PrivateDevices=yes
Wtedy usługa otrzymuje znacznie bardziej ograniczony widok urządzeń.
To kolejny przykład redukcji powierzchni ataku.
Atakujący, który przejął proces, nie dostaje automatycznie tego samego dostępu do /dev, który posiada host.
9. ProtectKernelTunables=
Kernel ma ogromną liczbę parametrów dostępnych poprzez interfejsy takie jak:
/proc/sys
/sys
Nie każda usługa powinna móc je modyfikować.
Dlatego możemy użyć:
ProtectKernelTunables=yes
To szczególnie istotne, gdy myślimy o scenariuszu:
RCE
|
v
przejęty proces
|
v
próba manipulacji konfiguracją kernela
Jeżeli aplikacja nie potrzebuje tej funkcjonalności, nie ma powodu jej udostępniać.
10. ProtectControlGroups=
cgroups są fundamentalną częścią współczesnego Linuksa.
Wykorzystują je między innymi:
- systemd,
- kontenery,
- Docker,
- Kubernetes,
- mechanizmy kontroli zasobów.
Ale zwykła aplikacja nie powinna potrzebować swobodnego zapisu do hierarchii cgroups.
systemd oferuje:
ProtectControlGroups=yes
albo bardziej restrykcyjne warianty.
Dokumentacja wskazuje, że większość usług nie powinna wymagać zapisu do hierarchii control groups i rekomenduje ograniczenie tego dostępu dla typowych usług.
To kolejna granica:
application
|
X
cgroup control
11. RestrictAddressFamilies= — aplikacja nie potrzebuje każdej sieci
To jedna z opcji, którą administratorzy często pomijają.
Domyślnie proces może korzystać z wielu rodzin adresowych.
Ale aplikacja WWW może potrzebować wyłącznie:
AF_UNIX
AF_INET
AF_INET6
Możemy więc zdefiniować:
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
systemd pozwala używać allowlisty rodzin adresowych, a dokumentacja zwraca uwagę między innymi na możliwość ograniczenia dostępu do wrażliwych rodzin, takich jak AF_PACKET.
To jest szczególnie interesujące w przypadku usług wystawionych do Internetu.
Aplikacja nie musi mieć dostępu do każdego rodzaju interfejsu sieciowego tylko dlatego, że potrafi otworzyć socket.
12. Ale tutaj jest haczyk
RestrictAddressFamilies= nie blokuje wszystkiego, co administrator może sobie wyobrazić.
Mechanizm ogranicza dostęp do rodzin adresowych poprzez socket().
Nie obejmuje automatycznie socketów przekazanych inną drogą, np. przez socket activation. Dokumentacja systemd wskazuje również ograniczenia związane z socketpair() i niektórymi ścieżkami io_uring.
Dlatego:
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
nie oznacza:
„Ten proces nie ma żadnej możliwości komunikacji sieciowej poza tym.”
To bardziej precyzyjna polityka.
I dlatego sandboxing trzeba rozumieć, a nie kopiować z bloga.
13. SystemCallFilter= — tutaj spotykamy poprzedni odcinek
W #13 omawialiśmy seccomp.
Teraz systemd pozwala wykorzystać podobny mechanizm bez ręcznego pisania filtrów.
Przykład:
SystemCallFilter=@system-service
Możemy również budować bardziej restrykcyjne polityki.
systemd pozwala definiować allowlisty syscalli lub grup syscalli, a przy użyciu prefiksu ~ również denylisty.
Czyli:
systemd
|
v
SystemCallFilter
|
v
seccomp
|
v
kernel
To bardzo eleganckie połączenie.
Administrator nie musi ręcznie implementować całego mechanizmu seccomp.
14. Nie blokuj syscalli „na czuja”
Tutaj wracamy do problemu z poprzedniego artykułu.
Nie rób:
SystemCallFilter=read write open
tylko dlatego, że aplikacja „pewnie tego potrzebuje”.
Programy korzystają z wielu syscalli, których nie widać na pierwszy rzut oka.
Dynamiczny linker potrzebuje określonych operacji.
Biblioteki potrzebują pamięci mapowanej.
DNS potrzebuje sieci.
TLS potrzebuje różnych operacji.
Logowanie może wymagać IPC.
Systemd sam wskazuje, że nadmierne blokowanie syscalli może uniemożliwić działanie zwykłych programów.
Najpierw test.
Potem polityka.
Nie odwrotnie.
15. CapabilityBoundingSet=
Jeżeli proces nie potrzebuje capabilities, nie dawaj mu ich.
Przykładowo:
CapabilityBoundingSet=
może być wykorzystane do wyczyszczenia zestawu capabilities jednostki.
W bardziej konkretnych usługach możemy pozostawić tylko określone capability.
Model:
ROOT
|
+-- CAP_NET_ADMIN X
+-- CAP_SYS_ADMIN X
+-- CAP_SYS_PTRACE X
+-- CAP_NET_BIND_SERVICE ✓
Zamiast:
„proces jest rootem, więc może wszystko”
mamy:
„proces ma dokładnie tę możliwość, której potrzebuje”
To jest dużo lepszy model.
16. Przykład: prosta usługa sieciowa
Załóżmy:
myapp
Aplikacja:
- działa jako użytkownik
myapp, - słucha na porcie,
- zapisuje dane w
/var/lib/myapp, - nie potrzebuje
/home, - nie potrzebuje urządzeń,
- nie musi zmieniać systemu,
- nie potrzebuje administracyjnych capabilities.
Możemy zacząć od:
[Unit]
Description=My hardened application
After=network.target
[Service]
User=myapp
Group=myapp
ExecStart=/opt/myapp/bin/myapp
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
ReadWritePaths=/var/lib/myapp
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
[Install]
WantedBy=multi-user.target
To nie jest uniwersalny profil.
I właśnie dlatego nie należy kopiować go bez testowania.
Jego wartość polega na pokazaniu sposobu myślenia.
17. Co się stanie po przejęciu aplikacji?
Bez sandboxingu:
RCE
|
v
myapp
|
+--> filesystem
+--> /home
+--> /root
+--> /dev
+--> kernel interfaces
+--> network families
+--> capabilities
+--> privilege escalation
Z sandboxingiem:
RCE
|
v
myapp
|
+--> /var/lib/myapp READ/WRITE
|
+--> system filesystem READ ONLY
|
+--> /home BLOCKED
|
+--> devices LIMITED
|
+--> capabilities LIMITED
|
+--> privilege gain BLOCKED
|
+--> syscall surface LIMITED
To nie oznacza, że exploit przestaje istnieć.
Oznacza coś ważniejszego:
jego konsekwencje są mniejsze.
18. systemd-analyze security
Jest jeszcze jeden bardzo praktyczny element.
Dla usług systemd można użyć:
systemd-analyze security
A dla konkretnej jednostki:
systemd-analyze security nginx.service
Narzędzie pomaga oceniać zastosowane mechanizmy sandboxingu.
Nie należy jednak traktować wyniku jako magicznej oceny:
9.8/10 = bezpieczne
To nie jest substytut analizy architektury.
Jeżeli aplikacja ma RCE i posiada dostęp do sekretów, fakt, że dostała dobry wynik sandboxingu, nie sprawia, że sekret przestaje być cenny.
Narzędzie jest punktem wyjścia.
Nie końcem audytu.
19. Największy błąd: włączenie wszystkiego naraz
Można znaleźć w Internecie konfiguracje wyglądające imponująco:
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
PrivateNetwork=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_UNIX
SystemCallFilter=@system-service
NoNewPrivileges=yes
Wygląda świetnie.
A potem:
systemctl restart application
i:
Failed
Aplikacja przestaje działać.
Dlaczego?
Bo sandboxing jest częścią konfiguracji aplikacji.
Jeżeli aplikacja potrzebuje:
DNS
network
/tmp
/dev/random
socketów UNIX
określonych syscalli
zapisu do konkretnego katalogu
to trzeba jej to świadomie udostępnić.
20. Hardening powinien działać iteracyjnie
Lepszy proces:
1. Uruchom usługę
↓
2. Zidentyfikuj potrzeby
↓
3. Włącz jedną warstwę
↓
4. Test
↓
5. Sprawdź logi
↓
6. Włącz kolejną
↓
7. Test
↓
8. Dokumentuj
Na przykład:
User=
NoNewPrivileges=
ProtectHome=
ProtectSystem=
PrivateTmp=
PrivateDevices=
ProtectKernelTunables=
ProtectControlGroups=
RestrictAddressFamilies=
SystemCallFilter=
Każda opcja powinna mieć powód.
21. Sandbox nie jest tylko ochroną przed atakującym
To ważne.
Sandboxing ogranicza również:
- błędy aplikacji,
- błędną konfigurację,
- kompromitację zależności,
- przypadkowe usunięcie plików,
- niepożądane zachowanie skryptów,
- skutki błędów administratora.
Czyli:
sandboxing
|
+-- malware
+-- exploit
+-- bug
+-- misconfiguration
+-- human error
Nie każda katastrofa zaczyna się od hakera.
22. Najciekawsze jest połączenie wszystkich warstw
Po czternastu odcinkach serii zaczyna być widoczny większy obraz.
Proces może mieć:
UID/GID
|
v
DAC
|
v
capabilities
|
v
AppArmor / SELinux
|
v
systemd sandbox
|
v
seccomp
|
v
namespace
|
v
kernel
To nie jest jeden firewall.
To łańcuch ograniczeń.
Jeżeli jedna warstwa zawiedzie, kolejna nadal może ograniczyć skutki.
I właśnie dlatego hardening jest procesem projektowania granic.
23. Minimalny profil nie zawsze jest najlepszym profilem
To subtelna rzecz.
Można zablokować praktycznie wszystko.
Problem?
Usługa przestanie działać.
Dlatego celem nie jest:
„Najbardziej restrykcyjna konfiguracja.”
Celem jest:
„Najbardziej restrykcyjna konfiguracja, która nadal realizuje wymagany model biznesowy i techniczny.”
To ogromna różnica.
Jeżeli PostgreSQL potrzebuje określonego dostępu do systemu plików, nie blokujemy go tylko dlatego, że „database powinien być sandboxed”.
Najpierw rozumiemy architekturę.
Potem ograniczamy.
24. Co sprawdzić dzisiaj na własnym serwerze?
Weź jedną usługę.
Nie wszystkie.
Na przykład:
systemctl status nginx
Potem:
systemctl cat nginx
Następnie:
systemd-analyze security nginx.service
Sprawdź:
systemctl show nginx \
-p User \
-p Group \
-p NoNewPrivileges \
-p ProtectSystem \
-p ProtectHome \
-p PrivateTmp \
-p PrivateDevices \
-p ProtectKernelTunables \
-p ProtectControlGroups \
-p RestrictAddressFamilies \
-p SystemCallFilter
I zadaj sobie jedno pytanie:
Czy nginx naprawdę potrzebuje każdego z tych dostępów?
Jeżeli odpowiedź brzmi „nie wiem”, znalazłeś potencjalny obszar do audytu.
25. systemd przestał być tylko init systemem
To chyba najważniejszy wniosek.
systemd nie jest już wyłącznie:
„programem, który uruchamia usługi”.
Może być również warstwą polityki bezpieczeństwa.
Proces dostaje środowisko:
filesystem
network
devices
capabilities
namespaces
syscalls
privileges
i systemd może część tego środowiska ograniczyć.
To oznacza, że bardzo często najtańszy hardening istniejącego serwera nie polega na instalacji kolejnego programu.
Polega na spojrzeniu na:
/etc/systemd/system/
i zadaniu prostego pytania:
Co ta usługa naprawdę musi mieć?
Jeżeli odpowiedź brzmi:
„Tylko katalog aplikacji, sieć i kilka syscalli.”
to dokładnie tyle powinna dostać.
Reszta jest powierzchnią ataku.
Źródła techniczne
Dokumentacja systemd.exec opisuje mechanizmy sandboxingu, w tym ProtectSystem=, ProtectHome=, RestrictAddressFamilies= i SystemCallFilter=.
Dokumentacja systemd opisuje również działanie NoNewPrivileges= i jego wpływ na możliwość uzyskiwania nowych uprawnień przez execve().







