Контейнер работает за несколькими границами минимальных привилегий, лишние полномочия отфильтрованы
DevOps

Docker без root внутри контейнера: USER, capabilities и read-only

Запускаем приложение непривилегированным пользователем, убираем capabilities, запрещаем повышение прав и проверяем запись только в разрешённые каталоги.

Содержание

Пользователь root внутри контейнера не равен root на хосте, но обычному веб-приложению его полномочия всё равно не нужны. Пространства имён отделяют видимые процессы и сеть, seccomp запрещает часть системных вызовов, а capabilities делят полномочия root на отдельные разрешения. Ошибка в приложении опаснее, если процесс может менять системные файлы, управлять сетью или записывать в широкий каталог хоста.

Какие ограничения нужны обычному приложению

Создайте в образе отдельного пользователя с постоянными UID и GID — числовыми номерами пользователя и группы, по которым Linux проверяет права к файлам. Передайте ему только нужные файлы и задайте USER. При запуске запретите запись в корневую файловую систему, оставьте отдельный каталог для данных, уберите лишние capabilities и запретите повышение прав. Затем проверьте фактический UID и допустимые операции записи.

Не путайте пользователя приложения и режим Docker

Процесс приложения без root

Инструкция Dockerfile USER или параметр Compose user выбирает UID/GID основного процесса. Docker daemon при обычной установке всё ещё работает с полномочиями root на host.

Переназначение пользовательских номеров

UID контейнера сопоставляется с другим непривилегированным диапазоном на хосте. Служба Docker остаётся root, но номера владельцев процессов и файлов получают дополнительное преобразование.

Docker без root на хосте

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

В этой статье настраивается первый уровень: он переносим между обычным и rootless Docker и даёт понятный результат для каждого образа.

Создайте пользователя в Dockerfile

Создайте Dockerfile в каталоге приложения. Пример использует Alpine и добавляет группу и пользователя с номером 10001; затем копирует файлы с тем же владельцем и переключает основной процесс на него:

FROM node:24-alpine

RUN addgroup -S -g 10001 app \
 && adduser -S -D -H -u 10001 -G app app

WORKDIR /app
COPY --chown=10001:10001 package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=10001:10001 server.js ./

USER 10001:10001
EXPOSE 3000
CMD ["node", "server.js"]

Постоянный числовой UID упрощает согласование прав volumes между версиями образа. COPY --chown не оставляет код принадлежащим root, однако приложению вовсе не обязательно иметь право менять собственный код. В более строгом варианте код остаётся root-owned и доступен пользователю только на чтение, а запись разрешена отдельному каталогу данных.

Не выполняйте package manager во время старта контейнера. Установка зависимостей требует дополнительных прав и сети; её место в контролируемой сборке образа.

Проверьте пользователя образа

docker build -t example-app:nonroot .
docker image inspect example-app:nonroot \
  --format 'configured-user={{.Config.User}}'
docker run --rm example-app:nonroot id

Ожидается UID и GID 10001, а не 0. Затем запустите приложение на локальном порту:

docker run -d --name nonroot-demo \
  -p 127.0.0.1:3000:3000 \
  example-app:nonroot

docker exec nonroot-demo id
docker top nonroot-demo -eo pid,user,group,args

Публикуйте высокий внутренний порт, чтобы приложению не требовалась способность bind к привилегированному порту. Внешние 80/443 может принимать reverse proxy.

Ограничьте runtime через Compose

services:
  web:
    image: example-app:nonroot
    user: "10001:10001"
    read_only: true
    tmpfs:
      - /tmp:size=64m,mode=1777
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    ports:
      - "127.0.0.1:3000:3000"

user дублирует защитное ожидание runtime и не даёт случайно запустить процесс как root из-за изменения образа. read_only запрещает запись в root filesystem; /tmp предоставлен отдельно. Его размер учитывается в memory limit контейнера, поэтому не делайте tmpfs безразмерным.

cap_drop: ALL убирает Linux capabilities. Если приложение действительно требует одну из них, сначала подтвердите отказ на тесте, затем добавьте только конкретную capability через cap_add. Не возвращайте ALL и не используйте privileged: true.

no-new-privileges запрещает процессу получить дополнительные права через setuid/setgid binary и сходные механизмы. Это дополнительный барьер, а не замена исправным permissions и seccomp/AppArmor.

