Seccomp w Kubernetes: 7 rzeczy, które należy znać od samego początku

Przyp. tłum.: Przedstawiamy tłumaczenie artykułu starszego inżyniera ds. bezpieczeństwa aplikacji brytyjskiej firmy ASOS.com. Rozpoczyna on cykl publikacji poświęconych zwiększaniu bezpieczeństwa w Kubernetes dzięki wykorzystaniu seccomp. Jeśli wprowadzenie przypadnie do gustu czytelnikom, podążymy za autorem i będziemy kontynuować jego przyszłe materiały na ten temat.

Seccomp w Kubernetes: 7 rzeczy, które należy znać od samego początku

Artykuł ten jest pierwszą częścią serii publikacji na temat tworzenia profili seccomp w duchu SecDevOps, bez korzystania z magii czy czarów. W pierwszej części omówię podstawy i szczegóły implementacji seccomp w Kubernetes.

Ekosystem Kubernetes oferuje wystarczającą różnorodność sposobów na zapewnienie bezpieczeństwa i izolacji kontenerów. Artykuł dotyczy Trybu Bezpiecznego Obliczeń, znanego również jako seccomp. Jego istota polega na filtrowaniu wywołań systemowych, które mogą być wykonywane przez kontenery.

Dlaczego to ważne? Kontener to tylko proces uruchomiony na określonej maszynie. Używa on jądra na równi z innymi aplikacjami. Gdyby kontenery mogły wykonywać dowolne wywołania systemowe, z niedługim czasem mogłyby to wykorzystać złośliwe programy, aby obejść izolację kontenera i wpływać na inne aplikacje: przechwytywać informacje, zmieniać ustawienia systemu itp.

Profile seccomp określają, które wywołania systemowe powinny być dozwolone lub zabronione. Środowisko wykonawcze kontenera aktywuje je podczas jego uruchamiania, aby jądro mogło kontrolować ich wykonanie. Zastosowanie takich profili pozwala ograniczyć wektor ataku i zredukować szkody w przypadku, gdy jakikolwiek program w obrębie kontenera (czyli twoje zależności lub ich zależności) zacznie robić coś, co nie jest dozwolone.

Zajmijmy się podstawami

Podstawowy profil seccomp zawiera trzy elementy: defaultAction, architectures (lub archMap) i syscalls:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(medium-basic-seccomp.json)

defaultAction określa domyślny los każdego wywołania systemowego, które nie jest wymienione w sekcji syscalls. Aby uprościć zadanie, skoncentrujmy się na dwóch głównych wartościach, które będą używane:

  • SCMP_ACT_ERRNO — blokuje wykonanie wywołania systemowego,
  • SCMP_ACT_ALLOW — zezwala.

W sekcji architectures wymienione są cele architektury. Jest to istotne, ponieważ sam filtr stosowany na poziomie jądra zależy od identyfikatorów wywołań systemowych, a nie od ich nazw zapisanych w profilu. Przed zastosowaniem środowisko uruchomieniowe kontenera mapuje je z identyfikatorami. Sens polega na tym, że wywołania systemowe mogą mieć zupełnie różne ID w zależności od architektury systemu. Na przykład, wywołanie systemowe recvfrom używane do odbierania informacji z gniazda ma ID = 64 w systemach x64 i ID = 517 w systemach x86. Tutaj możesz znaleźć listę wszystkich wywołań systemowych dla architektur x86-x64.

W sekcji syscalls wymienione są wszystkie wywołania systemowe i określone, co z nimi należy zrobić. Na przykład, można stworzyć białą listę, ustawiając defaultAction na SCMP_ACT_ERRNO, a wywołaniom w sekcji syscalls przypisać SCMP_ACT_ALLOW. W ten sposób zezwalasz tylko na wywołania wymienione w sekcji syscalls, a wszystkie pozostałe są zabronione. Dla czarnej listy należy zamienić wartości defaultAction i działania na przeciwne.

Teraz warto wspomnieć kilka słów o niuansach, które nie są tak oczywiste. Zauważ, że poniższe zalecenia pochodzą z założenia, że rozwijasz linię aplikacji biznesowych w Kubernetes i ważne jest, aby działały z jak najmniejszymi uprawnieniami.

