Иногда при работе возникает ситуация с неверными правами к файлам, созданными докер контейнерами. Например, логи сервера вдруг могут создастся с привилегиями root. В статье расскажу почему это происходит и как запустить контейнер из под текущего пользователя, чтобы избежать этого
Это статья является вольным переводом Juan Treminio
Running Docker Containers as Current Host User, автора https://puphpet.com/ — генератора настроек для vagrant.
Начало
Так почему же, файлы создаются с правами рута или другого пользователя. Начнем эксперимент, создайте тестовую директорию и в ней попробуем запустить команду
1 2 3 4 5 6 |
docker container run --rm \ -v ${PWD}:/var/www \ -w /var/www \ jtreminio/php:7.2 composer require psr/log |
Композер загрузить библиотеку и создаст файлы в текущей директории
1 2 3 4 5 6 7 8 9 |
total 24 drwxrwxr-x 4 zhandauletov zhandauletov 4096 окт 2 17:37 . drwxrwxr-x 14 zhandauletov zhandauletov 4096 окт 1 11:13 .. -rw-r--r-- 1 root root 53 окт 1 09:21 composer.json -rw-r--r-- 1 root root 2073 окт 1 09:21 composer.lock drwxrwxr-x 2 zhandauletov zhandauletov 4096 окт 2 17:37 .idea drwxr-xr-x 4 root root 4096 окт 1 09:21 vendor |
Видно, что файлы созданные контейнером принадлежат пользователю root, потому что демон Docker-а запускается root пользователем. В этом можно убедиться введя команду
1 2 3 4 |
$ ps -fe | grep dockerd root 1652 11.2 0.3 3863124 71764 ? Ssl сен24 1309:26 /usr/bin/dockerd |
Хорошо, контейнер запущен из root, так может просто запустить контейнер из под текущего пользователя? В Docker есть даже специальный ключ для этого
1 2 3 4 5 6 7 |
docker container run --rm \ -v ${PWD}:/var/www \ -w /var/www \ -u $(id -u ${USER}):$(id -g ${USER}) \ jtreminio/php:7.2 composer require psr/log |
1 2 3 4 5 6 7 8 9 |
total 24 drwxrwxr-x 4 zhandauletov zhandauletov 4096 окт 3 17:54 . drwxrwxr-x 14 zhandauletov zhandauletov 4096 окт 1 11:13 .. -rw-r--r-- 1 zhandauletov zhandauletov 53 окт 3 17:53 composer.json -rw-r--r-- 1 zhandauletov zhandauletov 2073 окт 3 17:54 composer.lock drwxrwxr-x 2 zhandauletov zhandauletov 4096 окт 3 09:26 .idea drwxr-xr-x 4 zhandauletov zhandauletov 4096 окт 3 17:54 vendor |
Задача выполнена, верно?
Верно… отчасти
Для большинства ситуации подойдет такое решение. Например, если вам нужно просто скачать библиотеку через composer или запустить контейнер с коротким циклом жизни это может прокатить. Но давайте копнем глубже
Что если в контейнере запущен процесс PHP-FPM у которого должны быть права на создание процесс файла в /var/run/php-fpm.pid или запись сессий в папку /var/lib/php/sessions? Ясно дело что этот процесс не получит доступа к ним, ведь контейнер запущен с ИД текущего пользователя
1 2 3 4 5 6 7 8 9 |
$ docker container run --rm \ -v ${PWD}:/var/www \ -w /var/www \ -u $(id -u ${USER}):$(id -g ${USER}) \ jtreminio/php:7.2 \ bash -c "echo \$(id -u \${USER}):\$(id -g \${USER})" 1001:1001 |
1 2 3 4 5 6 7 8 |
$ docker container run --rm \ -v ${PWD}:/var/www \ -w /var/www \ -u $(id -u ${USER}):$(id -g ${USER}) \ jtreminio/php:7.2 touch /var/lib/php/sessions/foo touch: cannot touch '/var/lib/php/sessions/foo': Permission denied |
Может создать контейнер из под пользователя www-data?
Хм.. доступ к закрытым директориям есть, но при попытке создать файлы на хосте выкинет ошибку
1 2 3 4 5 6 7 8 9 10 |
$ docker container run --rm \ -v ${PWD}:/var/www \ -w /var/www \ -u www-data \ jtreminio/php:7.2 composer require psr/log [ErrorException] file_put_contents(./composer.json): failed to open stream: Permission denied |
ведь пользователь 33:33 не может создать файлы в папке zhandauletov
Синхронизация файла /etc/passwd тоже не поможет, не в именах дело, а в User ID
Что за грабли такие с этими пользователями?
Процитирую из книги Using Docker — Использование Docker
Ядро Linux использует идентификаторы пользователей UID и идентификаторы групп GID для идентификации пользователей и определения их прав доступа. Преобразование числовых идентификаторов UID и GID в символьные идентификаторы выполняется операционной системой в пространстве пользователя. Поэтому идентификаторы пользователей UID в контейнере совпадают с аналогичными UID на хосте, но пользователи и группы, созданные внутри контейнера, не передаются на хост. Из-за этого возникает побочный эффект – может возникать беспорядок в правах доступа, а одни и те же файлы могут принадлежать одному пользователю внутри контейнера и другому пользователю вне контейнера
То есть, могут быть пользователи zhandauletov и на хосте, и в контейнере. Но на хосте у этого пользователи UID будет 1001, в контейнере 1002 и это разные пользователи.
Пространства имен для DOCkER
Более подробно это описано в статье https://www.jujens.eu/posts/en/2017/Jul/02/docker-userns-remap/
Суть в том что пространство zhandauletov на хосте маппится с пространством root в контейнере.
То есть ИД 1001:1001 zhandauletov будет равен ИД 0:0 root и все созданные файлы 0:0 рута в контейнере создаются на хосте с правами 1001:1001
Вроде бы все прекрасно, но не забывайте, что в контейнере скорее всего будет пользователь отличный от рута. Например, www-data c 33:33 в контейнере будет создавать файлы с правами 1033:1033 на хосте, в ведь такого пользователя нет на хосте, это может создать потенциальные ошибки с доступом.
Итак, что же на самом деле работает?
Единственным решением является заново создать нужного пользователя в контейнере с нужным UID
1 2 3 4 5 6 |
FROM jtreminio/php:7.2 RUN userdel -f www-data &&\ if getent group www-data ; then groupdel www-data; fi |
Это команда сначала проверяет наличие пользователя перед удалением. Полная команда выглядит так
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
FROM jtreminio/php:7.2 ARG USER_ID=1001 ARG GROUP_ID=1001 RUN if getent passwd www-data; then userdel -f www-data; fi &&\ 1 if getent group www-data ; then groupdel www-data; fi &&\ 2 groupadd -g ${GROUP_ID} www-data &&\ 3 useradd -l -u ${USER_ID} -g www-data www-data &&\ 4 install -d -m 0755 -o www-data -g www-data /home/www-data 5 chown --changes --silent --no-dereference --recursive \ --from=33:33 ${USER_ID}:${GROUP_ID} \ /home/www-data \ /.composer \ /var/run/php-fpm \ /var/lib/php/sessions 6 USER www-data |
Для начала давайте удалим пользователя и его группу — 1. getent passwd, getent group получение информации о пользователе из базы данных passwd, получение группы из базы group соответственно
2 и 3 добавление новых пользователя и группы
4 создание домашней директории для него
5 — смена владельца, те папки которые принадлежали 33:33 теперь принадлежат 1001:1001
6 — все последующие команды запускаются из под пользователя 1001:1001
Теперь все работает, но хорошо бы еще избавиться от захардкоженных данных 1001
Динамическая передача ИД
В этом поможет ключ —build-arg при создании образа
1 2 3 4 5 6 |
$ docker image build \ --build-arg USER_ID=$(id -u ${USER}) \ --build-arg GROUP_ID=$(id -g ${USER}) \ -t php_test \ |
А докерфайле нужно убрать весь харкод
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
FROM jtreminio/php:7.2 ARG USER_ID ARG GROUP_ID USER root RUN if getent passwd www-data; then \ userdel -f www-data &&\ if getent group www-data ; then groupdel www-data; fi &&\ groupadd -g ${GROUP_ID} www-data &&\ useradd -l -u ${USER_ID} -g www-data www-data &&\ install -d -m 0755 -o www-data -g www-data /home/www-data &&\ chown --changes --silent --no-dereference --recursive \ --from=33:33 ${USER_ID}:${GROUP_ID} \ /home/www-data \ /.composer \ /var/run/php-fpm \ /var/lib/php/sessions \ ;fi USER www-data |
Протестируем, запустим контейнер и проверим созданные им файлы
1 2 3 4 5 6 |
$ docker container run --rm \ -v ${PWD}:/var/www \ -w /var/www \ php_test:latest composer require psr/log |
1 2 3 4 5 6 7 8 9 10 |
$ ls -lan total 24K drwxrwxr-x. 3 1000 1000 4.0K Aug 4 22:50 ./ drwxr-xr-x. 7 1000 1000 4.0K Aug 4 19:31 ../ drwxr-xr-x. 4 1000 1000 4.0K Aug 4 22:50 vendor/ -rw-rw-r--. 1 1000 1000 545 Aug 4 22:48 Dockerfile -rw-r--r--. 1 1000 1000 53 Aug 4 22:50 composer.json -rw-r--r--. 1 1000 1000 2.1K Aug 4 22:50 composer.lock |
Тест прав доступа к защищенному каталогу
1 2 3 4 5 6 7 8 |
$ docker container run --rm \ -v ${PWD}:/var/www \ -w /var/www \ php_test:latest \ bash -c "touch /var/lib/php/sessions/foo && echo \$?" 0 |
А если я хочу использовать docker-compose?
Перенос в docker compose выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# docker-compose.yml version: '3.2' services: php: build: context: . dockerfile: Dockerfile args: - USER_ID - GROUP_ID volumes: - ${HOME}/.composer:/.composer - ${PWD}:/var/www ports: - 9000:9000 |
В этом случае берется значение из переменной окружения. Для Windows временную переменную можно поставить так, в Powershell
1 2 3 4 5 6 7 8 9 |
$Env:GROUP_ID += 1001 $Env:USER_ID += 1001 // посмотреть переменные окружения Get-ChildItem Env: // or ls env: |
Или можно явно указать в файле .env переменные окружения. Файл .env должен быть на одном уровне с docker-compose.yml
1 2 3 4 5 |
cat ./.env USER_ID=1001 GROUP_ID=1001 |
Здесь использована возможность докера работать с переменными окружения
https://docs.docker.com/compose/environment-variables/
А синтаксис переменных в docker-compose.yml описан здесь
https://docs.docker.com/compose/compose-file/#variable-substitution
Осталось лишь запустить
1 2 3 |
docker-compose up -d --build |
Вот, теперь добавление пользователей работает и через docker-compose
Конечно же, не все контейнеры требует таких манипуляции с пользователями. В большинстве случаев, например, если срок жизни контейнера маленький и/или не создает важных файлов (это может быть внутренний кэш), такого не надо.
А для долгоживущих контейнеров-сервисов такой принцип актуален.
Что ж, вот так вот создали переносимый код для безопасного запуска процессов в контейнере. И безопасность повысили, и разработчиков Docker уважили с их «никогда не запускайте контейнеры из под рута, хо-хо-хо» и от потенциальных проблем с правами доступа избавились.
· Permalink
А что мешает, в Dockerfile прописать
RUN usermod -u ${USER_ID} www-data && groupmod -g ${GROUP_ID} www-data
Зачем удалять, и по новой создавать пользователя www-data? Сменить ему при сборке права, и всё
· Permalink
Спасибо Алексей за подсказку. Сегодня вечером попробую протестировать
· Permalink
Как это все проделать для alpine? Аналоги всех команд нашел, но
chown: unrecognized option: from=33:33 (хотя наверно у меня 82:82, потому что это текущие ID для www-data)