Docker: ограничения контейнера — read-only, cap_drop, лимиты и порты

Контейнер по умолчанию запускается от root со стандартным набором привилегий, с записью в корневую файловую систему и без лимитов. Взлом приложения в такой оболочке даёт нарушителю заметно больше, чем нужно. Несколько флагов Docker и параметров Compose сужают возможности: файловая система только для чтения, отказ от привилегий Linux, запрет повышения прав, ограничения ресурсов и публикация портов только на локальном адресе. Ниже проверенный рецепт на примере nginx, разбор поломок и порядок внедрения.
Статья опирается на логику направления «Безопасность конечных устройств» из методики зрелости Группы Сбер (источник); это методика для крупных компаний, мы используем её как ориентир и пересказываем своими словами. Проверки выполнены на Docker 29.8 и Compose 5.5 на Debian 12 с образом nginx:alpine. Запуск от обычного пользователя и основы: контейнер не от root.
Что должно получиться
| Уровень | Что это значит на практике |
|---|---|
| 2. Гигиенический | Есть эталонная конфигурация и проверка отклонений; контейнеры работают с минимальными правами и ограничениями ресурсов |
Критерии сформулированы нами по мотивам методики, это ориентир, а не оценка соответствия.
Что делает каждый параметр
| Параметр Docker (Compose) | Эффект | Результат проверки |
|---|---|---|
| без ограничений | root в контейнере, запись в корневую ФС, набор привилегий | id -u даёт 0, запись в / удалась, CapEff: 00000000a80425fb |
--read-only (read_only: true) |
Корневая ФС только для чтения | запись в / даёт Read-only file system |
--tmpfs /tmp (tmpfs:) |
Записываемый каталог в памяти | запись в /tmp проходит; можно добавить noexec,nosuid |
--cap-drop ALL (cap_drop: [ALL]) |
Убрать все привилегии Linux | CapEff: 0000000000000000 |
--security-opt no-new-privileges:true |
Запретить повышение прав через setuid-программы | NoNewPrivs: 1; su выдаёт «must be suid to work properly» |
--user 10001:10001 (user:) |
Запуск не от root | id -u даёт 10001 |
--memory, --pids-limit (mem_limit, pids_limit) |
Лимит памяти и числа процессов (защита от исчерпания ресурсов) | в docker inspect: память 67108864 (64 МБ), процессы 32 |
--privileged |
Полный доступ к устройствам хоста; не использовать | в docker inspect поле Privileged: false у обычных контейнеров |
Контейнер, запущенный с --read-only --cap-drop ALL --user 10001, не смог записать в корневую ФС, но смог в /tmp; попытка записи в /proc/sys завершилась ошибкой «Read-only file system».
Рецепт: nginx в ограниченном контейнере
Не каждый образ запустится после включения всех ограничений. Первая попытка с --read-only --cap-drop ALL на nginx:alpine закончилась остановкой контейнера: mkdir() "/var/cache/nginx/client_temp" failed (30: Read-only file system). Образу нужны записываемые каталоги и часть привилегий. Рабочий набор:
# compose.yml: пример ограниченного контейнера (nginx как образец; подставьте свой образ)
services:
web:
image: nginx:alpine
read_only: true # корневая файловая система только для чтения
cap_drop: [ALL] # убрать все привилегии Linux
cap_add: [CHOWN, SETUID, SETGID, NET_BIND_SERVICE] # вернуть только нужные образу
security_opt:
- no-new-privileges:true # запретить повышение привилегий через setuid
tmpfs: # каталоги, в которые образ пишет при работе
- /var/cache/nginx
- /var/run
- /tmp
mem_limit: 128m
pids_limit: 100
ports:
- "127.0.0.1:8081:80" # опубликовать только на локальном адресе; наружу через прокси
restart: unless-stopped
Что сделано: каталоги, в которые nginx пишет при работе (/var/cache/nginx, /var/run, /tmp), вынесены в tmpfs; возвращены четыре привилегии, нужные образу для старта: смена владельца (CHOWN), смена пользователя (SETUID, SETGID) и открытие порта 80 (NET_BIND_SERVICE). Результат проверки: контейнер запущен, ответ на запрос 200, CapEff: 00000000000004c1 (минимальный набор), NoNewPrivs: 1, запись в каталог сайта даёт Read-only file system. Проверка конфигурации: docker compose config -q; запуск: docker compose up -d.
Публикация портов
- Строка
"127.0.0.1:8081:80"публикует порт только на локальном адресе: проверкаss -ltnпоказывает прослушивание на127.0.0.1:8081, а не на0.0.0.0. Наружу сервис выставляется через прокси (nginx, Traefik) с TLS. - Строка
"8081:80"без адреса открывает порт на всех интерфейсах. Docker вносит собственные правила в сетевой фильтр и обходит правила, написанные для штатного firewall (ufw, часть правилnftables): порт оказывается открытым, даже если вы его «закрыли». Правила для таких случаев и ограничения на уровне цепочки Docker: firewall в Linux. - Для внутреннего обмена между контейнерами используйте общую сеть Compose без публикации портов вовсе.
Остальные меры
- Не используйте
privileged: trueи подключение/var/run/docker.sockвнутрь контейнеров: доступ к сокету равен правам root на хосте. - Монтируйте тома с минимальными правами: каталоги данных на запись, конфигурацию только для чтения (
:ro). - Закрепляйте версии образов (не
latestв рабочей среде) и сканируйте их на уязвимости: проверка зависимостей в CI. - Секреты не в образах и не в переменных окружения в открытом виде: используйте секреты Compose или менеджер секретов: секреты в git.
- Профиль seccomp и AppArmor по умолчанию включены в Docker; не отключайте их без необходимости (
seccomp=unconfined). - Журналы контейнеров отправляйте в систему сбора: сбор логов.
Как внедрять
- Начните с безопасных параметров:
no-new-privileges, лимиты памяти и процессов, публикация портов на 127.0.0.1. - Включите
cap_drop: [ALL]и посмотрите, что не работает; добавляйте минимальные привилегии по одной, опираясь на ошибки в журнале контейнера (docker logs). - Включите
read_only: trueи вынесите записываемые каталоги вtmpfsили тома; список каталогов, в которые пишет приложение, видно по ошибкам запуска. - Запускайте не от root, если образ это позволяет (нужны права на тома).
- Проверьте командой
docker inspectитоговые параметры:docker inspect ИМЯ --format 'readonly={{.HostConfig.ReadonlyRootfs}} priv={{.HostConfig.Privileged}} mem={{.HostConfig.Memory}} pids={{.HostConfig.PidsLimit}}'. - Оформите шаблон Compose-файла для команды и проверку в конвейере; изменения вносите через карточку изменений: управление изменениями.
Диагностика
| Симптом | Причина и что делать |
|---|---|
Контейнер остановился: Read-only file system |
Приложение пишет в корневую ФС. Найдите путь в сообщении и вынесите в tmpfs или том |
Ошибка Operation not permitted при старте |
Не хватает привилегии. Определите нужную (CHOWN, SETUID, NET_BIND_SERVICE и т.д.) и добавьте через cap_add |
| Не открывается порт ниже 1024 у непривилегированного пользователя | Нужен NET_BIND_SERVICE либо используйте порт выше 1024 и сопоставьте при публикации |
| Контейнер убивается по памяти (OOM) | Лимит слишком мал. Измерьте реальное потребление (docker stats) и задайте запас |
| Порт доступен снаружи, хотя firewall его закрывает | Docker обошёл правила. Публикуйте на 127.0.0.1 или управляйте цепочкой Docker |
Где это в модели зрелости
Материал относится к направлению «Безопасность конечных устройств» (материалы направления, карта зрелости). Уровень 2 предполагает эталонную конфигурацию и проверку отклонений: Compose-шаблон с ограничениями и проверка docker inspect дают воспроизводимый эталон для контейнеров.
Как проверить
- Во всех контейнерах нет
privilegedи доступа кdocker.sock; порты публикуются на нужные адреса. - Включены
no-new-privileges,cap_drop: [ALL]с минимальнымcap_add, лимиты памяти и процессов. - Там, где возможно, корневая ФС только для чтения и запуск не от root.
- Образы закреплены по версии и регулярно проверяются на уязвимости.
Источники
- Методика оценки уровня зрелости кибербезопасности компании, Группа Сбер: sberbank.ru.
- Документация Docker: ограничения ресурсов и привилегий среды выполнения: docs.docker.com; справочник Compose: docs.docker.com.
- CIS Docker Benchmark: cisecurity.org. Международная практика, не российский норматив.



















