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

0
5
Обложка статьи: 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).
  • Журналы контейнеров отправляйте в систему сбора: сбор логов.

Как внедрять

  1. Начните с безопасных параметров: no-new-privileges, лимиты памяти и процессов, публикация портов на 127.0.0.1.
  2. Включите cap_drop: [ALL] и посмотрите, что не работает; добавляйте минимальные привилегии по одной, опираясь на ошибки в журнале контейнера (docker logs).
  3. Включите read_only: true и вынесите записываемые каталоги в tmpfs или тома; список каталогов, в которые пишет приложение, видно по ошибкам запуска.
  4. Запускайте не от root, если образ это позволяет (нужны права на тома).
  5. Проверьте командой docker inspect итоговые параметры: docker inspect ИМЯ --format 'readonly={{.HostConfig.ReadonlyRootfs}} priv={{.HostConfig.Privileged}} mem={{.HostConfig.Memory}} pids={{.HostConfig.PidsLimit}}'.
  6. Оформите шаблон 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 дают воспроизводимый эталон для контейнеров.

Как проверить

  1. Во всех контейнерах нет privileged и доступа к docker.sock; порты публикуются на нужные адреса.
  2. Включены no-new-privileges, cap_drop: [ALL] с минимальным cap_add, лимиты памяти и процессов.
  3. Там, где возможно, корневая ФС только для чтения и запуск не от root.
  4. Образы закреплены по версии и регулярно проверяются на уязвимости.

Источники

  • Методика оценки уровня зрелости кибербезопасности компании, Группа Сбер: sberbank.ru.
  • Документация Docker: ограничения ресурсов и привилегий среды выполнения: docs.docker.com; справочник Compose: docs.docker.com.
  • CIS Docker Benchmark: cisecurity.org. Международная практика, не российский норматив.

Оставьте свой ответ