Права доступа Docker-контейнеров: как защитить хост и не сломать работу | AdminWiki

Права доступа Docker-контейнеров: как защитить хост и не сломать работу

28 августа 2026 9 мин. чтения
Содержание статьи

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

Сначала разберём модель прав. Затем пошагово настроим запуск без root, корректные bind mounts, доступ к GPU и USB, а также включим user namespaces. В конце даны команды диагностики Permission denied и инструменты аудита, чтобы проверять среду после настройки.

Модель прав доступа Docker: почему root в контейнере опасен для хоста

Docker изолирует процессы через Linux namespaces, cgroups и capabilities. Однако ядро оперирует числовыми идентификаторами UID и GID, и по умолчанию UID 0 в контейнере равен UID 0 на хосте. Если процесс вырвется из контейнера или получит доступ к неправильно смонтированному каталогу, последствия для хоста будут такими же, как от локального root.

UID и GID: как контейнер видит пользователей, а как - хост

Внутри контейнера файл /etc/passwd может показывать пользователя app с UID 1000. На хосте UID 1000 может принадлежать другому пользователю, например developer. Ядру не важны имена, важен только числовой идентификатор. Если смонтировать каталог с хоста и файл принадлежит UID 1000, процесс с UID 1000 в контейнере получит к нему доступ, даже если имя пользователя внутри отличается.

# в контейнере
id
uid=1000(developer) gid=1000(developer) groups=1000(developer)

# на хосте
ls -n /srv/app
-rw-r--r-- 1 1000 1000 123 Aug 28 10:00 config.yml

Проверка прав всегда выполняется по числам UID/GID. Поэтому при сборке образа и запуске контейнера стоит задавать предсказуемые идентификаторы.

Capabilities: какие привилегии остаются у root в контейнере

Контейнерный root не имеет полного набора привилегий хоста. Docker по умолчанию оставляет ограниченный, но опасный список capabilities: CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID, NET_BIND_SERVICE, SYS_CHROOT и другие. Благодаря DAC_OVERRIDE root внутри контейнера может читать файлы, принадлежащие другим UID, если они доступны через смонтированные тома. Поэтому запуск под root в контейнере нельзя считать безопасным.

Сброс всех capabilities через --cap-drop ALL уменьшает surface attack. Добавление точечных --cap-add оставляет только нужные сервису права.

Базовые риски root в контейнере: доступ к Docker socket, эскалация через уязвимости ядра, запись в системные каталоги образа. Если root не изолирован user namespace, он отображается в root хоста.

Запуск контейнеров от непривилегированного пользователя

Первый рубеж защиты: исключить root-процессы внутри контейнера. Для этого задают пользователя в Dockerfile или переопределяют при запуске.

Инструкция USER в Dockerfile: создание и переключение пользователя

Явное задание UID/GID в образе делает права на смонтированных томах предсказуемыми.

FROM alpine:3.21
RUN addgroup -g 1001 appgroup \
 && adduser -u 1001 -G appgroup -D appuser
WORKDIR /app
COPY --chown=appuser:appgroup . /app
USER appuser
CMD ["/app/service"]

Проверить пользователя можно командой:

docker run --rm myimage id
uid=1001(appuser) gid=1001(appgroup) groups=1001(appgroup)

Не указывайте UID от балды. Совпадение UID внутри контейнера и владельца bind mount на хосте критично для записи.

Флаг --user при docker run: быстрое переопределение пользователя

Если образ не пересобрать, а контейнер требует root, запустите его с --user 1000:1000.

docker run -u 1000:1000 --rm nginx id
uid=1000 gid=1000 groups=1000

Пользователь может отсутствовать в /etc/passwd контейнера, это не мешает ядру применять числовой UID. При монтировании каталога с хоста, владелец которого не совпадает с этим UID, возникнет Permission denied. Решение: сменить владельца на хосте или запустить контейнер с нужным UID.

Повышение привилегий в entrypoint: gosu и su-exec

Некоторым сервисам нужен root только на старте, чтобы исправить владельцев каталогов или применить конфигурацию. Использовать sudo в контейнере избыточно и небезопасно. Вместо этого применяют gosu или su-exec.

#!/bin/sh
# entrypoint.sh
if [ -n "$FIX_OWNERSHIP" ]; then
  chown -R appuser:appgroup /var/lib/app
fi
exec gosu appuser "$@"

Такой шаблон не оставляет родительского root-процесса. Основное приложение работает от appuser, а root существует только на время инициализации.

Права доступа к данным: bind mounts, volumes и ownership

Ошибка Permission denied чаще всего связана с неверным владельцем смонтированного каталога. Права на bind mount берутся с хоста. Права на named volumes при первом создании копируются из образа.

Bind mounts: как владелец каталога хоста влияет на доступ из контейнера

Если процесс в контейнере работает под UID 1000, а каталог /srv/data на хосте принадлежит UID 0, запись будет запрещена, даже если внутри контейнера процесс смотрит на каталог как на доступный. Проверьте владельца на хосте через ls -n.

# на хосте
ls -ln /srv/data
drwxr-xr-x 2 0 0 4096 Aug 28 10:00 data

# в контейнере
docker run -u 1000:1000 -v /srv/data:/data alpine touch /data/test
touch: /data/test: Permission denied

Решение:

chown -R 1000:1000 /srv/data

Либо запускайте контейнер с UID, совпадающим с владельцем каталога.

Read-only монтирование: как запретить запись в важные каталоги

Docker позволяет ограничить запись на уровне монтирования.

docker run -v /etc/app:/etc/app:ro myimage

Попытка записи внутри даст Read-only file system. Флаг :ro работает для bind mounts и named volumes. Для приложений, которым нужна запись во временные каталоги, добавьте tmpfs:

docker run -v /etc/app:/etc/app:ro --tmpfs /tmp:rw,size=64m myimage

Монтирование отдельных файлов и подкаталогов: точечный доступ

Лучше монтировать конкретный конфигурационный файл, а не целое дерево.

docker run -v /srv/app/config.yml:/app/config.yml:ro myimage

Предупреждение: редакторы, создающие временный файл и заменяющие оригинал, могут «оторвать» mount. В таких случаях монтируйте родительский каталог или используйте Docker Configs.

Доступ к устройствам хоста: GPU, USB и группы cgroups

Для GPU-вычислений или USB-токенов не нужен --privileged. Достаточно передать конкретное устройство и добавить контейнер в нужную группу.

Флаг --device: передача конкретного устройства без privileged mode

Найдите устройство на хосте:

ls -l /dev/dri
crw-rw---- 1 root video 226, 0 Aug 28 10:00 card0

Передайте его в контейнер:

docker run --device=/dev/dri:/dev/dri myimage

--device открывает доступ только к указанному устройству, а не ко всем устройствам хоста. Доступ к устройству всё равно может дать серьёзные возможности для эксплуатации, поэтому передавайте только то, что нужно.

Группы устройств: как UID/GID влияют на доступ к /dev

Устройство /dev/dri на хосте принадлежит группе video (GID 44). Чтобы процесс в контейнере получил доступ, он должен состоять в этой группе. Добавить группу можно через --group-add:

docker run --device=/dev/dri:/dev/dri --group-add video myimage

Или запустите процесс с нужным GID:

docker run --device=/dev/dri:/dev/dri -u 1000:44 myimage

Для USB-устройств группа часто называется plugdev. Проверьте ls -l /dev/bus/usb на хосте.

Ограничения cgroups для устройств: точечный контроль

Docker автоматически применяет device cgroup правила. Для тонкого контроля разрешайте операции чтения, записи или создания устройства по диапазону major:minor.

docker run --device-cgroup-rule='c 189:* rmw' myimage

Это разрешает доступ к USB-устройствам с major 189. Используйте --device-cgroup-rule, когда нужно разрешить диапазон, а не одно устройство.

User namespaces: изоляция root-пользователя контейнера от root хоста

Даже с non-root и сброшенными capabilities контейнерный root может отображаться на root хоста. User namespace решает эту проблему: UID 0 внутри контейнера сопоставляется с непривилегированным UID на хосте.

Как работает remap UID: отображение идентификаторов на хосте

Например, root (UID 0) в контейнере на хосте работает как UID 165536. Диапазоны задаются в /etc/subuid и /etc/subgid. Файлы, созданные контейнером на bind mount, на хосте принадлежат высокому UID и отображаются как unknown.

# на хосте после записи из контейнера с userns-remap
ls -ln /srv/data
-rw-r--r-- 1 165536 165536 0 Aug 28 10:00 created-in-container

Настройка userns-remap в daemon.json

{
  "userns-remap": "default"
}

Перезапустите демон:

systemctl restart docker
docker info | grep -i userns

Чтобы задать конкретного пользователя/группу, укажите:

{
  "userns-remap": "dockremap"
}

Пользователь должен существовать и иметь записи в /etc/subuid и /etc/subgid.

Включение userns-remap после создания контейнеров потребует их пересоздания. Планируйте это как отдельную операцию.

Ограничения и совместимость user namespaces

User namespaces ломают ряд сценариев: права на bind mounts нужно настраивать заново, host network может не работать, не все storage drivers совместимы, Kubernetes требует отдельной настройки. Проверяйте на стенде перед включением в production.

Минимизация привилегий: capabilities, seccomp, AppArmor и read-only rootfs

После non-root и user namespaces применяйте defence in depth. Сбрасывайте capabilities, ограничивайте syscalls, запрещайте запись в корневую ФС.

Сброс capabilities: --cap-drop ALL и добавление только нужных

docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx

Типовые минимальные наборы:

СервисДостаточный набор capabilities
Веб-сервер на порту 80/443NET_BIND_SERVICE
СУБД без смены владельцабез capabilities
Утилита pingNET_RAW

Без CAP_DAC_OVERRIDE root в контейнере не сможет читать файлы другого владельца.

--security-opt no-new-privileges и read-only rootfs

Флаг --security-opt no-new-privileges запрещает процессам получать новые привилегии через setuid-бинарники.

docker run --security-opt no-new-privileges --read-only --tmpfs /tmp --tmpfs /var/run myapp

Приложения, которым нужна запись в /var/lib, требуют отдельного volume или tmpfs на эти каталоги.

Seccomp и AppArmor/SELinux: профили ограничения системных вызовов

Docker по умолчанию применяет seccomp-профиль, который блокирует опасные системные вызовы. Задать свой профиль:

docker run --security-opt seccomp=/path/profile.json myapp

AppArmor или SELinux добавляют мандатный контроль доступа. Не отключайте профили без крайней необходимости.

Связанный материал: продвинутый Docker: безопасность, сети и тонкая оптимизация.

Docker socket: почему монтирование /var/run/docker.sock опасно

Монтирование /var/run/docker.sock в контейнер эквивалентно root-доступу к хосту. Изнутри контейнера можно запустить привилегированный контейнер с volume /:/host и получить файловый доступ к хосту.

Безопасные альтернативы: Docker out of Docker с выделенным демоном, Kaniko, BuildKit, раздельные CI/CD агенты.

Диагностика и исправление ошибок Permission denied

Когда контейнер возвращает Permission denied, проверяйте цепочку: UID процесса, владелец каталога, опции монтирования, capabilities, userns-remap.

Алгоритм проверки UID/GID и прав на томе

  1. docker exec <container> id
  2. На хосте ls -ln /path/to/volume
  3. Сравнить UID процесса и владельца каталога
  4. Проверить опции mount: docker inspect --format '{{.Mounts}}' <container>
  5. Проверить capabilities: docker inspect --format '{{.HostConfig.CapAdd}} {{.HostConfig.CapDrop}}' <container>
  6. Проверить userns: docker inspect --format '{{.HostConfig.UsernsMode}}' <container>

Типовые сценарии: неверный UID, read-only, отсутствие group-add

  • Контейнер под UID 1000 не пишет в каталог, принадлежащий UID 0 хоста: выполните chown -R 1000:1000 /srv/data или запустите с UID владельца.
  • Смонтирован :ro, а приложение требует запись: уберите :ro или добавьте tmpfs для временных файлов.
  • /dev/dri не открывается: добавьте --group-add video или -u 1000:44.
  • Включён userns-remap, файлы на volume имеют high UID: выполните chown на subordinate UID на хосте.

Использование docker inspect для проверки привилегий и настроек

docker inspect --format '{{.HostConfig.Privileged}}' mycontainer
docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' mycontainer
docker inspect --format '{{.HostConfig.CapAdd}} {{.HostConfig.CapDrop}}' mycontainer
docker inspect --format '{{.HostConfig.UsernsMode}}' mycontainer

Эти команды показывают фактическое состояние, даже если запуск выполнялся с дефолтами.

Контроль и аудит безопасности Docker

Безопасность контейнеров требует регулярной проверки, а не разовой настройки. Docker Bench, Falco и сканеры уязвимостей помогают поддерживать среду в актуальном состоянии.

Docker Bench for Security: быстрая проверка конфигурации

docker run --rm -it --net host --pid host --userns host \
  --cap-add audit_control \
  -v /etc:/etc:ro \
  -v /var/lib/docker:/var/lib/docker:ro \
  -v /usr/bin/docker:/usr/bin/docker:ro \
  docker/docker-bench-security

WARN-сообщения указывают на несоответствия CIS Docker Benchmark. Исправляйте их по приоритету.

Готовый чек-лист аудита Docker и Kubernetes: аудит безопасности Docker и Kubernetes.

Рантайм-мониторинг: Falco и auditd

Falco отслеживает подозрительное поведение: запуск shell в контейнере, чтение /etc/shadow, монтирование файловых систем. Auditd на хосте логирует системные вызовы. Подключите оба инструмента для проактивного контроля.

Политика обновлений образов и сканирование уязвимостей

Используйте минимальные базовые образы: distroless или alpine. Регулярно пересобирайте образы и сканируйте их Trivy или Grype. Уязвимости ядра внутри контейнера не закрываются, поэтому обновляйте и хост.

trivy image myapp:latest
grype myapp:latest

Практические команды для сканирования и настройки защиты собраны в чек-листе безопасности Docker-контейнеров 2026.

Для проверки описанных настроек в изолированной среде подойдёт облачный сервер с Docker. Например, Timeweb Cloud предоставляет VDS и Kubernetes, на которых можно безопасно тестировать конфигурации без риска для рабочего хоста.

Поделиться:
Сохранить гайд? В закладки браузера