1. AllowPrivilegeEscalation=false

W securityContext w kontekście kontenera znajduje się parametr AllowPrivilegeEscalation. Jeśli jest ustawiony na false, kontenery będą uruchamiane z ustawionym (włączony) bitem no_new_priv. Sens tego parametru jest oczywisty z nazwy: nie pozwala to kontenerowi uruchamiać nowych procesów z uprawnieniami przekraczającymi te, które ma sam.

Efektem ubocznym ustawienia tego parametru na true wartość domyślna, jest to, że środowisko uruchomieniowe kontenera stosuje profil seccomp na początku procesu uruchamiania. W ten sposób wszystkie wywołania systemowe potrzebne do uruchomienia wewnętrznych procesów środowiska uruchomieniowego (np. ustalanie identyfikatorów użytkownika/grupy, odrzucanie niektórych uprawnień), muszą być dozwolone w profilu.

Kontener, który wykonuje proste echo hi, będzie potrzebował następujących uprawnień:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "capget",
                "capset",
                "chdir",
                "close",
                "execve",
                "exit_group",
                "fstat",
                "fstatfs",
                "futex",
                "getdents64",
                "getppid",
                "lstat",
                "mprotect",
                "nanosleep",
                "newfstatat",
                "openat",
                "prctl",
                "read",
                "rt_sigaction",
                "statfs",
                "setgid",
                "setgroups",
                "setuid",
                "stat",
                "uname",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-pod-seccomp.json)

… zamiast tych:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "close",
                "execve",
                "exit_group",
                "futex",
                "mprotect",
                "nanosleep",
                "stat",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-container-seccomp.json)

Ale znowu, dlaczego to jest problem? Osobiście unikałbym dodawania do białej listy następujących wywołań systemowych (jeśli nie ma realnej potrzeby): capset, set_tid_address, setgid, setgroups i setuid. Jednak prawdziwy problem polega na tym, że zezwalając na procesy, których całkowicie nie kontrolujesz, wiążesz profile z implementacją środowiska uruchomieniowego kontenerów. Innymi słowy, pewnego dnia możesz napotkać sytuację, w której po aktualizacji środowiska uruchomieniowego kontenera (przez ciebie lub, co bardziej prawdopodobne, dostawcę chmury) kontenery nagle przestaną działać.

Porada nr 1: Uruchamiaj kontenery z AllowPrivilegeEscaltion=false. To zmniejszy rozmiar profili seccomp i uczyni je mniej wrażliwymi na zmiany w środowisku uruchomieniowym kontenera.

2. Ustawianie profili seccomp na poziomie kontenera

Profil seccomp można ustawić na poziomie pod’a:

annotations:
  seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"

… lub na poziomie kontenera:

annotations:
  container.security.alpha.kubernetes.io/<container-name>: "localhost/profile.json"

Należy zauważyć, że powyższa składnia zmieni się, gdy Kubernetes seccomp stanie się GA (to zdarzenie oczekiwane jest w następnej wersji Kubernetes — 1.18 — przyp. tłum.).

Mało kto wie, że w Kubernetes zawsze istniał błąd, który sprawiał, że profile seccomp stosowano do kontenera pauzyŚrodowisko wykonawcze częściowo rekompensuje ten brak, jednak ten kontener nie znika z pod'ów, ponieważ jest używany do konfigurowania ich infrastruktury.

Problem polega na tym, że ten kontener zawsze uruchamia się z AllowPrivilegeEscalation=true, co prowadzi do problemów opisanych w punkcie 1, a zmiana tego jest niemożliwa.

Stosując profile seccomp na poziomie kontenera, unikasz tej pułapki i możesz stworzyć profil, który będzie 'dostosowany' do konkretnego kontenera. Tak trzeba postępować, dopóki twórcy nie naprawią błędu, a nowa wersja (może 1.18?) nie stanie się dostępna dla wszystkich zainteresowanych.

Porada nr 2: Ustalaj profile seccomp na poziomie kontenera.

W praktyce ta zasada zazwyczaj stanowi uniwersalną odpowiedź na pytanie: „Dlaczego mój profil seccomp działa z docker run, ale nie działa po wdrożeniu w klastrze Kubernetes?”.

