Docker per użytkownik — jak oddzielić kontenery między wieloma kontami użytkowników?

Kategoria: Oprogramowanie Data publikacji:

Z kontenerów opartych na usłudze Docker korzystam na co dzień — w zastosowaniach prywatnych, jak również w ramach mojej pracy zawodowej, którą wykonuję zdalnie na własnym komputerze. Bardzo lubię wyraźny podział między kwestiami osobistymi i zawodowymi, dlatego w swoim systemie operacyjnym posiadam dwie partycje dyskowe i dwa konta użytkowników — oddzielne do pracy i do prywatnego użytku. Niestety architektura Dockera utrudnia użytkownikowi tego typu separację.

Na czym polega problem?

Program Docker, używany natywnie w systemie z rodziny Linux, uruchamia się system-wide, tj. dla całego systemu operacyjnego: pliki kontenerów zapisywane są poza katalogiem domowym, a wywołanie polecenia takiego jak docker ps -a wyświetla dane na temat wszystkich kontenerów w systemie, a nie tylko tych należących do bieżącego użytkownika.

Jak wspomniałem, mam dwa konta użytkowników, a katalog domowy każdego z nich znajduje się na oddzielnej partycji dyskowej. Zachowanie Dockera psuje higienę separacji między sprawami prywatnymi a służbowymi. Dodatkowo, przechowywanie danych i konfiguracji kontenerów na partycji systemowej sprawia, że przy ewentualnej reinstalacji systemu wszystkie te dane zostałyby utracone, jeśli zapomniałbym o tym szczególe.

Jednym z rozwiązań tego problemu jest rootless Docker — oficjalna inicjatywa umożliwiająca uruchomienie tej usługi per user. Nie podoba mi się ona ze względu na niepełną kompatybilność w porównaniu ze standardowym sposobem uruchamiania oraz nieprzejrzystą formę instalacji. Do kontenerów niewymagających usługi uruchamianej jako root zdecydowanie wolę program Podman firmy Red Hat.

Przygotowałem swoje alternatywne rozwiązanie, które nie posiada właściwie żadnych negatywnych skutków ubocznych (przynajmniej w moich zastosowaniach). Zmodyfikowałem konfigurację jednostek systemd tak, aby w moim systemie pracowały dwie oddzielne kopie usługi Docker — każda dla jednego z kont użytkowników w systemie. Za pomocą dowiązań symbolicznych przeniosłem też dane kontenerów na oddzielne partycje każdego z kont.

Moje rozwiązanie — krok po kroku

Instrukcję przedstawiam na przykładzie Fedory 35 i paczki moby-engine z repozytoriów systemowych, jednak jej implementacja w innych dystrybucjach Linuksa, jak również w przypadku paczek z upstreamu nie powinna różnić się za bardzo. W razie wątpliwości zachęcam do zostawienia komentarza pod artykułem.

Stworzymy całkowicie nową usługę typu template o nazwie docker@, którą zastąpimy domyślną usługę docker. Dzięki temu uruchamianie nowych kopii Dockera dla każdego z użytkowników będzie bardzo proste.

(Uwaga: zastosowanie niewielkich plików „drop-in” do podmiany jedynie wymaganych opcji nie jest możliwe w tym przypadku ze względu na konieczność nadpisania zależności (np. pola After), co nie jest wspierane w tym mechanizmie.)

Krok 1: przygotowanie jednostki systemd typu template

Skopiuj plik /usr/lib/systemd/system/docker.socket do /etc/systemd/system/docker@.socket. W nowo utworzonym pliku wprowadź zmiany według zaznaczenia poniżej.

[Unit]
Description=Docker Socket for the API

[Socket]
ListenStream=/run/docker.sock
SocketMode=0660
SocketUser=root
SocketGroup=docker
[Unit]
Description=Docker Socket (for user %i)

[Socket]
ListenStream=/run/docker-%i.sock
SocketMode=0660
SocketUser=root
SocketGroup=docker

Skopiuj plik /usr/lib/systemd/system/docker.service do /etc/systemd/system/docker@.service. W nowym pliku wprowadź zmiany, jak zaznaczyłem poniżej.

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=docker.socket network-online.target firewalld.service
Requires=docker.socket
Wants=network-online.target

[Service]
Type=notify
EnvironmentFile=-/etc/sysconfig/docker
ExecStart=/usr/bin/dockerd \
         --host=fd:// \
         --exec-opt native.cgroupdriver=systemd \
         $OPTIONS
ExecReload=/bin/kill -s HUP $MAINPID

