W Comtegrze umożliwiamy naszym klientom korzystanie z kontenerów z dostępem do
GPU na zasadzie chmury: Comtegra GPU Cloud (CGC).
Dla wielu z nich dostęp z poziomu przeglądarki jest wygodny (np. Jupyter
Notebook).
Z biegiem czasu jednak pojawiły się pytania o bardziej bezpośredni dostęp do
powłoki kontenerów.
Zainspirowani mechanizmem działania Gita przez SSH, stworzyliśmy
rozwiązanie, dzięki któremu użytkownicy mogą zdalnie połączyć się do powłoki
swoich kontenerów w Kubernetes.
W tym artykule pierwszy raz rzucamy światło na komponenty CGC aby podzielić się
efektami naszej pracy z szerszym gronem odbiorców.
Żeby użytkownik mógł bezpiecznie zalogować się do powłoki kontenera, musieliśmy
rozwiązać 4 problemy.
- Uwierzytelnienie: kto próbuje się zalogować? Czy jest tym, za kogo się podaje?
- Autoryzacja: do których kontenerów powinien mieć dostęp?
- Powłoka: połączyć użytkownika do powłoki odpowiedniego kontenera.
- Zabezpieczenia: użytkownik powinien móc jedynie komunikować się z powłoką
kontenera. Pozostałe akcje powinny być zablokowane.
Użytkownicy mogą zalogować się do powłok swoich kontenerów przez SSH.
Uwierzytelniani i autoryzowani są za pomocą par kluczy, których publiczną część
dodali wcześniej do swojego konta w CGC.
Do tego wykorzystujemy opcję AuthorizedKeysCommand serwera OpenSSH (sshd).
Kontener wybierają, dodając jego nazwę do polecenia SSH (np.
ssh cgc@cgc-api.comtegra.cloud kontener1
).
Z powłoką łączymy użytkowników za pomocą wywołania systemowego exec i programu
kubectl exec.
Uwierzytelnienie
Już od wersji OpenSSH 6.2, opublikowanej w marcu 2013, sshd posiada opcję
AuthorizedKeysCommand.
Umożliwia ona dynamiczne generowanie list kluczy, które są uprawnione do łączenia
się z serwerem.
Wcześniej, listy mogły być jedynie ładowane z systemu plików.
Oprócz kluczy listy te zawierają opcje, które są przydatne do kontroli dostępu
np. wyłączenie przekierowywania portów, agentów i X11, wyłączenie alokacji PTY,
wykonywania zawartości plików ~/.ssh/rc.
Do tych opcji należy też wymuszenie użycia konkretnej powłoki.
W CGC podobnie jak w GitHubie, GitLabie itp. użytkownicy mogą dodać do swojego
konta publiczne części kluczy, którymi chcą się posługiwać w komunikacji SSH z
platformą.
Dzięki temu, możemy zidentyfikować użytkownika na podstawie klucza, jakim próbuje
się zalogować.
Weryfikacją klucza zajmuje się sshd.
Zobaczmy teraz, jak w praktyce możemy uwierzytelnić użytkownika za pomocą opcji
AuthorizedKeysCommand na podstawie bazy kluczy publicznych.
W konfiguracji sshd dodajemy następującą linię, która instruuje usługę, aby
podczas każdej próby logowania uruchamiała program cgc-keys z argumentami:
nazwa użytkownika (%u), typ klucza (%t), klucz (%k).
To, co ten program wypisze na standardowe wyjście, zostanie potraktowane przez
sshd jak zawartość pliku authorized_keys.
AuthorizedKeysCommand /usr/local/bin/cgc-keys "%u" "%t" "%k"
Program cgc-keys najpierw sprawdza, czy użytkownik loguje się jako cgc
(ssh cgc@host
), a następnie odpytuje bazę danych o użytkownika na podstawie
klucza publicznego.
Na tym etapie sshd już zweryfikował, że użytkownik posiada prywatną parę do
podanego klucza publicznego.
Zatem jeśli klucz znaleziono w bazie, to tożsamość pasującego użytkownika można
uznać za potwierdzoną.
Przykładowe fragmenty kodu w tym artykule zostały napisane w języku powłoki
kompatybilnej z /bin/sh.
Obsługa błędów w przykładach została ograniczona dla zwięzłości przykładów.
user="$1"
keytype="$2"
key="$3"
if [ "$user" != "cgc" ]; then
exit 0
fi
user=$(query_db "username" "$keytype" "$key")
if [ -z "$user" ]; then
exit 0
fi
Autoryzacja
W CGC użytkownicy mają dostęp do wszystkich kontenerów w przestrzeni nazw, do
której należą.
Zatem wystarczy, że określimy przestrzeń nazw, do której przyporządkowan=y jest
właściciel klucza.
Można to zrobić, zastępując zapytanie do bazy poniższym.
namespace=$(query_db "namespace" "$keytype" "$key")
if [ -z "$namespace" ]; then
exit 0
fi
Powłoka
Na tym etapie użytkownik został uwierzytelniony i autoryzowany do pewnej
przestrzeni nazw.
Pozostaje rozwiązanie problemu połączenia go do powłoki odpowiedniego kontenera.
Tym zajmie się program cgc-shell.
Wspomniałem wcześniej, że program cgc-keys powinien wypisać na standardowe
wyjście tekst w formacie authorized_keys.
Możemy wykorzystać opcje tego formatu do wymuszenia wykonania programu
cgc-shell przez użytkownika.
Jeśli wypiszemy linię jak niżej, to użytkownik zostanie zalogowany do serwera,
ale zamiast powłoki serwera lub innego, podanego polecenia (np.
ssh cgc@host ls
) w jego imieniu zostanie uruchomiony program cgc-shell z
argumentem $namespace
.
echo "command=\"cgc-shell $namespace\" $keytype $key cgc"
Program cgc-keys jest świadomie rozdzielny z programem cgc-shell, gdyż
pełnią one inną funkcję.
Pierwszy z nich, za pomocą opcji AuthorizedKeysCommandUser uwierzytelnia
użytkownika, natomiast
drugi stanowi jego powłokę.
Strumienie wejścia/wyjścia procesu cgc-shell są połączone z terminalem
użytkownika.
Kubernetes umożliwia wykonywanie dowolnych poleceń w kontenerach za pomocą
polecenia kubectl exec.
Możemy zatem wykonać odpowiednie polecenie kubectl w imieniu użytkownika, aby
połączyć go do powłoki żądanego kontenera.
Podmianę procesu cgc-shell na kubectl z odpowiednimi argumentami można
zrealizować wywołaniem systemowym exec.
W CGC użytkownicy wybierają kontener poprzez podanie nazwy deploymentu
(rozlokowania).
Takiemu deploymentowi w danej chwili prawie zawsze przypada jeden pod, do którego
z kolei przypisany jest zwykle jeden kontener.
Oznacza to, że po nazwie deploymentu, którą nadaje użytkownik można
zidentyfikować odpowiedni kontener.
Warto również zauważyć, że polecenie kubectl exec umożliwia podanie
deploymentu.
Wtedy samo spróbuje zidentyfikować odpowiedni kontener.
Jednak najpierw należy zdobyć od użytkownika odpowiednie argumenty, a mianowicie
nazwę deploymentu i polecenie, które zostanie wykonane w kontenerze.
Argumenty te użytkownik może przekazać, dopisując je do polecenia ssh (np.
ssh cgc@host deployment1 ls -la
).
sshd przekaże te argumenty do ssh-shell w zmiennej środowiskowej
SSH_ORIGINAL_COMMAND.
Pozostaje jedynie oddzielenie nazwy deploymentu od polecenia, które ma zostać
wykonane w kontenerze.
Do tego można wykorzystać set
, $1
, shift
oraz $@
jak pokazano poniżej.
namespace=$1
set -- $SSH_ORIGINAL_COMMAND
resource="$1"
if [ -z "$resource" ]; then
echo "Error: resource name not specified" >&2
exit 1
fi
shift
cmd="$@"
if [ -z "$cmd" ]; then
cmd='/bin/sh'
fi
exec kubectl --namespace "$namespace" \
exec --stdin --tty deployment/"$resource" -- $cmd
Zabezpieczenia
Kod i konfiguracja w tym artykule stanowią jedynie przykłady i nie były poddane
audytowi bezpieczeństwa.
Pozostało zadbać o bezpieczeństwo tego rozwiązania.
Jest klika rzeczy, które możemy zrobić w celu jego poprawy.
Wspominałem już, że format authorized_keys umożliwia ustawienie dodatkowych
opcji.
Jedną z nich jest restrict, która włącza wszystkie ograniczenia.
Należy do nich zablokowanie alokacji tty, która jest niezbędna do działania
interaktywnej powłoki.
Możemy jednak użyć opcji restrict i dodać wyjątek dla blokady tty za pomocą
opcji pty.
echo "restrict,pty,command=\"cgc-shell $namespace\" $keytype $key cgc"
Oprócz opcji restrict możemy również umocnić konfigurację samego serwera.
Poniżej znajdują się przydatne opcje sshd i ich wartości.
PermitRootLogin no
AuthorizedKeysFile /dev/null
AuthorizedKeysCommandUser nobody
PasswordAuthentication no
KbdInteractiveAuthentication no
GatewayPorts no
PermitUserEnvironment no
Programy cgc-keys i cgc-shell wymagają innego zestawu uprawnień.
cgc-keys wymaga jedynie dostępu do bazy danych, więc jest uruchamiany jako
użytkownik systemowy o właśnie takich uprawnieniach (opcja
AuthorizedKeysCommandUser).
cgc-shell wymaga natomiast jedynie dostępu do powłoki kontenerów i jest
uruchamiany dopiero po pomyślnym uwierzytelnieniu i autoryzacji użytkownika, co
zmniejsza powierzchnię ataku na taki system.
W tym przykładzie cgc-shell zostanie uruchomiony jako użytkownik systemowy
cgc.
A propos uprawnień do powłoki kontenerów, uprawnienia użytkownika Kubernetes,
którego używa kubectl powinny być minimalne.
W tym przypadku wymagane są jedynie poniższe uprawnienia.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: exec-all
rules:
- {apiGroups: ['apps'], resources: ['deployments'], verbs: ['get']}
- {apiGroups: ['batch'], resources: ['jobs'], verbs: ['get']}
- {apiGroups: [''], resources: ['pods'], verbs: ['list']}
- {apiGroups: [''], resources: ['pods/exec'], verbs: ['create']}
Dodatkowym zabezpieczeniem może być uruchomienie sshd na oddzielnym serwerze,
maszynie wirtualnej, kontenerze.
Może być to dobrym sposobem na izolację tej usługi i dalsze zmniejszenie
powierzchni ataku.
CGC-SSH w praktyce
Pokazaliśmy jak wykorzystać opcję AuthorizedKeysCommand
i kilka innych
mechanizmów serwera OpenSSH do stworzenia rozwiązania, które umożliwia
użytkownikom połączenie się do powłok kontenerów Kubernetes przez klienta SSH.
Po wprowadzeniu tej funkcji do CGC jeden z naszych najbardziej wymagających
klientów napisał:
[…] uważamy, że narzędzia cgc przez ostatnie parę miesięcy wzbogaciły się o
bardzo dużo świetnej z naszego punktu widzenia funkcjonalności […]
Co tu dużo mówić - kawał dobrej roboty po Państwa stronie :) Dziękujemy.
Takie wiadomości powodują, że my — inżynierowie — mamy zapał do pracy i do
dalszego rozwijania produktów.
Podziękowania
Dziękujemy Drew DeVault, założycielowi platformy sourcehut za publikację
artykułu What happens when you push to git.sr.ht, and why was it so slow?,
który stanowił dla nas inspirację i źródło wiedzy na temat niestandardowych
aplikacji serwera OpenSSH.
Copyright 2024 Comtegra S.A.