3. Używaj runtime/default tylko w ostateczności

W Kubernetes są dwa wbudowane profile: runtime/default i docker/default. Oba są realizowane przez środowisko wykonawcze kontenera, a nie przez Kubernetes. Dlatego mogą się różnić w zależności od używanego środowiska wykonawczego i jego wersji.

Innymi słowy, w wyniku zmiany runtime kontener może uzyskać dostęp do innego zbioru wywołań systemowych, które może używać lub nie używać. Większość środowisk wykonawczych używa implementacji Docker. Jeśli chcesz użyć tego profilu, upewnij się, że jest on dla Ciebie odpowiedni.

Profil docker/default jest uznawany za przestarzały od wersji Kubernetes 1.11, dlatego unikaj jego stosowania.

Moim zdaniem profil runtime/default idealnie nadaje się do celów, dla których został stworzony: ochrony użytkowników przed ryzykiem związanym z wykonywaniem polecenia docker run na ich maszynach. Jednak mówiąc o aplikacjach biznesowych działających w klastrach Kubernetes, śmiem twierdzić, że taki profil jest zbyt otwarty i programiści powinni skoncentrować się na tworzeniu profili dla swoich aplikacji (lub typów aplikacji).

Porada nr 3: Twórz profile seccomp pod konkretne aplikacje. Jeśli to niemożliwe, zajmij się profilami dla typów aplikacji, na przykład stwórz rozszerzony profil, który uwzględni wszystkie web-API aplikacji w Golangu. Tylko w ostateczności używaj runtime/default.

W przyszłych publikacjach opowiem, jak stworzyć profile seccomp w duchu SecDevOps, zautomatyzować je i testować w pipeline'ach. Innymi słowy, nie będzie już żadnych wymówek, aby nie przechodzić na profile zdefiniowane dla konkretnych aplikacji.

4. Unconfined — to NIE jest opcja

Z pierwszego audytu bezpieczeństwa Kubernetes okazało się, że domyślnie seccomp jest wyłączony. Oznacza to, że jeśli nie zdefiniujesz PodSecurityPolicy, która włączy go w klastrze, wszystkie pod'y, dla których nie określono profilu seccomp, będą działać w trybie seccomp=unconfined.

Praca w takim trybie oznacza, że tracimy cały poziom izolacji, zapewniający ochronę klastra. Tego podejścia nie zalecają specjaliści ds. bezpieczeństwa.

Porada nr 4: Żaden kontener w klastrze nie powinien działać w trybie seccomp=unconfined, szczególnie w środowiskach produkcyjnych.

5. „Tryb audytu”

Ten aspekt nie jest unikalny dla Kubernetes, ale wciąż mieści się w kategorii „o czym warto wiedzieć jeszcze przed rozpoczęciem”.

Tradycyjnie tworzenie profili seccomp zawsze było trudnym zadaniem i w dużej mierze opierało się na metodzie prób i błędów. Chodzi o to, że użytkownicy po prostu nie mają możliwości przetestowania ich w środowiskach produkcyjnych, nie ryzykując „zwalenia” aplikacji.

Po wprowadzeniu jądra Linux 4.14 pojawiła się możliwość uruchamiania części profilu w trybie audytu, rejestrując w syslog informacje o wszystkich wywołaniach systemowych, nie blokując ich. Można aktywować ten tryb za pomocą parametru SCMT_ACT_LOG:

SCMP_ACT_LOG: seccomp nie wpłynie na działanie wątku, który wykonuje wywołanie systemowe, jeśli nie podlega żadnej regule z filtra, jednak informacje o wywołaniu systemowym zostaną zapisane w dzienniku.

Oto typowa strategia korzystania z tej możliwości:

  1. Zezwól na wywołania systemowe, które są niezbędne.
  2. Zablokuj wywołania systemowe, które wiadomo, że nie będą potrzebne.
  3. Informacje o wszystkich pozostałych wywołaniach zapisuj w dzienniku.

Uproszczony przykład wygląda następująco:

{
    "defaultAction": "SCMP_ACT_LOG",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "names": [
                "add_key",
                "keyctl",
                "ptrace"
            ],
            "action": "SCMP_ACT_ERRNO"
        }
    ]
}

