Безопасность глубже: rootless, user namespaces, seccomp, capabilities

Рейтинг: 48.7% · 7 голосов
Практический курс по Docker: образы, контейнеры, тома, сети, Compose и продакшен. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
Marina_DevOps
Сообщения: 54
Зарегистрирован: 11 май 2026, 05:31

Безопасность глубже: rootless, user namespaces, seccomp, capabilities

Сообщение Marina_DevOps »

Оглавление курса (46)
  1. Что такое Docker и какие задачи он решает
  2. Установка Docker и запуск первого контейнера
  3. Образы: слои, теги и реестр Docker Hub
  4. Пишем свой Dockerfile
  5. Тома и хранение данных: volumes и bind mounts
  6. Сети в Docker: связываем контейнеры между собой
  7. Переменные окружения и конфигурация контейнеров
  8. Docker Compose: поднимаем многоконтейнерное приложение
  9. Оптимизация образов: multi-stage сборка, размер и кэш слоёв
  10. Логи, отладка и мониторинг контейнеров
  11. Базовая безопасность контейнеров
  12. Подготовка к продакшену: что важно учесть
  13. Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root
  14. Реестры образов: приватные registry, push и pull, теги и digest, imagePullSecrets
  15. BuildKit и buildx: multi-arch сборки, секреты сборки, экспорт кэша
  16. Docker в CI/CD: автосборка, сканирование образов (Trivy, Docker Scout), публикация
  17. Итоговый проект и куда расти: от Dockerfile до прода, обзор оркестрации (Kubernetes, Podman, OCI)
  18. Архитектура Docker: dockerd, containerd, runc и OCI
  19. Как работает изоляция: namespaces и cgroups
  20. Образы изнутри: слои, OverlayFS и storage driver
  21. Жизненный цикл контейнера и политики перезапуска
  22. Отладка контейнеров: exec, attach, nsenter, debug
  23. Сети Docker глубже: bridge, overlay, macvlan и iptables
  24. Docker Compose глубже: profiles, healthcheck-зависимости, override
  25. Данные и тома глубже: драйверы, бэкап, права
  26. Секреты и конфигурация: как не светить пароли
  27. Ограничение ресурсов: CPU, память, OOM и cgroups
  28. Здоровье и автозапуск: HEALTHCHECK, init и сигналы
  29. Логирование контейнеров: драйверы, ротация, сбор
  30. Мониторинг контейнеров: stats, cAdvisor, Prometheus
  31. Безопасность глубже: rootless, user namespaces, seccomp, capabilities (вы здесь)
  32. Supply chain: сканирование, SBOM и подпись образов
  33. Глубокая оптимизация образов: distroless, scratch, кэш
  34. Multi-arch и сборка под ARM: buildx и эмуляция
  35. Реестры в продакшене: Harbor и облачные registry
  36. Docker без Docker: Podman, containerd, nerdctl, Buildah
  37. От Compose к оркестрации: когда контейнеров становится много
  38. Docker Swarm: встроенная оркестрация
  39. Сборка образов в CI без демона: kaniko, BuildKit, Buildah
  40. Траблшутинг Docker: типичные ошибки и как чинить
  41. Производительность и эксплуатация Docker-хоста
  42. Docker Desktop, WSL2 и альтернативы на Windows и Mac
  43. Контейнеризация приложений: бэкенд, фронтенд, БД
  44. Docker и Kubernetes: containerd, миграция, kompose
  45. Лучшие практики и антипаттерны Docker
  46. Сквозной проект: production-ready стек и путь дальше
Почему root в контейнере - это не тот root, о котором ты думаешь

Когда ты впервые запускаешь

Код: Выделить всё

docker run -it ubuntu bash
и видишь приглашение

Код: Выделить всё

root@a1b2c3:/#
, возникает ложное чувство уюта: я в песочнице, я тут король, наружу ничего не утечёт. На деле по умолчанию этот root внутри контейнера - тот же самый UID 0, что и root на хосте. Их разделяют не разные личности, а только namespaces и пара фильтров ядра. Стоит этим фильтрам дать слабину (баг в runc, забытый

Код: Выделить всё

--privileged
, проброшенный сокет), и UID 0 из контейнера становится UID 0 на машине. Это и есть побег из контейнера - container escape.

