Seccomp w Kubernetes: 7 rzeczy, które musisz wiedzieć od samego początku

Notatka. przeł.: Przedstawiamy Państwu tłumaczenie artykułu starszego inżyniera bezpieczeństwa aplikacji w brytyjskiej firmie ASOS.com. Wraz z nim rozpoczyna cykl publikacji poświęconych poprawie bezpieczeństwa w Kubernetesie poprzez zastosowanie seccomp. Jeśli czytelnikom spodoba się wprowadzenie, będziemy śledzić autora i kontynuować jego przyszłe materiały na ten temat.

Seccomp w Kubernetes: 7 rzeczy, które musisz wiedzieć od samego początku

Artykuł ten jest pierwszym z serii postów na temat tworzenia profili seccomp w duchu SecDevOps, bez uciekania się do magii i czarów. W części XNUMX omówię podstawy i wewnętrzne szczegóły wdrażania seccomp w Kubernetes.

Ekosystem Kubernetes oferuje szeroką gamę sposobów zabezpieczania i izolowania kontenerów. Artykuł dotyczy trybu bezpiecznego przetwarzania danych, znanego również jako druga komp. Jego istotą jest filtrowanie wywołań systemowych dostępnych do wykonania według kontenerów.

Dlaczego to jest ważne? Kontener to po prostu proces działający na określonej maszynie. Używa jądra tak samo, jak inne aplikacje. Gdyby kontenery mogły wykonywać dowolne wywołania systemowe, już wkrótce złośliwe oprogramowanie wykorzystałoby to do ominięcia izolacji kontenerów i wpływania na inne aplikacje: przechwytywania informacji, zmiany ustawień systemowych itp.

Profile seccomp definiują, które wywołania systemowe powinny być dozwolone, a które wyłączone. Środowisko wykonawcze kontenera aktywuje je po uruchomieniu, aby jądro mogło monitorować ich wykonanie. Korzystanie z takich profili pozwala ograniczyć wektor ataku i zmniejszyć szkody, jeśli jakikolwiek program wewnątrz kontenera (to znaczy Twoje zależności lub ich zależności) zacznie robić coś, czego nie wolno mu robić.

Dotarcie do podstaw

Podstawowy profil seccomp składa się z trzech elementów: defaultAction, architectures (or 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"
        }
    ]
}

(średni-podstawowy-seccomp.json)

defaultAction określa domyślny los wszelkich wywołań systemowych nieokreślonych w tej sekcji syscalls. Dla ułatwienia skupmy się na dwóch głównych wartościach, które będą stosowane:

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

W sekcji architectures docelowe architektury są wymienione. Jest to o tyle istotne, że sam filtr, stosowany na poziomie jądra, zależy od identyfikatorów wywołań systemowych, a nie od ich nazw podanych w profilu. Środowisko wykonawcze kontenera dopasuje je do identyfikatorów przed użyciem. Pomysł jest taki, że wywołania systemowe mogą mieć zupełnie różne identyfikatory w zależności od architektury systemu. Na przykład wywołanie systemowe recvfrom (używany do odbierania informacji z gniazda) ma identyfikator = 64 w systemach x64 i identyfikator = 517 w systemach x86. Tutaj możesz znaleźć listę wszystkich wywołań systemowych dla architektur x86-x64.

W dziale syscalls wyświetla listę wszystkich wywołań systemowych i określa, co z nimi zrobić. Na przykład możesz utworzyć białą listę, ustawiając defaultAction na SCMP_ACT_ERRNOi wywołania w sekcji syscalls właściwy SCMP_ACT_ALLOW. Dlatego zezwalasz tylko na połączenia określone w tej sekcji syscallsi zabraniaj wszystkich innych. W przypadku czarnej listy należy zmienić wartości defaultAction i działania wręcz przeciwne.

Teraz powinniśmy powiedzieć kilka słów o niuansach, które nie są tak oczywiste. Pamiętaj, że w poniższych zaleceniach założono, że wdrażasz szereg aplikacji biznesowych na platformie Kubernetes i chcesz, aby działały z możliwie najmniejszymi uprawnieniami.

1. Zezwól na EskalacjęPrivilege=false

В securityContext kontener ma parametr AllowPrivilegeEscalation. Jeśli jest zainstalowany w false, kontenery zaczną się od (on) fragment no_new_priv. Znaczenie tego parametru jest oczywiste już z nazwy: zapobiega on uruchamianiu przez kontener nowych procesów z większymi uprawnieniami niż on sam.

Efekt uboczny ustawienia tej opcji true (domyślnie) środowisko wykonawcze kontenera stosuje profil seccomp na samym początku procesu uruchamiania. Dlatego wszystkie wywołania systemowe wymagane do uruchomienia wewnętrznych procesów wykonawczych (np. ustawianie identyfikatorów użytkowników/grup, rezygnacja z niektórych funkcji) muszą być włączone w profilu.

Do kontenera, który robi banalne rzeczy echo hiwymagane będą następujące uprawnienia:

