Подмания: правило вычисления UID хоста для пользователя в контейнере

от автора

Особенность отображения UID/GID пользователя в rootless podman контейнере на пространство имён хоста

Привет, Хабр!

Давеча я приметил в Интернете, что коллеги отображают UID/GID своих сервисов в rootless podman контейнерах, начиная с круглого числа, например --uidmap=0:1000000:65536 для первого, --uidmap=0:2000000:65536 для другого, --uidmap=0:3000000:65536 для третьего и т.д. В общем-то, в этом нет ничего предосудительного, ведь задача таким образом решается без пересечения пространства имён. Но и причина, по которой так делают, тоже понятна — перестраховаться. Давайте разберёмся, как отображать UID-в-UID в rootless podman:


Пусть по условию задачи сервис в контейнере не запустится, если смонтированная в него хостовая структура папок принадлежит не тем хостовым UID/GID, на которые отображаются контейнерные UID/GID. Для примера возьмём сервис DNS прокси unbound из этого проекта.

В данном образе процесс unbound внутри контейнера принадлежит UID 9898. Допустим, файлы /etc/subuid и /etc/subgid предоставляют нашему пользователю 512 блоков по 65536 идентификаторов:

myuser:100000:65536myuser:165536:33488896

Это равносильно такой записи:

myuser:100000:33554432

Но мы разбиваем на 2 диапазона для того, чтобы далее увидеть, заступаем мы на следующий диапазон при запуске контейнера или нет.

Для одного и того же имени пользователя (myuser) podman использует все доступные диапазоны, как один.

Структура файлов /etc/sub{u,g}id такова: имя:индекс:количество. Индекс есть первый элемент из количества (запрошенных идентификаторов). Смешать диапазоны, к счастью, не получится — newuidmap выдаст invalid internal status при запуске контейнера. Оставлять бреши в промежутках можно, так как podman их всё равно сольёт в один. Podman кеширует эти значения, но обновить кеш после изменения в этих файлах можно командой podman system migrate.

Допустим, вы создали в домашней папке структуру папок для нашего примера:

mkdir -p unbound3/{etc,log,dns} && cp /usr/share/dns/* unbound3/dns && cp podmaniac/unbound/docker_files/* unbound3/etc

Вопрос: какому хостовому UID/GID должны принадлежать файлы и папки (за исключением root.hints, который обязательно принадлежит хостовому 0:0), чтобы наш контейнер запустился?

Допустим, мы создаём и запускаем контейнер такой командой (проброс портов опущен для краткости):

 podman run \ --rm -it \ --cap-drop=ALL \ --cap-add=NET_BIND_SERVICE \ --cap-add=SETUID \ --cap-add=SETGID \ --security-opt=no-new-privileges \ --log-driver=journald \ --name=unbound3 \ -v $HOME/unbound3/etc:/usr/local/etc/unbound:rw \ -v $HOME/unbound3/dns:/opt/unbound/dns:rw \ -v $HOME/unbound3/log:/opt/unbound/log:rw \ --tmpfs /tmp:rw,noexec,nosuid,size=64m \ --tmpfs /run:rw,noexec,nosuid,size=16m \ --tmpfs /var/run,rw,noexec,nosuid,size=16m \ localhost/unbound3

Это пространство имён по умолчанию. В нём root контейнера (uid 0) отображается на идентификатор хостового пользователя системы (у меня это 1000). Остальные же, начиная с uid 1, отображаются на все доступные идентификаторы в /etc/sub{u,g}id из всех диапазонов и становятся доступными пространству имён данного контейнера.

Посмотрим актуальное отображение командой:

$ cat /proc/$(podman inspect -f '{{.State.Pid}}' unbound3)/uid_map         0       1000          1         1     100000      65536     65537     165536   33488896

Видим 3 отображения:

  • начиная с root (0) контейнера включительно, отображаем на хостовые, начиная с 1000 включительно последовательно 1 штуку

  • начиная с uid 1 контейнера включительно, отображаем на хостовые, начиная с 100000 включительно последовательно 65536 штук

  • начиная с uid 65537 контейнера включительно, отображаем на хостовые, начиная с 165536 включительно последовательно 33488896 штук

То есть пользователь в контейнере с UID 1 это пользователь с UID 100000 на хосте, UID 2 контейнера это UID 100001 хоста, UID 9898 контейнера это 109897 хоста. Поэтому ответ: файлы и папки должны принадлежать пользователю с UID 109897.

По умолчанию в контейнер отображаются все подчинённые (т.е. указанные в /etc/sub{u,g}id для данного пользователя) идентификаторы. Вы можете запустить несколько таких контейнеров, изменяя название контейнера (--name=unbound4,--name=unbound5, т.д.) и, если нужно, хостовые порты (-p 20053:53/tcp, -p 30053:53/tcp, т.д.), и для всех этих контейнеров хостовые UID/GID будут одинаковыми, т.е. пространство подчинённых имён по умолчанию одно на всех.

Допустим, теперь мы хотим выделять контейнеру лишь ограниченное количество идентификаторов в количестве 65536 штук. Для этого к вышеуказанной команде podman run добавьте аргументы:

    --uidmap=0:0:65536 \    --gidmap=0:0:65536 \

структура значения данных аргументов индекс_в_контейнере:индекс_промежуточный:количество. UID это индекс. С данными аргументами вывод команды podman inspect unbound3 | jq '.[0].HostConfig.IDMappings' больше не null, и отображение больше не прямое, а через промежуточные идентификаторы. По индексу 0 промежуточных идентификаторов всегда находится хостовый UID пользователя myuser (т.е. 1000). Т.е. промежуточный UID всегда лежит вне диапазона, который вы задаёте аргументами uidmap/gidmap, а именно: на один индекс младше. Если в файлах /etc/sub{u,g}id для пользователя myuser мы начинаем с 100000, как в нашем случае, то это означает отображение хостового UID 100000 на промежуточный UID 1, и далее уже по порядку.

Отображение:

$ cat /proc/$(podman inspect -f '{{.State.Pid}}' unbound3)/uid_map         0       1000          1         1     100000      65535

Как видим, UID 1 это UID 100000 на хосте, UID 2 это UID 100001, UID 9898 это UID 109897 хоста. Поэтому ответ: аналогично 109897.

Допустим, мы хотим избежать использования промежуточного UID 0. Для этого поменяем значения аргументов:

    --uidmap=0:1:65536 \    --gidmap=0:1:65536 \

И это даст нам самое красивое отображение:

$ cat /proc/$(podman inspect -f '{{.State.Pid}}' unbound3)/uid_map         0     100000      65536

Мы не используем промежуточный UID 0. Теперь пользователь с UID 0 в контейнере это пользователь с UID 100000 на хосте, UID 1 контейнера это UID 100001 хоста, UID 9898 контейнера это 109898 хоста. Поэтому ответ: файлы и папки, кроме root.hints, должны принадлежать пользователю с UID 109898. Самое приятное здесь то, что мы не заступаем на диапазон из 65536 идентификаторов следующего контейнера.

Убедимся в этом, идя от противного: запустите первый контейнер с аргументами:

    --uidmap=0:2:65536 \    --gidmap=0:2:65536 \    --name=unbound3

а второй с аргументами:

    --uidmap=0:65537:65536 \    --gidmap=0:65537:65536 \    --name=unbound4 \

Порты между контейнерами, если они всё же пробрасываются, должны различаться.

Сравните их отображения:

$ cat /proc/$(podman inspect -f '{{.State.Pid}}' unbound3)/uid_map         0     100001      65535     65535     165536          1

и

$ cat /proc/$(podman inspect -f '{{.State.Pid}}' unbound4)/uid_map         0     165536      65536

Как видите, пользователь с UID 65535 в первом контейнере является рутом (0) во втором контейнере в рамках пространства имён хоста.

В завершение рассмотрим ещё один способ: аргумент --userns=keep-id:uid=9898,gid=9898 вместо аргументов uidmap и gidmap. С данным аргументом podman смещает взаимное отображение промежуточных и хостовых UID от 0 до заданного (9898) включительно влево, и тогда выпавшее значение хостового UID по индексу 0 промежуточного (а мы помним, что это UID хостового пользователя 1000) становится на место промежуточного с UID 9898. То есть диапазон в /etc/sub{u,g}id разрывается надвое заданным UID, а заданный UID отображается на UID вашего пользователя в системе. Поэтому в данном случае ответ: папки должны принадлежать хостовому пользователю, который выполняет команду podman run, т.е. 1000.

Вот так выглядит отображение в данном последнем случае:

$ cat /proc/$(podman inspect -f '{{.State.Pid}}' unbound3)/uid_map         0     100000       9898      9898       1000          1      9899     109898      55638     65537     165536   33554432

Как видите, он отображает все доступные в /etc/sub{u,g}id идентификаторы в контейнер, что делает невозможным избежать пересечения пространства имён между контейнерами.

На этом рассмотрение особенностей вычисления хостовых UID, соответствующих контейнерным, в rootless podman версии 4.3.1, закончен.

ссылка на оригинал статьи https://habr.com/ru/articles/1068092/