Linux Hardening: Seccomp od środka — jak odciąć proces od niepotrzebnych syscalli
Proces Linuksa może być zwykłym użytkownikiem.
Może mieć ograniczone capabilities.
Może działać pod AppArmor albo SELinux.
Może być zamknięty w namespace.
A mimo to nadal ma dostęp do ogromnego interfejsu kernela.
Wystarczy wywołać syscall.
To właśnie tutaj zaczyna się seccomp.
Nie jest firewallem. Nie jest systemem uprawnień plików. Nie jest zamiennikiem AppArmor. Nie jest też magicznym sandboxem.
Seccomp robi coś znacznie bardziej konkretnego:
ogranicza system calls, które proces może wykonywać.
Kernel Linux udostępnia userspace bardzo duży interfejs syscalli, a wiele aplikacji wykorzystuje tylko niewielki jego fragment. Dokumentacja kernela wskazuje właśnie redukcję tej powierzchni jako podstawowy cel seccomp filtering.
To oznacza, że możemy postawić kolejną granicę:
APPLICATION
|
| syscall
v
+--------------+
| SECCOMP |
| FILTER |
+--------------+
| |
ALLOW DENY
| |
v v
KERNEL ERROR /
SIGNAL
Jeżeli aplikacja zostanie przejęta, atakujący nadal może wykonywać kod.
Ale niekoniecznie będzie mógł wykonać każdy syscall, którego potrzebuje.
I właśnie w tym tkwi wartość seccomp.
1. Najpierw trzeba zrozumieć syscall
Aplikacja nie rozmawia z kernelem bezpośrednio poprzez open(), read() czy socket() w takim sensie, jak często przedstawiają to uproszczone diagramy.
Zwykle robi to przez bibliotekę użytkową, np. glibc.
Schemat wygląda bardziej tak:
Application
|
v
glibc / runtime
|
v
system call
|
v
CPU transition
|
v
Linux kernel
|
v
kernel subsystem
Przykładowo aplikacja chce otworzyć plik.
Może wywołać:
open(...)
ale rzeczywisty syscall używany przez współczesne implementacje może być inny.
To ważne przy pisaniu filtrów seccomp.
Dokumentacja seccomp(2) zwraca uwagę, że wrappery biblioteczne nie zawsze odpowiadają nazwą syscallowi wykonywanemu przez kernel; przykładowo wrapper exit() może korzystać z exit_group(), a implementacja open() może prowadzić do openat().
Dlatego zasada:
„Aplikacja używa
open(), więc zablokujęopen”
może być zwyczajnie błędna.
2. Seccomp działa na granicy syscalla
Seccomp filter może analizować między innymi:
numer syscalla
architekturę
argumenty syscalla
Następnie filtr zwraca decyzję.
W uproszczeniu:
syscall()
|
v
+------------------+
| seccomp BPF |
+------------------+
|
+---- ALLOW
|
+---- ERRNO
|
+---- LOG
|
+---- TRAP
|
+---- KILL
Linux wykorzystuje do tego mechanizm BPF.
Nie oznacza to jednak, że seccomp jest „eBPF-em do filtrowania wszystkiego”.
Seccomp używa określonego modelu BPF do oceny danych dotyczących syscalla. Kernel opisuje ten mechanizm jako filtr działający na numerze syscalla, argumentach i metadanych procesu.
To bardzo precyzyjna granica.
3. Dlaczego to jest interesujące dla hardeningu?
Wyobraźmy sobie serwer HTTP.
Proces aplikacji potrzebuje:
read
write
openat
close
mmap
munmap
futex
epoll
socket
connect
accept
Ale prawdopodobnie nie potrzebuje:
init_module
delete_module
kexec_load
mount
umount
ptrace
reboot
swapon
Jeżeli aplikacja zostanie przejęta, dlaczego pozwalać jej na dostęp do całego interfejsu kernela?
Nie ma dobrego powodu.
Model hardeningu powinien wyglądać więc tak:
KERNEL
|
+--------+--------+
| |
potrzebne niepotrzebne
syscalls syscalls
| |
v X
aplikacja blokada
To jest klasyczna zasada least privilege, ale zastosowana nie do użytkownika czy pliku.
Zastosowana do syscalli.
4. Seccomp nie chroni przed wszystkim
I tutaj pojawia się jeden z najważniejszych punktów.
Kernel Linux mówi wprost:
system call filtering nie jest sandboxem.
Seccomp ma ograniczać powierzchnię kernela, ale nie powinien być traktowany jako kompletna izolacja procesu. Do kontroli dostępu i zachowania potrzebne są również inne mechanizmy, takie jak LSM oraz pozostałe warstwy hardeningu.
Czyli:
Seccomp
+
AppArmor / SELinux
+
Capabilities
+
Namespaces
+
Filesystem permissions
+
cgroups
dopiero tworzą sensowny model obrony.
Seccomp sam nie odpowiada na pytanie:
„Do jakiego pliku proces może uzyskać dostęp?”
Odpowiada na inne:
„Jakich syscalli proces może użyć?”
To różne problemy.
5. Najciekawsze jest CAP_SYS_ADMIN
W poprzednich odcinkach serii pojawił się już temat capabilities.
Tutaj pojawia się ważne połączenie.
Proces może potrzebować CAP_SYS_ADMIN, aby wykonać określone operacje.
Seccomp może jednak ograniczyć syscalle, które umożliwiają część takich operacji.
Przykładowo kernel dokumentuje między innymi uprzywilejowane operacje związane z:
mount()
umount()
pivot_root()
setns()
swapon()
swapoff()
jako operacje związane z CAP_SYS_ADMIN.
To pokazuje, dlaczego bezpieczeństwo powinno być wielowarstwowe.
Nie:
CAP_SYS_ADMIN = allow everything
ale:
CAP_SYS_ADMIN
+
seccomp
+
namespace
+
LSM
Każda warstwa ogranicza inny fragment możliwości procesu.
6. no_new_privs — mały bit, duże znaczenie
Seccomp ma jeszcze jeden bardzo interesujący element.
Proces bez odpowiednich uprawnień nie może po prostu założyć filtra seccomp, który potem pozwoli mu manipulować zachowaniem programów uprzywilejowanych.
Dlatego przed instalacją filtra wymagane jest między innymi ustawienie:
PR_SET_NO_NEW_PRIVS
albo posiadanie odpowiednich uprawnień.
Kernel opisuje to jako ochronę przed sytuacją, w której nieuprzywilejowany proces założyłby złośliwy filtr, a następnie uruchomił program uprzywilejowany przez execve().
Schemat:
USER PROCESS
|
| no_new_privs
v
SECCOMP FILTER
|
v
EXEC
|
v
PROGRAM
Flaga no_new_privs ma fundamentalną właściwość:
proces nie powinien dzięki późniejszej operacji uzyskać nowych uprawnień, których wcześniej nie posiadał.
To bardzo dobrze pasuje do hardeningu.
7. Filtr może przetrwać execve()
To kolejny detal, który często umyka.
Jeżeli filtr został poprawnie założony, a proces może wykonać execve(), ograniczenia seccomp mogą pozostać aktywne również po uruchomieniu nowego programu. Podobnie procesy potomne mogą odziedziczyć ograniczenia, jeśli dozwolone są odpowiednie operacje fork/clone.
Możemy więc mieć:
launcher
|
+-- seccomp
|
v
execve()
|
v
application
i nadal obowiązuje ograniczenie.
To ważne, bo inaczej aplikacja mogłaby po prostu uruchomić inny program i ominąć założoną politykę.
8. ERRNO jest często lepsze niż natychmiastowe zabicie procesu
Seccomp oferuje kilka rodzajów reakcji.
Jedną z nich jest:
SECCOMP_RET_ERRNO
Wtedy syscall nie jest wykonywany, a proces otrzymuje błąd.
To może wyglądać dla aplikacji jak:
Operation not permitted
albo inny odpowiedni errno.
To przydatne podczas wdrażania hardeningu, ponieważ aplikacja może przeżyć próbę wykonania niepotrzebnej operacji.
Inne akcje są znacznie bardziej agresywne.
Można między innymi:
LOG
TRAP
KILL
SECCOMP_RET_KILL_PROCESS może zakończyć cały proces, jeśli wykona on zabroniony syscall.
W produkcji trzeba więc uważać.
Źle przygotowany profil może nie tyle poprawić bezpieczeństwo, co wyłączyć aplikację.
9. Najgorszy filtr to filtr napisany „na oko”
Administrator patrzy na aplikację:
Nginx
i mówi:
„Nginx potrzebuje pewnie 20 syscalli.”
Problem?
Nie.
Rzeczywisty zestaw zależy od:
wersji programu
bibliotek
modułów
konfiguracji
architektury
sposobu uruchamiania
TLS
DNS
logowania
workerów
dodatkowych funkcji
Dlatego profil seccomp powinien być budowany na podstawie obserwacji rzeczywistego zachowania.
Nie zgadywania.
10. Zacznij od obserwacji
Jednym z praktycznych narzędzi jest:
strace
Przykładowo:
strace -f -c ./application
Możemy zobaczyć, jakie syscall’e występowały podczas działania programu.
Przy bardziej szczegółowej analizie:
strace -f -e trace=%file ./application
albo:
strace -f -e trace=network ./application
To nie jest gotowy profil bezpieczeństwa.
To materiał do analizy.
Jeżeli program podczas normalnej pracy wykonuje:
openat
read
write
mmap
munmap
futex
epoll_wait
a nigdy nie wykonuje:
mount
ptrace
kexec_load
init_module
reboot
to zaczynamy widzieć realną różnicę między:
kernel API potrzebne aplikacji
a:
kernel API dostępne aplikacji
I właśnie tę różnicę chcemy wykorzystać.
11. Seccomp i Docker
Tutaj teoria przechodzi bezpośrednio w praktykę.
Docker wykorzystuje seccomp do ograniczania syscalli wykonywanych przez kontenery.
Aktualna dokumentacja Dockera opisuje domyślny profil seccomp jako allowlistę: domyślną akcją jest odmowa, a wybrane syscall’e są jawnie dopuszczane. Docker podaje również, że domyślny profil blokuje około 44 syscalli spośród ponad 300 dostępnych.
Czyli:
Container
|
v
Docker
|
v
seccomp profile
|
+---- allowed syscall
|
+---- blocked syscall
To jedna z powodów, dla których nie powinno się bez potrzeby wyłączać seccomp.
Konfiguracja:
docker run \
--security-opt seccomp=unconfined \
...
powinna być traktowana jako świadome wyłączenie warstwy ochronnej, a nie jako zwykła opcja kompatybilności.
Docker wprost rekomenduje pozostawienie domyślnego profilu, chyba że istnieje konkretny powód do jego zmiany.
12. Dlaczego --privileged i seccomp tworzą zły duet?
Wyobraźmy sobie administratora, który ma problem:
Container:
"Operation not permitted"
Pierwsza reakcja:
--privileged
Druga:
--cap-add=SYS_ADMIN
Trzecia:
--security-opt seccomp=unconfined
Aplikacja zaczyna działać.
Problem został „naprawiony”.
Tylko że właśnie usunięto kilka warstw bezpieczeństwa.
Lepszy proces wygląda tak:
Operation not permitted
|
v
Który syscall?
|
v
Dlaczego jest potrzebny?
|
v
Czy aplikacja naprawdę go potrzebuje?
|
v
Czy wystarczy capability?
|
v
Czy można ograniczyć profil seccomp?
|
v
Test
|
v
Produkcja
To jest hardening.
Nie:
nie działa
↓
daj root
↓
działa
13. Jak sprawdzić seccomp działający dla procesu?
Kernel udostępnia informacje o stanie seccomp procesu poprzez /proc.
Można sprawdzić:
grep Seccomp /proc/$(pidof nginx | awk '{print $1}')/status
Przykładowo wynik może wyglądać jak:
Seccomp: 2
W praktyce:
0 = SECCOMP_DISABLED
1 = SECCOMP_MODE_STRICT
2 = SECCOMP_MODE_FILTER
To prosty sposób na sprawdzenie, czy proces korzysta z filtrowania.
Można również analizować konkretny proces przez:
cat /proc/<PID>/status
i zestawić wynik z konfiguracją usługi.
14. systemd potrafi wykorzystać ten sam model
Seccomp nie jest zarezerwowany dla Dockera.
systemd oferuje mechanizmy związane z ograniczaniem capabilities, syscalli i innych aspektów środowiska usług.
W przypadku systemd warto patrzeć na bezpieczeństwo procesu całościowo:
User=
Group=
CapabilityBoundingSet=
NoNewPrivileges=
RestrictAddressFamilies=
RestrictNamespaces=
ProtectSystem=
ProtectHome=
PrivateTmp=
SystemCallFilter=
Szczególnie interesujące jest:
SystemCallFilter=
które pozwala ograniczać dostęp usługi do syscalli.
Dokumentacja systemd.exec opisuje również CapabilityBoundingSet= jako mechanizm kontrolowania capability bounding set procesu.
Dzięki temu możemy zbudować usługę:
systemd
|
+-- User=app
+-- NoNewPrivileges=yes
+-- CapabilityBoundingSet=...
+-- SystemCallFilter=...
+-- ProtectSystem=strict
+-- ProtectHome=yes
To już nie jest pojedyncza opcja bezpieczeństwa.
To kompletna polityka procesu.
15. Nie filtruj syscalli tylko dlatego, że wyglądają groźnie
To bardzo ważna zasada.
Lista:
mount
ptrace
clone
unshare
setns
io_uring_setup
bpf
może wyglądać przerażająco.
Ale administrator nie powinien blokować syscalli wyłącznie dlatego, że mają „security-looking” nazwę.
Powinien odpowiedzieć:
Czy moja aplikacja potrzebuje tego syscalla?
Jeżeli tak — dlaczego?
Jeżeli nie — dlaczego jest dostępny?
To jest różnica między:
security by checkbox
a:
security by policy
16. io_uring pokazuje, dlaczego temat jest trudny
Nowoczesne aplikacje korzystają z coraz bardziej rozbudowanych interfejsów kernela.
io_uring jest dobrym przykładem.
Jeżeli aplikacja potrzebuje określonych operacji I/O, blokowanie całego interfejsu może spowodować awarię.
Z drugiej strony nowe mechanizmy kernela zwiększają powierzchnię, którą administrator powinien rozumieć.
Dlatego profil seccomp nie może być raz napisany i zapomniany.
Aktualizacja aplikacji może zmienić jej zachowanie.
Aktualizacja kernela może zmienić dostępne interfejsy.
Nowa biblioteka może zmienić używane syscalle.
Hardening musi więc być utrzymywany.
17. Najlepszy model: deny by default
Dojrzały profil powinien zmierzać w stronę:
SYSCALL
|
v
+---------------+
| SECcomp policy|
+---------------+
| |
known unknown
syscall syscall
| |
ALLOW DENY
Ale trzeba uważać na jedno słowo:
known.
„Known” oznacza tutaj nie:
„znam nazwę syscalla”
ale:
„wiem, dlaczego aplikacja go potrzebuje”.
To ogromna różnica.
18. Co daje seccomp po przejęciu procesu?
Załóżmy:
Vulnerability
|
v
Remote Code Execution
|
v
Application process
|
v
Attacker-controlled code
Bez dodatkowych ograniczeń kod atakującego może próbować korzystać z dostępnego interfejsu kernela.
Z seccomp:
Attacker code
|
v
syscall
|
v
SECcomp
/ \
ALLOW DENY
| |
v v
kernel failure
Nie oznacza to, że exploit automatycznie przestaje działać.
Może nadal:
czytać dane aplikacji
kraść tokeny
komunikować się z siecią
modyfikować dostępne pliki
uruchamiać procesy
Seccomp ogranicza konkretną klasę działań.
Ale jeżeli atak wymaga syscalla znajdującego się poza dozwolonym zestawem, atakujący trafia na dodatkową ścianę.
I o to chodzi w defense in depth.
19. Jak wdrożyć seccomp bez rozwalenia produkcji?
Praktyczny proces powinien wyglądać mniej więcej tak:
Etap 1 — poznaj aplikację
Uruchom ją w normalnym środowisku.
Zbierz syscall’e.
strace -f -c ./app
Etap 2 — zbierz scenariusze
Nie wystarczy startup.
Przetestuj:
startup
normal request
heavy request
network error
logging
restart
reload
child process
TLS
DNS
filesystem
backup
Etap 3 — znajdź wspólny zestaw
Nie interesuje nas:
syscall wykonany raz podczas testu
tylko:
syscall rzeczywiście potrzebny aplikacji
Etap 4 — przygotuj politykę
Najpierw allowlista.
Potem testy.
Etap 5 — uruchom w środowisku testowym
Monitoruj:
EPERM
SIGSYS
application errors
service restart
timeouts
failed requests
Etap 6 — dopiero produkcja
I tutaj polityka musi być traktowana jak kod.
Powinna mieć:
wersję
repozytorium
review
testy
rollback
dokumentację
20. Seccomp nie zastępuje AppArmor
To szczególnie ważne po ostatnim artykule serii.
Możemy myśleć o tym tak:
PROCESS
|
+-----------+-----------+
| | |
DAC capabilities LSM
| | |
+-----------+-----------+
|
seccomp
|
syscall
|
KERNEL
AppArmor może powiedzieć:
„Nie pozwalam temu procesowi czytać tego pliku.”
Seccomp może powiedzieć:
„Nie pozwalam temu procesowi wykonać tego syscalla.”
Capabilities mogą powiedzieć:
„Ten proces nie ma określonej możliwości uprzywilejowanej operacji.”
Namespace może powiedzieć:
„Ten proces znajduje się w odizolowanej przestrzeni zasobów.”
To nie są konkurencyjne technologie.
One rozwiązują różne problemy.
21. Najważniejszy błąd administratora
Największym błędem nie jest brak seccomp.
Największym błędem jest przekonanie:
„Mam seccomp, więc proces jest sandboxowany.”
Nie.
Kernel Linux sam ostrzega przed takim uproszczeniem. Seccomp jest mechanizmem redukcji powierzchni syscalli, a nie kompletnym systemem izolacji.
Dlatego prawidłowy model wygląda tak:
UNTRUSTED INPUT
|
v
APPLICATION
|
+----------+----------+
| | |
DAC AppArmor seccomp
| | |
+----------+----------+
|
capabilities
|
namespace
|
v
KERNEL
Każda kolejna warstwa ma ograniczyć konsekwencje przejęcia poprzedniej.
22. Co sprawdzić na własnym serwerze?
Na początek:
grep Seccomp /proc/1/status
Dla konkretnej usługi:
pidof nginx
następnie:
grep -E 'Seccomp|NoNewPrivs' /proc/<PID>/status
Sprawdź konfigurację systemd:
systemctl cat nginx
oraz:
systemctl show nginx \
-p NoNewPrivileges \
-p SystemCallFilter \
-p CapabilityBoundingSet
Dla kontenerów:
docker inspect <container>
oraz sprawdź, czy ktoś nie uruchomił kontenera z:
seccomp=unconfined
Jeżeli znajdziesz:
--security-opt seccomp=unconfined
nie zakładaj automatycznie, że konfiguracja jest błędna.
Zadaj inne pytanie:
Dlaczego ta usługa tego potrzebuje?
Jeżeli odpowiedź brzmi:
„Bo kiedyś nie działało.”
to właśnie znalazłeś zadanie dla audytu.
Seccomp jako ostatnia linia przed kernelem
Najciekawsze w seccomp nie jest to, że potrafi zablokować syscall.
Najciekawsze jest miejsce, w którym działa.
Proces może mieć:
UID
GID
groups
capabilities
plikowe uprawnienia
AppArmor
namespace
a następnie próbuje wejść do kernela.
I właśnie tam możemy postawić jeszcze jedną kontrolę:
userspace
|
| syscall
v
+----------------+
| seccomp |
+----------------+
|
| allowed
v
+----------------+
| Linux kernel |
+----------------+
To bardzo elegancki mechanizm.
Nie próbujemy przewidzieć wszystkich działań atakującego.
Nie próbujemy stworzyć gigantycznej listy wszystkich możliwych exploitów.
Ograniczamy interfejs, przez który proces może oddziaływać na kernel.
To jest właśnie sedno Linux Hardening.
Nie chodzi o stworzenie systemu, którego nie da się zaatakować.
Chodzi o stworzenie systemu, w którym kompromitacja jednego procesu nie daje automatycznie dostępu do całej reszty.
A seccomp jest jedną z najbardziej bezpośrednich metod zmniejszenia tej granicy.