Подготовьте каталог данных

Для bind mount создайте точный каталог на host с ожидаемым владельцем:

sudo install -d -o 10001 -g 10001 -m 0750 /srv/example/data

Compose long syntax явно показывает границу записи:

services:
  web:
    volumes:
      - type: bind
        source: /srv/example/data
        target: /app/data

Не меняйте владельца всего /srv, /var/lib/docker или домашнего каталога. Если образ использует named volume, изучите его штатную процедуру начальной инициализации: некоторые образы кратко запускаются как root, готовят каталог и затем сбрасывают права. Самодельный entrypoint должен завершаться ошибкой, если подготовка не удалась, и передавать сигналы основному процессу через exec.

Проверьте запреты, а не только успешный запуск

docker compose config
docker compose up -d --force-recreate web
docker compose exec web id
docker compose exec web sh -c 'touch /app/should-fail'
docker compose exec web sh -c 'touch /tmp/should-work && rm /tmp/should-work'
docker compose exec web sh -c 'touch /app/data/should-work && rm /app/data/should-work'

Первая попытка записи должна завершиться ошибкой из-за read-only filesystem. /tmp и каталог данных должны принимать запись. Если приложение не содержит shell, выполните проверки отдельным тестовым target образа или через его встроенную диагностическую команду; не добавляйте shell в production только ради exec.

Посмотрите применённые настройки:

docker inspect "$(docker compose ps -q web)" --format \
  'user={{.Config.User}} readonly={{.HostConfig.ReadonlyRootfs}} caps_drop={{json .HostConfig.CapDrop}} security={{json .HostConfig.SecurityOpt}}'

Не выдавайте доступ к Docker socket

Mount /var/run/docker.sock позволяет клиенту управлять Docker API: запускать привилегированные контейнеры, подключать host paths и читать конфигурацию других экземпляров. Для обычного приложения это полномочия уровня управления узлом, даже если его процесс имеет UID 10001.

Если инструменту действительно требуется Docker API, вынесите его в отдельный доверенный компонент, ограничьте API-поверхность и оцените альтернативу без socket. Не подключайте socket «для удобства» сборки или просмотра соседних контейнеров.

Частые ошибки

  • USER app задан до установки пакетов, а следующие шаги сборки требуют root;
  • UID меняется между версиями и перестаёт совпадать с владельцем данных;
  • read_only включён без отдельного /tmp или каталога runtime-файлов;
  • приложение слушает порт ниже 1024 и ради этого получает лишние права;
  • chmod -R 777 скрывает неверного владельца;
  • privileged: true используется как средство диагностики;
  • rootless daemon ошибочно считают заменой USER внутри образа;
  • Docker socket подключён процессу, который принимает внешние запросы.

Как откатывать ограничения

Если приложение не запускается, сохраните docker compose logs, inspect и точный отказ доступа. Снимайте ограничения по одному на тестовом экземпляре: сначала предоставьте нужный writable path, затем проверьте владельца и только после этого оценивайте capability.

Верните предыдущий проверенный образ или Compose-файл и пересоздайте сервис. Не оставляйте временный user: root, privileged или широкий writable mount после диагностики.

Основы безопасного Dockerfile описаны в материале о сборке образа. Права постоянных каталогов разобраны в статье о volumes и bind mounts, а границы контейнерной изоляции — в объяснении модели Docker.

Источники

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

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

01Что произойдёт, если в образе не задан USER и runtime не переопределяет пользователя?
02Зачем использовать cap_drop: [ALL]?
03Какое изменение нельзя считать нормальным исправлением Permission denied?

Разбираем коротко

Частые вопросы

Достаточно ли инструкции USER для полной безопасности контейнера?

Нет. USER уменьшает права процесса, но остаются mounts, capabilities, kernel, network и доступные секреты. Нужны дополнительные ограничения и обновляемый доверенный образ.

Одинаковы ли non-root процесс и rootless Docker?

Нет. USER или параметр user меняет учётную запись процесса внутри контейнера. Rootless mode запускает без root ещё и Docker daemon в пользовательском namespace.

Почему нельзя монтировать docker.sock в обычное приложение?

Доступ к Docker API позволяет управлять контейнерами и mounts host и фактически даёт очень широкие полномочия на узле. Такой socket нельзя считать обычным файлом интеграции.