Podman, Buildah, Skopeo - same dobre rzeczy

Ten post został napisany ponad 2 lata temu, do wszystkich porad technologicznych w nim zawartych lepiej będzie podejść z dużą rezerwą, bo bardzo możliwe że tego rodzaju informacje są już nieaktualne.

Opublikowano: 05.12.2020

Ostatnia modyfikacja: 07.02.2024

kontenery

podman

programowanie

Mija tydzień odkąd zacząłem używać Podmana i Buildah na poważnie. Podoba mi się coraz więcej rzeczy, choć dostrzegam również pewne braki. To będzie kolejny artykuł, który będzie WIP, bo zamierzam zebrać te wszystkie fajne rzeczy, które Podman i Buildah wniosły w moje programistyczne życie. Po kolei, każda z omówieniem.

Armenia

Podman, rootless

Dla mnie to jest właśnie najważniejsze - Podman nie wymaga uprawnień administracyjnych dla użytkownika, który będzie uruchamiał kontener. Wynika to z odmiennego modelu wykonywania (typowy fork procesu, a nie klient-serwer jak w przypadku Dockera) oraz czujnego użycia przestrzeni nazw użytkownika, co w sumie daje coś, co rozwiązuje bardzo wiele problemów, na jakie natyka się każdy, kto uruchamia kod w kontenerach.

W przypadku Dockera źródłem problemu jest containerd, czyli proces serwera, który jest uruchomiony na roocie, a klient, który komunikuje się z serwerem przez plik gniazda - również musi być uruchamiany przez roota (grupa docker efektywnie daje wszystkie uprawnienia administracyjne). Żaden problem na laptopie developera, ale na produkcji?

Proponuję mały teścik. Proszę sobie uruchomić kontener z Busyboxem:

$ podman run --rm -ti -v .:/output docker.io/library/busybox
# id
uid=0(root) gid=0(root) groups=10(wheel)
# echo "test" > /output/test.txt

Jak widać, w kontenerze jestem rootem. A kto jest właścicielem nowo utworzonego pliku na hoście?

$ ls -la test.txt
-rw-r--r-- 1 jazg jazg 5 gru  5 19:59 test.txt

Tadam! Czy nie o tym właśnie marzył każdy, komu kod odpalony pod Dockerem wyprodukował pliki, których właścicielem był root (na przykład niedostępne artefakty testów pod Jenkinsem)? Oto właśnie problem znika w chmurce dymu - to co się dzieje w kontenerze pozostaje w kontenerze, a na zewnątrz właścicielem procesu jestem ja i ostatecznie to ja jestem właścicielem wszystkiego u siebie. Tak powinno być w każdym domu.

Zaraz, upewnijmy się jak to wygląda w kontenerze.

$ podman run --rm -ti -v .:/output docker.io/library/busybox
# ls -la /output/test.txt
-rw-r--r--    1 root     root             5 Dec  5 18:59 /output/test.txt

Wygląda dokładnie tak, jak powinno wyglądać, właśnie o to chodziło w tych całych kontenerach od samego początku, żeby nam nie przeciekało między środowiskami, ale dopiero teraz naprawdę nie przecieka, a nie zwłaszcza, że prawie w ogóle nie padało.

Choćby z tego tylko powodu Podman wart jest zainteresowania, a to przecież tylko wisienka na czubku tortu rootless. Tym, co niezauważalne, a wyjątkowo ważne - jest bezpieczeństwo. Nawet w trybie privileged kod w kontenerze nie będzie mógł zrobić więcej, niż właściciel procesu uruchamiającego kontener (choć cały czas dużo, a nawet zazwyczaj za dużo). Istnieją powody aby uruchomić kontener w trybie privileged, ale przez lata używania kontenerów tylko raz musiałem to zrobić, a przypadek był bardzo szczególny (aplikacja uruchomiona na izolowanym urządzeniu wymagała pełnego dostępu do podsystemu USB na poziomie sprzętowym). Internet jest pełen mrożących krew w żyłach opowieści o ucieczkach z kontenera. Dlatego uruchamianie kontenerów w trybie rootless jest tak ważnym dodatkiem, i choć nie widać go z punktu siedzenia developera aplikacji, to ops napewno to doceni.

Czy to znaczy, że można przestać kombinować z uruchamianiem procesu z konta zwykłego użytkownika wewnątrz kontenera? Jak sytuacja wygląda w takim przypadku? Może to nie jest już dłużej potrzebne?

Jeżeli uruchomimy w trybie rootless kontener w którym proces jest uruchomiony z roota, efekt będzie taki, jak powyżej, proces będzie rootem w kontenerze, ale zwykłym użytkownikiem na hoście. A jak to zrobić, żeby proces był mną?

$ id
uid=1000(jazg) gid=1000(jazg)
$ podman run -ti --rm --userns keep-id --user $(id -u):$(id -g) docker.io/library/busybox
$ id
uid=1000(jazg) gid=1000(jazg)

Ma to pewne ograniczenia, na przykład system plików w kontenerze jest efektywnie tylko do odczytu, więc jedynym sposobem by zapisać jakiś artefakt jest zamontować z hosta wolumin. Zostanie on w kontenerze zamontowany z pełnymi uprawnieniami użytkownika uruchamiającego proces.

$ podman run -ti --rm --userns keep-id --user $(id -u):$(id -g) -v .:/opt/app docker.io/library/busybox
$ echo "test" > /opt/app/t1.txt
$ cat /opt/app/t1.txt
test
$ ls -la /opt/app/t1.txt
-rw-r--r--    1 jazg     jazg             5 Dec  6 20:06 /opt/app/t1.txt