Этот урок про то, как docker безопасность строится слоями. Контейнер - не виртуалка, у него нет своего ядра. Один общий kernel на хост и на все контейнеры. Значит вся изоляция - это договорённость ядра ограничивать процесс. И договорённость можно усилить: убрать лишние привилегии (docker capabilities), отрезать опасные системные вызовы (seccomp docker), переназначить root на безымянного пользователя (user namespace docker) и вовсе убрать root из-под демона (docker rootless). Разберём каждый слой не как галочку в чеклисте, а как механику: что именно происходит в ядре и от какого класса атак это спасает.

Изображение

Capabilities: разбираем монолитный root на 40 кусочков

Исторически в Unix была дихотомия: либо ты root (UID 0) и можешь всё, либо обычный пользователь и почти ничего. Linux разбил всемогущество root на отдельные права - capabilities. Их около сорока. CAP_NET_BIND_SERVICE - право слушать порты ниже 1024. CAP_NET_RAW - сырые сокеты (ping, sniffing). CAP_SYS_ADMIN - помойка, куда свалили половину опасных операций (mount, изменение namespaces, и прочее). CAP_SYS_PTRACE - заглядывать в память чужих процессов. CAP_CHOWN, CAP_SETUID, CAP_DAC_OVERRIDE - обход прав на файлы.

Docker по умолчанию НЕ даёт контейнеру все capabilities. Он оставляет урезанный набор примерно из 14 штук: CHOWN, DAC_OVERRIDE, FSETID, FOWNER, MKNOD, NET_RAW, SETGID, SETUID, SETFCAP, SETPCAP, NET_BIND_SERVICE, SYS_CHROOT, KILL, AUDIT_WRITE. Уже неплохо - нет SYS_ADMIN, нет SYS_PTRACE, нет SYS_MODULE. Но для типичного веб-приложения и это избыточно. Зачем nginx право CHOWN или сырые сокеты?

Правильный паттерн - drop ALL, потом add только нужное:

Код: Выделить всё

docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
Проверим, что реально осталось внутри. Внутри контейнера ставим libcap (пакет libcap2-bin) и смотрим:

Код: Выделить всё

$ capsh --print
Current: cap_net_bind_service=ep
Bounding set =cap_net_bind_service
Поле Bounding set - это потолок: процесс не сможет получить capability, которой нет в этом множестве, даже если очень захочет. Видишь одну строку вместо четырнадцати - значит drop сработал. Суффикс это флаги: e (effective, право активно прямо сейчас) и p (permitted, разрешено включить).

Грабли. Нельзя dropнуть ALL у образа, который рассчитан на root-операции при старте. Классика - официальный образ, который при запуске делает chown на смонтированном томе под нужным uid. Без CAP_CHOWN он упадёт с Operation not permitted. Лечится либо добавлением точечной capability, либо переходом на образ, который не дёргает root-операции (или ставит права заранее на этапе сборки).

no-new-privileges: запираем дверь для эскалации

Даже урезав capabilities, ты оставляешь лазейку - setuid-бинарники. Файл с битом setuid (например

Код: Выделить всё

/usr/bin/sudo
или

Код: Выделить всё

/bin/ping
) при запуске поднимает привилегии до владельца файла. Если внутри образа затесался setuid-root бинарник с уязвимостью, процесс из-под обычного пользователя может через него подняться до root внутри контейнера и вернуть себе сброшенные права.

Флаг no-new-privileges ставит в ядре бит, который запрещает процессу и всем его потомкам получать привилегии, которых не было у родителя. setuid перестаёт работать как лифт наверх:

Код: Выделить всё

docker run --security-opt no-new-privileges:true myapp
Это дешёвая и почти всегда безопасная мера. Единственное исключение - если приложение легитимно зависит от setuid (редко в контейнерах). В Compose это поле security_opt:

Код: Выделить всё

services:
  app:
    image: myapp:1.0
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE]
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
seccomp docker: фильтруем системные вызовы

Capabilities ограничивают права, но не сужают саму поверхность ядра. Программа всё ещё может позвать любой из ~400 системных вызовов. А ядро - это код, в коде есть баги, и многие побеги шли через экзотические syscalls вроде , ,

Код: Выделить всё

userfaultfd
. seccomp (secure computing mode) - это фильтр на уровне syscall: для каждого вызова ядро решает, пропустить, вернуть ошибку или убить процесс.

Docker применяет дефолтный seccomp-профиль автоматически, без всяких флагов. Он блокирует около 44 опасных вызовов из общего числа: среди них mount, umount2, reboot, swapon, init_module, kexec_load, ptrace (в старых ядрах), и упомянутые keyctl, userfaultfd. Логика whitelist-подобная: разрешено то, что нужно нормальным программам, остальное опасное закрыто.

Посмотреть дефолтный профиль можно в исходниках moby (файл profiles/seccomp/default.json) - это JSON со списком syscalls и действием по умолчанию SCMP_ACT_ERRNO (вернуть EPERM). Свой профиль подключается так:

Код: Выделить всё

docker run --security-opt seccomp=/path/to/profile.json myapp
А вот это - самое опасное, что можно написать, и оно встречается в чужих гайдах под видом починки:

Код: Выделить всё

docker run --security-opt seccomp=unconfined myapp

Код: Выделить всё

seccomp=unconfined
полностью отключает фильтр. Все 400 syscalls открыты. Люди ставят это, когда что-то не запускается (часто старый gVisorНое окружение или strace внутри), и забывают убрать. Никогда не оставляй unconfined в продакшене. Если конкретный syscall нужен (debugger требует ptrace), правильнее сделать копию дефолтного профиля и точечно разрешить один вызов, а не сносить весь фильтр.

Тонкость. Дефолтный профиль Docker заточен под обычные приложения. Некоторые легитимные программы (Chromium в headless, отдельные JIT-движки, инструменты, дёргающие clone с экзотическими флагами) могут спотыкаться. Симптом - падение с EPERM или Operation not permitted там, где прав вроде хватает. Тогда смотришь strace, находишь зарезанный syscall и добавляешь его в кастомный профиль.

user namespace docker: root внутри = никто снаружи

Главная архитектурная проблема осталась: UID 0 в контейнере по умолчанию равен UID 0 на хосте. User namespace remapping (userns-remap) это чинит. Идея: внутри контейнера процесс думает, что он root (UID 0), а ядро транслирует этот UID в безобидный высокий идентификатор на хосте, скажем 165536. Если процесс сбежит из контейнера, на хосте он окажется пользователем 165536 без всяких прав - не сможет читать чужие файлы, не сможет управлять системой.

Включается на уровне демона в /etc/docker/daemon.json:

Код: Выделить всё

{
  "userns-remap": "default"
}
После

Код: Выделить всё

systemctl restart docker
Docker создаёт пользователя dockremap, читает диапазоны из /etc/subuid и /etc/subgid и разворачивает данные в подкаталог вида /var/lib/docker/165536.165536/. Проверяем эффект - запускаем процесс внутри и смотрим UID снаружи:

Код: Выделить всё

# внутри контейнера
$ id -u
0
# на хосте в это же время
$ ps -o user,pid,cmd -C sleep
USER       PID CMD
165536   48213 sleep 600
Внутри ноль, снаружи 165536. Вот это и есть изоляция личности.

Цена. userns-remap ломает часть привычных паттернов. Тома, к которым контейнер пишет, теперь принадлежат remapped-UID, и привычный bind-mount каталога с хоста может стать недоступным (права не совпадают). Нельзя совмещать с

Код: Выделить всё

--privileged
, с некоторыми сетевыми режимами host. Поэтому в реальности userns-remap часто проигрывает следующему подходу.

docker rootless: убираем root из-под самого демона

userns-remap ремапит контейнеры, но сам демон dockerd по-прежнему работает от root. А демон - это здоровенный кусок кода, слушающий сокет. Скомпрометируешь демон - получишь root на хосте. Docker rootless идёт дальше: весь демон и все контейнеры запускаются от обычного непривилегированного пользователя внутри его собственного user namespace. На хосте нет ни одного процесса Docker от root.

Это и есть ключевое отличие, которое путают. userns-remap: демон под root, контейнеры ремапнуты. Rootless: вообще всё под обычным юзером. Если злоумышленник пробьёт rootless-контейнер и даже сам демон, максимум что он получит - права того непривилегированного пользователя. Это резко снижает риск побега до root.

Ставится и запускается под нужным пользователем (не под root):

Код: Выделить всё

$ dockerd-rootless-setuptool.sh install
$ systemctl --user start docker
$ docker context use rootless
$ docker run hello-world
Под капотом rootless опирается на RootlessKit и slirp4netns (или сетевой стек с поддержкой ядра) для сети в user namespace, и на newuidmap/newgidmap - единственные бинарники с setuid/file-capability, которые тут используются. За изоляцию хранилища отвечает overlayfs в режиме fuse-overlayfs либо нативный overlay, если ядро это разрешает в user namespace.

Ограничения rootless, которые надо знать заранее.
  • Порты ниже 1024 по умолчанию недоступны - ты же непривилегированный. Либо публикуешь высокий порт, либо настраиваешь sysctl net.ipv4.ip_unprivileged_port_start.
  • Сеть медленнее на дефолтном slirp4netns (пользовательский TCP/IP стек). Для нагруженных сервисов это заметно.
  • Часть возможностей (некоторые storage-драйверы, отдельные cgroup-операции) ограничена.
  • Rootless - не серебряная пуля. В 2025 Qualys показал способы обойти ограничения на unprivileged user namespaces в Ubuntu, и был ряд CVE 2025-2026, где побег шёл именно через user namespaces. Слой полезный, но не отменяет остальные.
AppArmor, SELinux и read-only rootfs - доборные слои

Поверх seccomp и capabilities работают системы мандатного доступа (MAC). Docker на Debian/Ubuntu по умолчанию навешивает профиль AppArmor docker-default, который ограничивает, к каким файлам и операциям процесс контейнера вообще имеет доступ - независимо от его UID и capabilities. На RHEL/Fedora аналогичную роль играет SELinux с типом container_t: даже root внутри контейнера не дотянется до файлов хоста, у которых не тот SELinux-контекст. Свой AppArmor-профиль цепляется через

Код: Выделить всё

--security-opt apparmor=myprofile
, SELinux-метки тома - суффиксом или при монтировании.

Ещё два дешёвых и сильных приёма. read-only корневая ФС - контейнер не может писать в свой образ вообще:

Код: Выделить всё

docker run --read-only --tmpfs /tmp --tmpfs /run myapp
Флаг

Код: Выделить всё

--read-only
монтирует rootfs только на чтение. Малварь не запишет себе бэкдор, не подменит бинарник, не скачает второй стейдж. Туда, где приложению реально нужна запись (/tmp, /run, кэши), подкладываем tmpfs - это RAM-диск, который испаряется при остановке контейнера и не остаётся на диске. В Compose это read_only: true плюс секция tmpfs. Связка read-only rootfs + drop ALL + no-new-privileges превращает контейнер из удобного плацдарма в неуютную для атакующего коробку.

Главная дыра: docker.sock и docker privileged

Можно идеально настроить все слои выше и убить всё одним движением. Два смертных греха.

Первый - проброс сокета демона внутрь контейнера:

Код: Выделить всё

# так делать опасно
docker run -v /var/run/docker.sock:/var/run/docker.sock myapp
docker.sock - это управляющий канал к демону. Кто пишет в этот сокет, тот командует Docker. Процесс внутри такого контейнера может одной командой запустить новый контейнер с

Код: Выделить всё

--privileged
и смонтированным корнем хоста - и всё, он root на хосте. Проброс сокета равносилен раздаче root. Если CI или мониторинг реально требует доступ к API, используй socket-proxy с фильтрацией методов (только нужные эндпоинты, только чтение) либо rootless-сокет, где компрометация даёт лишь права непривилегированного юзера.

Второй грех - docker privileged:

Код: Выделить всё

docker run --privileged myapp

Код: Выделить всё

--privileged
снимает почти все защиты разом: возвращает все capabilities, отключает seccomp и AppArmor, даёт доступ ко всем устройствам хоста в /dev. Это фактически контейнер без стен. Его ставят, когда что-то не работает и лень разбираться - и это самый частый путь к побегу. Если нужна одна конкретная привилегия (например доступ к конкретному устройству), давай её точечно:

Код: Выделить всё

--device=/dev/...
или одну

Код: Выделить всё

--cap-add
, но не

Код: Выделить всё

--privileged
.

Побеги из контейнера: классы CVE

Чтобы слои выше не казались паранойей, посмотри на реальные классы побегов.
  • Уязвимости рантайма (runc). Самый свежий пример - связка CVE-2025-31133, CVE-2025-52565 и CVE-2025-52881 (конец 2025). Атака через гонки и символические ссылки на bind-mount-ах /dev/null и /dev/console: runc монтировал подменённую цель read-write до того, как защиты вставали на место, что давало запись в /proc хоста и побег. Починено в runc 1.2.8, 1.3.3 и 1.4.0. Вывод простой - держи runc/containerd свежими, это твоя первая линия.
  • Утечка через /proc и /sys при

    Код: Выделить всё

    --privileged
    или избыточных capabilities - классический трюк с release_agent у cgroups v1.
  • Проброшенные ресурсы хоста - тот самый docker.sock, монтирование / хоста, host PID/network namespace.
Заметь: ни один hardening-флаг не спас бы от уязвимости в самом runc, кроме одного - userns/rootless, который ограничивает, КЕМ ты станешь после побега. Поэтому слои не взаимозаменяемы, они складываются. seccomp режет syscalls, capabilities режет права, userns режет последствия, обновления чинят сам рантайм.

Мини-лаба: руками собрать укреплённый контейнер

Повтори по шагам и понаблюдай разницу.
  • Запусти обычный контейнер и посмотри его права:

    Код: Выделить всё

    docker run --rm -it alpine sh
    , внутри

    Код: Выделить всё

    apk add libcap; capsh --print
    . Запомни длину Bounding set.
  • Запусти то же с

    Код: Выделить всё

    --cap-drop=ALL --security-opt no-new-privileges:true
    и снова

    Код: Выделить всё

    capsh --print
    . Сравни множества.
  • Проверь seccomp: внутри обычного контейнера выполни

    Код: Выделить всё

    unshare --map-root-user --user echo ok
    - и сравни поведение при дефолтном профиле и при

    Код: Выделить всё

    --security-opt seccomp=unconfined
    . Увидишь, как фильтр меняет картину.
  • Сделай контейнер только на чтение:

    Код: Выделить всё

    docker run --rm --read-only alpine touch /x
    - получишь Read-only file system. Добавь

    Код: Выделить всё

    --tmpfs /tmp
    и убедись, что

    Код: Выделить всё

    touch /tmp/x
    проходит.
  • Включи userns-remap в daemon.json, перезапусти демон, запусти в контейнере и сверь UID внутри () и снаружи (

    Код: Выделить всё

    ps -o user -C sleep
    ).
Контрольные вопросы
  • В чём принципиальная разница между userns-remap и rootless Docker, и от какого именно сценария атаки защищает только rootless?
  • Почему

    Код: Выделить всё

    --cap-drop=ALL
    без

    Код: Выделить всё

    no-new-privileges
    всё ещё оставляет лазейку, и как она работает?
  • Что конкретно делает seccomp=unconfined и почему этот режим опасен в продакшене?
  • Чем проброс /var/run/docker.sock в контейнер эквивалентен по последствиям запуску

    Код: Выделить всё

    --privileged
    ?
Итог

Безопасность контейнера - это не один флаг, а стек независимых слоёв на общем ядре. capabilities урезают права (drop ALL, add точечно), no-new-privileges глушит setuid-эскалацию, seccomp режет опасные syscalls, AppArmor/SELinux ставят мандатные стены, read-only rootfs и tmpfs лишают атакующего места для закрепления, userns-remap и особенно rootless обезвреживают сам факт побега, переназначая root в безобидного юзера. И всё это не работает, если ты пробросил docker.sock или включил

Код: Выделить всё

--privileged
. Свежий runc/containerd закрывает дыры в самом рантайме. Складывай слои, не полагайся ни на один в одиночку - так строится настоящая docker безопасность.
👍7 ❤️2 🔥2 😄 🤔1
Аватара пользователя
prometheus87
Сообщения: 1
Зарегистрирован: 02 июн 2026, 15:45

Re: Безопасность глубже: rootless, user namespaces, seccomp, capabilities

Сообщение prometheus87 »

Сидел гадал почему официальный образ падает с Operation not permitted после cap-drop ALL, оказалось ему CHOWN на томе нужен. Раздел про грабли прям в точку, спасибо
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
postfach205
Сообщения: 1
Зарегистрирован: 30 май 2026, 11:03

Re: Безопасность глубже: rootless, user namespaces, seccomp, capabilities

Сообщение postfach205 »

А правда что в rootless порт 80 нельзя слушать без танцев с бубном? У меня traefik как раз на 80/443 висит, теперь думаю стоит ли вообще переезжать или хватит userns-remap
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Мониторинг контейнеров: stats, cAdvisor, Prometheus
Следующая глава →
Supply chain: сканирование, SBOM и подпись образов

Все главы курса «Docker: контейнеризация от основ до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: docker rootless и безопасность контейнеровkubernetes rbac как настроить доступы и ролиnjs и динамические модули nginxБезопасность nginx: заголовки и hardeningRate limiting в nginx: защита от перегрузкиКонтроль доступа в nginx: auth и allow/deny

Вернуться в «Docker с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость