Пользователь в Docker

    Андрей Копылов, наш технический директор, любит, активно использует и пропагандирует 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

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

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

          Это понятно.


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


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

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

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

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


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

          0
          Можете дополнить статью информацией о применении 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"
            0
            Хабровчане, может кто нибудь подскажет как реализовать автозапуск в контейнере?
              +1
              автозапуск чего?
              • используешь ENTRYPOINT или CMD что бы запустить свою команду при запуске контейнера.
              • если надо что-то навороченное, тогда заворачиваешь все это в самописный script.sh и передаешь его в ENTRYPOINT или CMD
              • можно пойти дальше и использовать github.com/krallin/tini
              • или github.com/Yelp/dumb-init
              • или наверное еще 100500 других извращенческих методов
                0
                ENTRYPOINT это не автозапуск, я говорю об автозапуске в классическом его понимании. Автозапуск нужен для промежуточного образа в котором будет окружение для выполнения программ, и на основе его будет собираться контейнер каждый со своей программой. Так что ENTRYPOINT будет занят.
                  0
                  Так нужен запуск какой-то программы в рантайме перед запуском основной?
                    0
                    не до конца понятно. но опять попробую угадать.
                    Может речь идет о многоуровневых билдах multi-stage builds?
                    Перед созданием конечного образа, запускается промежуточный контейнер в котором запускается какаято магия, подготавливающая чтото для конечного образа.
                      0
                      Все намного проще, нужен образ с окружением Wine для запуска windows приложений. Что бы для каждого сервиса не собирать свой wine проще его собрать в одном образе, а потом на его основе делать контейнеры подкидывая нужное ПО через copy. Автозапуск нужен для фейковых иксов.
                      VolCh, это ответ и на вас вопрос тоже.
                        0
                        1. Соберите базовый образ.
                        2. Протэгайте его.
                        3. Собирайте новые образы из этого базового образа.

                        Протэганный образ можно либо залить на docker registry, а можно и не заливать.
                          0
                          А еще можно почитать ветку сначала и понять что нужен автозапуск иксов в промежуточном образе т.к. в конечном Entrypoint будет занят полезным сервисом.
                            0

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


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


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

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


                            RUN ln -s /path/to/program program


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

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

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

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое