По умолчанию процесс в 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/443 | NET_BIND_SERVICE |
| СУБД без смены владельца | без capabilities |
| Утилита ping | NET_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 и прав на томе
docker exec <container> id- На хосте
ls -ln /path/to/volume - Сравнить UID процесса и владельца каталога
- Проверить опции mount:
docker inspect --format '{{.Mounts}}' <container> - Проверить capabilities:
docker inspect --format '{{.HostConfig.CapAdd}} {{.HostConfig.CapDrop}}' <container> - Проверить 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, на которых можно безопасно тестировать конфигурации без риска для рабочего хоста.