{
    "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 jeszcze raz: dlaczego jest to problem? Osobiście unikałbym umieszczania na białej liście następujących wywołań systemowych (chyba że istnieje realna potrzeba): capset, set_tid_address, setgid, setgroups и setuid. Jednak prawdziwym wyzwaniem jest to, że zezwalając na procesy, nad którymi nie masz żadnej kontroli, wiążesz profile z implementacją środowiska wykonawczego kontenera. Innymi słowy, pewnego dnia może się okazać, że po aktualizacji środowiska wykonawczego kontenerów (przez Ciebie lub, co bardziej prawdopodobne, przez dostawcę usług w chmurze), kontenery nagle przestaną działać.

Porada nr 1: Uruchom kontenery za pomocą AllowPrivilegeEscaltion=false. Zmniejszy to rozmiar profili seccomp i sprawi, że będą one mniej wrażliwe na zmiany w środowisku wykonawczym kontenera.

2. Ustawianie profili seccomp na poziomie kontenera

Profil seccomp można ustawić na poziomie podu:

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 pamiętać, że powyższa składnia ulegnie zmianie, gdy Kubernetes seccomp stanie się GA (tego zdarzenia należy spodziewać się w kolejnej wersji Kubernetesa – 1.18 – ok. tł.).

Niewiele osób wie, że Kubernetes zawsze tak miał błądco spowodowało zastosowanie profili seccomp wstrzymaj kontener. Środowisko wykonawcze częściowo rekompensuje tę niedogodność, ale kontener ten nie znika z podów, gdyż służy do konfiguracji ich infrastruktury.

Problem w tym, że ten kontener zawsze zaczyna się od AllowPrivilegeEscalation=true, co prowadzi do problemów wskazanych w ust. 1 i nie można tego zmienić.

Używając profili seccomp na poziomie kontenera, unikasz tej pułapki i możesz stworzyć profil dostosowany do konkretnego kontenera. Trzeba będzie to zrobić, dopóki programiści nie naprawią błędu i nowa wersja (może 1.18?) nie stanie się dostępna dla wszystkich.

Porada nr 2: Ustaw profile seccomp na poziomie kontenera.

W praktyce zasada ta służy zwykle jako uniwersalna odpowiedź na pytanie: „Dlaczego mój profil seccomp działa docker runale nie działa po wdrożeniu w klastrze Kubernetes?

3. Używaj środowiska wykonawczego/domyślnego tylko w ostateczności

Kubernetes ma dwie opcje wbudowanych profili: runtime/default и docker/default. Obydwa są implementowane przez środowisko wykonawcze kontenera, a nie przez Kubernetes. Dlatego mogą się różnić w zależności od używanego środowiska uruchomieniowego i jego wersji.

Innymi słowy, w wyniku zmiany środowiska wykonawczego kontener może mieć dostęp do innego zestawu wywołań systemowych, z których może skorzystać lub nie. Większość środowisk wykonawczych używa Implementacja Dockera. Jeśli chcesz korzystać z tego profilu, upewnij się, że jest on dla Ciebie odpowiedni.

profil docker/default jest przestarzały od wersji Kubernetes 1.11, więc unikaj jego używania.

Moim zdaniem profil runtime/default doskonale nadaje się do celu, dla którego został stworzony: ochrony użytkowników przed zagrożeniami związanymi z wykonaniem polecenia docker run na swoich samochodach. Jeśli jednak chodzi o aplikacje biznesowe działające na klastrach Kubernetesa, odważyłbym się stwierdzić, że taki profil jest zbyt otwarty i programiści powinni skupić się na tworzeniu profili dla swoich aplikacji (lub typów aplikacji).

Porada nr 3: Tworzenie profili seccomp dla określonych zastosowań. Jeśli nie jest to możliwe, utwórz profile dla typów aplikacji, na przykład utwórz profil zaawansowany, który zawiera wszystkie internetowe interfejsy API aplikacji Golang. Używaj środowiska wykonawczego/domyślnego tylko w ostateczności.

W przyszłych postach omówię, jak tworzyć profile seccomp inspirowane SecDevOps, automatyzować je i testować w potokach. Innymi słowy, nie będziesz miał wymówki, aby nie uaktualnić do profili specyficznych dla aplikacji.

4. Bez ograniczeń NIE wchodzi w grę.

Z pierwszy audyt bezpieczeństwa Kubernetesa okazało się, że domyślnie seccom wyłączony. Oznacza to, że jeśli nie ustawisz PodSecurityPolicy, co umożliwi to w klastrze, będą działać wszystkie pody, dla których nie zdefiniowano profilu seccomp seccomp=unconfined.

Praca w tym trybie oznacza utratę całej warstwy izolacji chroniącej klaster. Takie podejście nie jest zalecane przez ekspertów ds. bezpieczeństwa.

Porada nr 4: Żaden kontener w klastrze nie powinien być uruchomiony seccomp=unconfinedzwłaszcza w środowiskach produkcyjnych.

5. „Tryb audytu”

Ten punkt nie dotyczy wyłącznie Kubernetesa, ale nadal należy do kategorii „co warto wiedzieć przed rozpoczęciem”.

Tak się składa, że ​​tworzenie profili seccomp zawsze było wyzwaniem i opierało się w dużej mierze na próbach i błędach. Faktem jest, że użytkownicy po prostu nie mają możliwości przetestowania ich na środowiskach produkcyjnych bez ryzyka „upuszczenia” aplikacji.

Po wydaniu jądra Linuksa 4.14 stało się możliwe uruchamianie części profilu w trybie audytu, rejestrując informacje o wszystkich wywołaniach systemowych w syslogu, ale bez ich blokowania. Możesz aktywować ten tryb za pomocą parametru SCMT_ACT_LOG:

SCMP_ACT_LOG: seccomp nie wpłynie na wątek wykonujący wywołanie systemowe, jeśli nie pasuje do żadnej reguły w filtrze, ale informacja o wywołaniu systemowym zostanie zarejestrowana.

Oto typowa strategia korzystania z tej funkcji:

  1. Zezwalaj na potrzebne wywołania systemowe.
  2. Blokuj połączenia z systemu, o których wiesz, że nie będą przydatne.
  3. Zapisuj informacje o wszystkich pozostałych połączeniach 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"
        }
    ]
}