...
[Unit]
Description=Docker (for user %i)
Documentation=https://docs.docker.com
After=docker@%i.socket network-online.target firewalld.service
Requires=docker@%i.socket
Wants=network-online.target

[Service]
Type=notify
EnvironmentFile=-/etc/sysconfig/docker
ExecStart=/usr/bin/dockerd \
         --host=fd:// \
         --exec-opt native.cgroupdriver=systemd \
         --pidfile /var/run/docker-%i.pid \
         --data-root /var/lib/docker-%i \
         --exec-root /var/run/docker-%i \
         $OPTIONS
ExecReload=/bin/kill -s HUP $MAINPID

...

Krok 2: przygotowanie folderów dla każdej instancji usługi Docker

Stwórz foldery według schematu /var/lib/docker-NAZWA_UŻYTKOWNIKA. Zadbaj o uprawnienie właściciela i grupy root:root na tworzonych folderach. W moim przypadku nazwy użytkowników to tomaszwork-tomasz.

sudo mkdir /var/lib/docker-tomasz
sudo mkdir /var/lib/docker-work-tomasz

Z użyciem poniższych poleceń stosuję alternatywne rozwiązanie: tworzę dowiązania symboliczne do miejsca w katalogu domowym każdego z kont użytkowników w moim systemie. W ten sposób osiągam ładną separację warstw abstrakcji: jednostka systemd nie musi „wiedzieć” o tym, że tak naprawdę pliki lądują w katalogach domowych użytkowników.

sudo mkdir /home/tomasz/.local/share/docker
sudo mkdir /home/work-tomasz/.local/share/docker

sudo ln -s /home/tomasz/.local/share/docker /var/lib/docker-tomasz
sudo ln -s /home/work-tomasz/.local/share/docker /var/lib/docker-work-tomasz

W przypadku dystrybucji Linuksa takiej jak Fedora należy zadbać także o konfigurację SELinuksa.

sudo semanage fcontext -a -e /var/lib/docker /var/lib/docker-tomasz
sudo semanage fcontext -a -e /var/run/docker /var/run/docker-tomasz
sudo restorecon -R /var/{lib,run}/docker-tomasz

sudo semanage fcontext -a -e /var/lib/docker /var/lib/docker-work-tomasz
sudo semanage fcontext -a -e /var/run/docker /var/run/docker-work-tomasz
sudo restorecon -R /var/{lib,run}/docker-work-tomasz

W przypadku wyboru podejścia opartego o zapis danych w katalogach domowych podobną zmianę trzeba wprowadzić również dla nich.

sudo semanage fcontext -a -e /var/lib/docker /home/tomasz/.local/share/docker
sudo semanage fcontext -a -e /var/lib/docker /home/work-tomasz/.local/share/docker
sudo restorecon -R /home/{tomasz,work-tomasz}/.local/share/docker

Krok 3: wyłączenie domyślnych i włączenie własnych jednostek systemd

Wyłącz domyślną usługę Docker, aby mieć pewność, że nie pracuje i nie koliduje ze stworzonymi samodzielnie instancjami.

sudo systemctl disable --now docker.{socket,service}
sudo systemctl mask docker.{socket,service}

Następnie włącz nowo utworzone usługi — oczywiście tutaj również wprowadź swoje własne nazwy kont użytkowników. :)

sudo systemctl enable --now docker@tomasz.socket
sudo systemctl enable --now docker@work-tomasz.socket

Krok 4: konfiguracja połączenia w środowisku

Ze względu na istnienie dwóch usług Docker, trzeba dokonać sprecyzowania dla aplikacji klienckich (polecenia docker czy docker-compose), z którą z nich mają się połączyć.

Dodaj do .bashrc albo .profile poniższą definicję zmiennej środowiskowej.

export DOCKER_HOST=unix:///var/run/docker-$USER.sock

Jeżeli korzystasz z poleceń CLI Dockera bez używania sudo, tj. twoje konto użytkownika jest dodane do grupy docker, następująca zmiana nie jest wymagana. Natomiast zdecydowanie nie jest to właściwa konfiguracja pod względem bezpieczeństwa (pomimo, że powszechnie stosowana i polecana). Z perspektywy zabezpieczeń systemu operacyjnego używanie Dockera bez sudo jest otwieraniem poważnej luki bezpieczeństwa.

sudo domyślnie nie przekazuje wszystkich zmiennych środowiskowych uruchamianemu oprogramowaniu. Stwórz plik w folderze /etc/sudoers.d o nazwie takiej jak na przykład 90-docker-host-env i zdefiniuj w nim wyjątek dla zmiennej DOCKER_HOST.

Defaults env_keep += "DOCKER_HOST"

Dodaj komentarz