A co się stanie w przypadku, gdy proces w kontenerze jest uruchomiony przez zwykłego użytkownika? Przygotowałem sobie taki obraz:

#! /bin/bash

set -euo pipefail

cnt=$(buildah from "docker.io/library/busybox")

buildah run ${cnt} addgroup -S app
buildah run ${cnt} adduser -S -H -D -G app app

buildah run ${cnt} mkdir -p "/opt/app/test"
buildah run ${cnt} chown -R app:app /opt/app

buildah config \
    --user app:app \
    --workingdir /opt/app \
    --volume "/opt/app/test" \
    ${cnt}

buildah commit --rm ${cnt} "quay.io/zgoda/test:1.0.0"

Efekt nie jest powalający.

$ podman run -ti --rm -v .:/opt/app/test quay.io/zgoda/test:1.0.0
$ ls -la
total 12
drwxr-xr-x    3 app      app           4096 Dec  6 19:36 .
drwxr-xr-x    3 root     root          4096 Dec  6 19:36 ..
drwxrwxr-x    3 root     root          4096 Dec  6 19:52 test
$ echo "test" > test/t1.txt
sh: can't create test/t1.txt: Permission denied

Ups, wolumin został zamontowany z rootem jako właścicielem. Proces w kontenerze może sobie pisać w swoim katalogu, ale do woluminu nie ma uprawnień. Niezbyt to przydatne. Czy coś można z tym zrobić? Podanie trybu rw przy montowaniu nic nie zmienia. No to teraz będzie ta skomplikowana część.

Spójrzmy najpierw kim jest nasz użytkownik w kontenerze.

$ podman run -ti --rm -v .:/opt/app/test quay.io/zgoda/test:1.0.0
$ id
uid=100(app) gid=101(app)

Na hoście mam uid=1000 i gid=1000 (widać to w jednym z wcześniejszych przykładów), a uid=100 jest przypisane do użytkownika systemd-network, więc odpada zmiana na żywca. Trzeba zmienić właściciela na na użytkownika z tymi uid i gid w mojej przestrzeni nazw. Do tego używa się unshare, a efekt będzie zgodny z tabelą mapowania w /proc/self/uid_map.

$ podman unshare cat /proc/self/uid_map
         0       1000          1
         1     100000      65536
$ podman unshare chown 100:101 ./v1
$ ls -la ./v1
razem 8
drwxrwxr-x 2 100099 100100 4096 gru  6 21:53 .
drwxrwxr-x 3 jazg   jazg   4096 gru  6 20:52 ..
$ podman run -ti --rm -v ./v1:/opt/app/test quay.io/zgoda/test:1.0.0
$ ls -la
total 12
drwxr-xr-x    3 app      app           4096 Dec  6 19:36 .
drwxr-xr-x    3 root     root          4096 Dec  6 19:36 ..
drwxrwxr-x    2 app      app           4096 Dec  6 20:53 test
$ echo "test" > test/t1.txt
$ ls -la test/t1.txt
-rw-r--r--    1 app      app              5 Dec  6 20:57 test/t1.txt

Tyle przynajmniej się udało, ale zaraz, kto jest właścicielem tego artefaktu?

$ ls -la v1/
razem 20
drwxrwxr-x 2 100099 100100 4096 gru  6 21:57 .
drwxrwxr-x 3 jazg   jazg   4096 gru  6 20:52 ..
-rw-r--r-- 1 100099 100100    5 gru  6 21:57 t1.txt
$ rm -rf v1/t1.txt
rm: nie można usunąć 'v1/t1.txt': Brak dostępu

Wracamy do punktu wyjścia, a właściwie do tego samego rozwiązania, które zastosowaliśmy wcześniej uruchamiając kontener rootfull.

$ podman run -ti --rm --userns keep-id --user $(id -u):$(id -g) -v ./v1:/opt/app/test quay.io/zgoda/test:1.0.0
$ id
uid=1000(jazg) gid=1000(jazg)
$ ls -la
total 12
drwxr-xr-x    3 app      app           4096 Dec  6 19:36 .
drwxr-xr-x    3 root     root          4096 Dec  6 19:36 ..
drwxrwxr-x    2 jazg     jazg          4096 Dec  6 21:02 test
$ echo "test" > test/t1.txt
$ ls -la test/t1.txt
-rw-r--r--    1 jazg     jazg             5 Dec  6 21:03 test/t1.txt

Sytuacja patowa, ale chyba już mogę sobie pozwolić na konkluzję. Tak naprawdę w przypadku uruchamiania kontenerów z konta zwykłego użytkownika nie ma żadnego znaczenia, czy proces wewnątrz kontenera działa na roocie czy na zwykłym użytkowniku. Żeby mieć w miarę naturalny dostęp do własnych zasobów w kontenerze wygodniej jest mieć proces z roota, ale przy uruchomieniu na zwykłym użytkowniku będzie to działać tak samo.

Rootless to fajna rzecz, ale wymaga nieco przemyślenia, nie da się tak zmapować Dockerfile jeden do jednego. Uruchamianie kontenerów w kontekście zwykłego użytkownika jest całkowicie nową sytuacją i trzeba się do niej dostosować. Jeszcze nie jestem pewien jak to zrobić, żeby obraz był w miarę bezpieczny zarówno podczas uruchomienia z roota (przez Dockera) jak i z normalnego użytkownika (Podman rootless), a jednocześnie żeby było dobrze, ale myślę nad tym intensywnie. Będzie jeszcze klika testów.