(średni-mieszany-seccomp.json)

Pamiętaj jednak, że musisz zablokować wszystkie wywołania, o których wiesz, że nie będą używane i które mogą potencjalnie zaszkodzić klasterowi. Dobrą podstawą do sporządzenia listy jest oficjalna Dokumentacja Dockera. Wyjaśnia szczegółowo, które wywołania systemowe są blokowane w profilu domyślnym i dlaczego.

Jest jednak jeden haczyk. Chociaż SCMT_ACT_LOG wspierany przez jądro Linuksa od końca 2017 roku, do ekosystemu Kubernetes wszedł stosunkowo niedawno. Dlatego, aby skorzystać z tej metody, będziesz potrzebować jądra Linuksa 4.14 i wersji runC nie niższej v1.0.0-rc9.

Porada nr 5: Można utworzyć profil trybu audytu do testowania w środowisku produkcyjnym, łącząc czarne i białe listy, a wszystkie wyjątki mogą być rejestrowane.

6. Korzystaj z białych list

Dodanie białej listy wymaga dodatkowego wysiłku, ponieważ trzeba zidentyfikować każde wywołanie, którego może potrzebować aplikacja, ale takie podejście znacznie poprawia bezpieczeństwo:

Zdecydowanie zaleca się stosowanie metody białej listy, ponieważ jest ona prostsza i bardziej niezawodna. Czarna lista będzie musiała zostać zaktualizowana za każdym razem, gdy zostanie dodane potencjalnie niebezpieczne wywołanie systemowe (lub niebezpieczna flaga/opcja, jeśli znajduje się na czarnej liście). Ponadto często można zmienić reprezentację parametru bez zmiany jego istoty i tym samym ominąć ograniczenia czarnej listy.

Dla aplikacji Go opracowałem specjalne narzędzie, które jest dołączone do aplikacji i zbiera wszystkie wywołania wykonane podczas jej działania. Na przykład dla następującej aplikacji:

package main

import "fmt"

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

...startujemy gosystract w następujący sposób:

go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf ""%s",n" .Name}}{{- end}}' application-path

... i otrzymujemy następujący wynik:

"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 — więcej szczegółów na temat narzędzi pojawi się później.

Porada nr 6: Zezwalaj tylko na te połączenia, których naprawdę potrzebujesz, i blokuj wszystkie inne.

7. Połóż odpowiednie fundamenty (lub przygotuj się na nieoczekiwane zachowanie)

Jądro wymusi profil niezależnie od tego, co w nim napiszesz. Nawet jeśli nie jest to dokładnie to, czego chciałeś. Na przykład, jeśli zablokujesz dostęp do połączeń takich jak exit lub exit_group, kontener nie będzie w stanie poprawnie zamknąć, ani nawet za pomocą prostego polecenia, takiego jak echo hi powieś goo na czas nieokreślony. W rezultacie uzyskasz wysokie użycie procesora w klastrze:

Seccomp w Kubernetes: 7 rzeczy, które musisz wiedzieć od samego początku

W takich przypadkach na ratunek może przyjść narzędzie strace - pokaże w czym może być problem:

Seccomp w Kubernetes: 7 rzeczy, które musisz wiedzieć od samego początku
sudo strace -c -p 9331

Upewnij się, że profile zawierają wszystkie wywołania systemowe potrzebne aplikacji w czasie wykonywania.

Porada nr 7: Zwróć uwagę na szczegóły i upewnij się, że wszystkie niezbędne wywołania systemowe znajdują się na białej liście.

Na tym kończy się pierwsza część serii artykułów na temat wykorzystania seccomp w Kubernetesie w duchu SecDevOps. W kolejnych częściach porozmawiamy o tym, dlaczego jest to ważne i jak zautomatyzować proces.

PS od tłumacza

Przeczytaj także na naszym blogu:

Źródło: www.habr.com

Dodaj komentarz