Андрей Копылов, наш технический директор, любит, активно использует и пропагандирует Docker. В новой статье он рассказывает, как создать пользователей в Docker. Правильная работа с ними, почему пользователей нельзя оставлять с root правами и, как решить задачу несовпадения идентификаторов в Dockerfile.


Все процессы в контейнере будут работать из-под пользователя root, если специальным образом его не указать. Это кажется очень удобно, ведь у этого пользователя нет никаких ограничений. Именно поэтому работать под рутом неправильно с точки зрения безопасности. Если на локальном компьютере никто в здравом уме не работает с рутовыми правами, то многие запускают процессы под рутом в контейнерах.


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


Создание пользователя


Создание пользователя в контейнере не отличается от его создания в линуксовых дистрибутивах. Однако для разных базовых образов команды могут различаться.


Для дистрибутивов основанных на debian в Dockerfile необходимо добавить:



RUN groupadd --gid 2000 node   && useradd --uid 2000 --gid node --shell /bin/bash --create-home node

Для alpine:


RUN addgroup -g 2000 node     && adduser -u 2000 -G node -s /bin/sh -D node

Запуск процессов от пользователя


Для запуска всех последующих процессов от пользователя с UID 2000 выполните:


USER 2000

Для запуска всех последующих процессов от пользователя node выполните:


USER node

Подробнее в документации.


Монтирование томов


При монтировании томов внутрь контейнера обеспечьте пользователю возможность читать и (или) писать файлы. Для этого UID (GID) пользователя в контейнере и пользователя за пределами контейнера, у которого есть соответствующие права на доступ к файлу, должны соответствовать. При этом имена пользователей значения не имеют.


Часто на линуксовом компьютере у пользователя UID и GID равны 1000. Эти идентификаторы присваиваются первому пользователю компьютера.


Узнать свои идентификаторы просто:


id

Вы получите исчерпывающую информацию о своем пользователе.
Замените 2000 из примеров на свой идентификатор и все будет в порядке.


Присвоение пользователю UID и GID


Если пользователь создан ранее, но необходимо изменить идентификаторы, то можно сделать это так:


RUN usermod -u 1000 node   && groupmod -g 1000 node

Если вы используете базовый образ alpine, то нужно установить пакет shadow:


RUN apk add —no-cache shadow

Передача идентификатора пользователя внутрь контейнера при построении образа


Если ваш идентификатор и идентификаторы всех людей, которые работают над проектом, совпадают, то достаточно указать этот идентификатор в Dockerfile. Однако часто идентификаторы пользователей не совпадают.


Как осуществить желаемое не сразу понятно. Для меня это было самым сложным в процессе освоения docker. Многие пользователи docker не задумываются о том, что есть разные этапы жизни образа. Сначала образ собирается для этого используют Dockerfile. При запуске контейнера из образа Dockerfile уже не используется.


Создание пользователей должно происходить при построении образа. Это же касается и определения пользователя, из-под которого запускаются процессы. Значит, что мы каким-то образом должны передать внутрь контейнера UID (GID).


Для использования внешних переменных в Dockerfile служат директивы ENV и ARG. Подробное сравнение директив тут.


Dockerfile


ARG UID=1000
ARG GID=1000
ENV UID=${UID}
ENV GID=${GID}
RUN usermod -u $UID node   && groupmod -g $GID node

Передать аргументы через docker-compose можно так:


docker-compose


build:
  context: ./src/backend
  args:
    UID: 1000
    GID: 1000

P.S. Для освоения всех премудростей docker недостаточно читать документацию или статьи. Нужно много практиковаться, нужно почувствовать docker.

Комментарии (18)


  1. TonyLorencio
    19.04.2019 11:40
    +1

    Как гарантировать, что пользователь с UID=${UID} не имеет никаких прав на хостовой машине?


    1. aak74
      19.04.2019 11:47

      Присвоить ему UID пользователя, которого нет на хостовой машине. Например 10000. И GID такой же.


      1. TonyLorencio
        19.04.2019 13:32

        Это понятно.


        Допустим, на нашем сервере есть пользователь с UID 10000, у которого есть какие-то права, но я не знаю о факте существовании такого пользователя.


        Можно ли вообще гарантировать, что на любой машине, где будет использоваться этот образ, будет отсутствовать пользователь с некоторым UID/GID?


        1. aak74
          19.04.2019 13:47

          Можно ли вообще гарантировать, что на любой машине, где будет использоваться этот образ, будет отсутствовать пользователь с некоторым UID/GID?

          Для любой машины вряд ли. По крайней мере мне о таком способе неизвестно.


  1. VolCh
    19.04.2019 13:35

    Создание пользователей должно происходить при построении образа. Это же касается и определения пользователя, из-под которого запускаются процессы. Значит, что мы каким-то образом должны передать внутрь контейнера UID (GID).

    Неоднозначно как-то. На этапе построения образа контейнера нет (билд-контейнеры не рассматриваем), а частая задача: есть образ с каким-то пользователем, надо запустить контейнер, обеспечив связь с пользователем хоста.


    P.S. А вообще думал пост будет про https://docs.docker.com/engine/security/userns-remap/ и ко.


  1. MMik
    19.04.2019 23:37

    Можете дополнить статью информацией о применении gosu, и об обязательных переменных типа таких:

    version: "3"
    services:
    app:
    image: repo/app:v1
    user: "${UID:?Please export UID}:${GID:?Please export GID}"
    volumes:
    - "vol1:/data/vol1:rw"
    - "vol2:/data/vol2:ro"
    ports:
    - "8080:8080"


  1. 0xf0a00
    21.04.2019 17:19

    Хабровчане, может кто нибудь подскажет как реализовать автозапуск в контейнере?


    1. iDen
      21.04.2019 19:43

      автозапуск чего?

      • используешь ENTRYPOINT или CMD что бы запустить свою команду при запуске контейнера.
      • если надо что-то навороченное, тогда заворачиваешь все это в самописный script.sh и передаешь его в ENTRYPOINT или CMD
      • можно пойти дальше и использовать github.com/krallin/tini
      • или github.com/Yelp/dumb-init
      • или наверное еще 100500 других извращенческих методов


      1. 0xf0a00
        22.04.2019 10:42

        ENTRYPOINT это не автозапуск, я говорю об автозапуске в классическом его понимании. Автозапуск нужен для промежуточного образа в котором будет окружение для выполнения программ, и на основе его будет собираться контейнер каждый со своей программой. Так что ENTRYPOINT будет занят.


        1. VolCh
          22.04.2019 10:57

          Так нужен запуск какой-то программы в рантайме перед запуском основной?


        1. iDen
          22.04.2019 11:09

          не до конца понятно. но опять попробую угадать.
          Может речь идет о многоуровневых билдах multi-stage builds?
          Перед созданием конечного образа, запускается промежуточный контейнер в котором запускается какаято магия, подготавливающая чтото для конечного образа.


          1. 0xf0a00
            22.04.2019 12:11

            Все намного проще, нужен образ с окружением Wine для запуска windows приложений. Что бы для каждого сервиса не собирать свой wine проще его собрать в одном образе, а потом на его основе делать контейнеры подкидывая нужное ПО через copy. Автозапуск нужен для фейковых иксов.
            VolCh, это ответ и на вас вопрос тоже.


            1. aak74
              22.04.2019 12:51

              1. Соберите базовый образ.
              2. Протэгайте его.
              3. Собирайте новые образы из этого базового образа.

              Протэганный образ можно либо залить на docker registry, а можно и не заливать.


              1. 0xf0a00
                22.04.2019 13:44

                А еще можно почитать ветку сначала и понять что нужен автозапуск иксов в промежуточном образе т.к. в конечном Entrypoint будет занят полезным сервисом.


                1. VolCh
                  22.04.2019 17:16

                  Фраза "запуск в образе" не имеет смысла, запуск всегда в контейнере.


                  А в целом, делаете ENTRYPOINT типа entrypoint.sh


                  xserver -d // или как там иксы запускаются, лет 10 не трогал
                  program

                  а в дочернем образе в докерфайле делайте что-то вроде


                  RUN ln -s /path/to/program program


                  ну или другую сотню вариаций на эту тему


                  1. 0xf0a00
                    22.04.2019 20:03

                    Я конечно может и не прав, но RUN и CMD выполняются 1(!) раз при создании контейнера. А ENTRYPOINT затирается если в родительском образе он был. Запуск должен быть прописан внутри самого контейнера, но там все настолько кастрированно, что моих знаний линукс не хватает что бы его реализовать.


                    1. VolCh
                      23.04.2019 08:50

                      Запуск и иксов, и основной программы (отсутствующей, ну или true) в ENTRYPOINT-скрипте родительского образа. В дочерних же через RUN создаётся симлинк на основную программу.

                      Смысл идеи в терминах ООП — в базовом образе создаём абстрактный (или с бессмысленной бесполезной реализацией) «метод» в «классе» ентрипоинт или кмд, наряду с нужной инициализацией, а в потомках этот «метод» переопределяем.


                      1. 0xf0a00
                        23.04.2019 09:39

                        Спасибо за совет, возможно это сработает, попробую. Просто очень хотелось отделить вайн от работающего сервиса максимально «красиво». :)