(medium-mixed-seccomp.json)

Jednak pamiętaj, że należy zablokować wszystkie wywołania, o których wiadomo, że nie będą używane, a które potencjalnie mogą zaszkodzić klasterowi. Dobrym punktem wyjścia do stworzenia listy jest oficjalna dokumentacja Docker. Zawiera ona szczegółowe wyjaśnienia dotyczące tego, jakie wywołania systemowe zostały zablokowane w profilu domyślnym i dlaczego.

Jednak istnieje jedno zastrzeżenie. Chociaż SCMT_ACT_LOG jest obsługiwany przez jądro Linux od końca 2017 roku, do ekosystemu Kubernetes wszedł stosunkowo niedawno. Dlatego do korzystania z tej metody będzie potrzebne jądro Linux w wersji 4.14 oraz runC w wersji co najmniej v1.0.0-rc9.

Rada nr 5: Profil trybu audytu do testowania w produkcji można stworzyć, łącząc czarne i białe listy, a wszystkie wyjątki rejestrując w dzienniku.

6. Używaj białych list

Tworzenie białych list wymaga dodatkowego wysiłku, ponieważ trzeba zidentyfikować każde wywołanie, które może być potrzebne aplikacji, jednak podejście to znacznie zwiększa bezpieczeństwo:

Zdecydowanie zaleca się stosowanie podejścia opartego na białych listach, ponieważ jest ono prostsze i bardziej niezawodne. Czarne listy należy aktualizować za każdym razem przy dodawaniu potencjalnie niebezpiecznego wywołania systemowego (lub niebezpiecznej flagi/opcji, jeśli znajdują się na czarnej liście). Ponadto, często można zmienić reprezentację parametru, nie zmieniając jego istoty i tym samym obejść ograniczenia czarnej listy.

Dla aplikacji napisanych w języku Go opracowałem specjalne narzędzie, które towarzyszy aplikacji i zbiera wszystkie wywołania dokonane podczas wykonywania. Na przykład dla następującej aplikacji:

package main

import "fmt"

func main() {
	fmt.Println("test")
}

… uruchomimy gosystract tak:

go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf "%s", .Name}}{{- end}}' ścieżka-do-aplikacji

… i uzyskamy następujący rezultat:

"sched_yield",
"futex",
"write",
"mmap",
"exit_group",
"madvise",
"rt_sigprocmask",
"getpid",
"gettid",
"tgkill",
"rt_sigaction",
"read",
"getpgrp",
"arch_prctl",

Na razie to tylko przykład — szczegóły dotyczące narzędzi zostaną podane później.

Rada nr 6: Zezwalaj tylko na te wywołania, które są naprawdę potrzebne, i blokuj wszystkie inne.

7. Stwórz solidne podstawy (lub przygotuj się na nieprzewidziane zachowanie)

Jądro będzie monitorować przestrzeganie profilu bez względu na to, co w nim zapisano. Nawet jeśli nie jest to do końca to, czego się oczekiwało. Na przykład, jeśli zablokujesz dostęp do wywołań, takich jak exit lub exit_group, kontener nie będzie w stanie poprawnie zakończyć działania, a nawet prosta komenda typu echo hi zawiesi gona czas nieokreślony. W rezultacie będziesz miał wysokie obciążenie CPU w klastrze:

Seccomp w Kubernetes: 7 rzeczy, które należy znać od samego początku

W takich przypadkach przydatne może okazać się narzędzie strace — pokaże, gdzie może tkwić problem:

Seccomp w Kubernetes: 7 rzeczy, które należy znać od samego początku
sudo strace -c -p 9331

Upewnij się, że profile zawierają wszystkie wywołania systemowe potrzebne aplikacji podczas działania.

Rada nr 7: Zwracaj uwagę na szczegóły i sprawdź, czy wszystkie niezbędne wywołania systemowe są uwzględnione na liście białej.

Pierwsza część cyklu artykułów o wykorzystaniu seccomp w Kubernetes w duchu SecDevOps dobiega końca. W kolejnych częściach omówimy, dlaczego to ważne i jak zautomatyzować ten proces.

P.S. od tłumacza

Przeczytaj także na naszym blogu:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster