Углубленный курс по безопасности Docker и Podman

Профессиональная программа подготовки специалистов по ИБ, сфокусированная на механизмах изоляции Linux, аудите по мировым стандартам и внедрении практик DevSecOps. Курс базируется на методологии Liz Rice и актуальных Best Practices для защиты контейнерных сред.

1. Архитектура контейнеризации и анализ векторов атак на инфраструктуру

Архитектура контейнеризации и анализ векторов атак на инфраструктуру

Представьте, что вы строите многоквартирный дом, где вместо капитальных кирпичных стен между соседями установлены лишь тонкие гипсокартонные перегородки, а водопровод и вентиляция — общие и никак не изолированы. В мире информационных систем долгое время господствовала виртуализация (VM), предоставлявшая каждому «жильцу» полноценную изолированную крепость с собственным фундаментом (ядром ОС). Контейнеризация изменила правила игры, предложив коммунальный комфорт: общие ресурсы, общее ядро, но иллюзию полной приватности. Для специалиста по безопасности это означает, что любая трещина в «гипсокартоне» пространства имен или малейшая ошибка в настройке общих коммуникаций может привести к тому, что злоумышленник из одной квартиры мгновенно окажется в спальне у соседа или, что еще хуже, захватит управление всем зданием.

Контейнер как процесс: архитектурный сдвиг

Главное заблуждение, с которым приходится сталкиваться при аудите безопасности, — восприятие контейнера как «легковесной виртуальной машины». С точки зрения безопасности это опасная когнитивная ловушка. Виртуальная машина изолирована на уровне гипервизора, который эмулирует аппаратное обеспечение. Контейнер же — это всего лишь обычный процесс в операционной системе Linux, который «обманут» механизмами ядра так, что он считает себя изолированным.

Если мы выполним команду ps aux на хостовой машине, мы увидим процессы контейнеров точно так же, как процессы веб-браузера или текстового редактора. Разница лишь в том, что эти процессы запущены внутри специфических ограничителей. Архитектура Docker и Podman строится на трех столпах ядра Linux:

  • Namespaces (Пространства имен): отвечают за то, что процесс видит. Они создают видимость отдельной файловой системы, сети и списка пользователей.
  • Control Groups (cgroups): отвечают за то, сколько процесс может потреблять. Они ограничивают ресурсы: CPU, RAM, I/O.
  • LSM (Linux Security Modules) и Capabilities: отвечают за то, что процессу разрешено делать.
  • Понимание этой «процессной» природы критично. Если в VM злоумышленнику для захвата хоста нужно преодолеть барьер гипервизора (что случается крайне редко и требует сложнейших эксплойтов), то в контейнере он уже находится внутри ядра хоста. Его отделяет от свободы лишь программная конфигурация этих самых пространств имен и прав доступа.

    Модель угроз: от Docker Daemon до рантайма

    При анализе векторов атак на контейнерную инфраструктуру мы должны сегментировать среду на четыре ключевые зоны поражения. Каждая зона требует своего подхода к защите и мониторингу.

    !Необходимо отобразить Модель угроз: от Docker Daemon до рантайма. Сделать 1 общую картинку для модели угроз и 4 картинки соответственно: 1. Зона 1: Цепочка поставок (Supply Chain) 2. Зона 2: Инфраструктура управления (The Orchestrator & Daemon) 3. Зона 3: Среда исполнения (Container Runtime) 4. Зона 4: Сетевое взаимодействие

    Зона 1: Цепочка поставок (Supply Chain)

    Атака начинается задолго до запуска контейнера. Образ — это статический слепок файловой системы. Если разработчик использует в качестве базового образа node:latest из публичного репозитория, он неявно доверяет сотням мейнтейнеров пакетов. Векторы атаки здесь включают: * Poisoning (Отравление образов): внедрение бэкдоров в популярные образы на Docker Hub. * Typosquatting: создание образа с именем, похожим на популярное (например, alpinne вместо alpine), содержащего майнер криптовалюты. * Секреты в слоях: оставленные в Dockerfile ключи SSH или API-токены. Даже если вы удалите файл в следующем слое, он останется в истории образа и может быть извлечен.

    !Модель угроз Docker Зона 1: Цепочка поставок (Supply Chain) Атака начинается задолго до запуска контейнера. Образ — это статический слепок файловой системы. Если разработчик использует в качестве базового образа node:latest из публичного репозитория, он неявно доверяет сотням мейнтейнеров пакетов. Векторы атаки здесь включают: Poisoning (Отравление образов): внедрение бэкдоров в популярные образы на Docker Hub. Typosquatting: создание образа с именем, похожим на популярное (например, alpinne вместо alpine), содержащего майнер криптовалюты. Секреты в слоях: оставленные в Dockerfile ключи SSH или API-токены. Даже если вы удалите файл в следующем слое, он останется в истории образа и может быть извлечен.

    Зона 2: Инфраструктура управления (The Orchestrator & Daemon)

    Здесь кроется фундаментальное различие между Docker и Podman. Docker по умолчанию использует клиент-серверную архитектуру с центральным демоном (dockerd), работающим от имени root. * Атака через сокет: если злоумышленник получает доступ к /var/run/docker.sock, он фактически получает права root на хосте. Команда docker run -v /:/host alpine позволяет смонтировать корень хостовой системы внутрь контейнера и изменить, например, /etc/shadow. * API без аутентификации: Docker API, выставленный на сетевой порт без TLS, — это открытая дверь для массового заражения червями-шифровальщиками.

    !Модель угроз Docker Зона 2: Инфраструктура управления (The Orchestrator & Daemon) Здесь кроется фундаментальное различие между Docker и Podman. Docker по умолчанию использует клиент-серверную архитектуру с центральным демоном (dockerd), работающим от имени root. Атака через сокет: если злоумышленник получает доступ к /var/run/docker.sock, он фактически получает права root на хосте. Команда docker run -v /:/host alpine позволяет смонтировать корень хостовой системы внутрь контейнера и изменить, например, /etc/shadow. API без аутентификации: Docker API, выставленный на сетевой порт без TLS, — это открытая дверь для массового заражения червями-шифровальщиками.

    Зона 3: Среда исполнения (Container Runtime)

    Это момент, когда образ превращается в процесс. Основной риск здесь — Container Escape (побег из контейнера). * Злоупотребление Capabilities: выдача контейнеру избыточных привилегий (например, CAP_SYS_ADMIN) позволяет ему манипулировать монтированием файловых систем или загружать модули ядра. * Уязвимости ядра: поскольку ядро общее, любая уязвимость в syscall (системном вызове), которую ядро не успело закрыть патчем, может быть использована для повышения привилегий до уровня хоста.

    !Модель угроз Docker Зона 3: Среда исполнения (Container Runtime) Это момент, когда образ превращается в процесс. Основной риск здесь — Container Escape (побег из контейнера). Злоупотребление Capabilities: выдача контейнеру избыточных привилегий (например, CAP_SYS_ADMIN) позволяет ему манипулировать монтированием файловых систем или загружать модули ядра. Уязвимости ядра: поскольку ядро общее, любая уязвимость в syscall (системном вызове), которую ядро не успело закрыть патчем, может быть использована для повышения привилегий до уровня хоста.

    Зона 4: Сетевое взаимодействие

    Контейнеры часто общаются друг с другом через виртуальные мосты (bridge). По умолчанию в Docker все контейнеры в одной сети могут «видеть» друг друга. Без микросегментации взлом одного микросервиса (например, фронтенда) открывает прямой путь к базе данных через внутреннюю сеть, минуя внешние фаерволы.

    !Модель угроз Docker Зона 4: Сетевое взаимодействие Контейнеры часто общаются друг с другом через виртуальные мосты (bridge). По умолчанию в Docker все контейнеры в одной сети могут «видеть» друг друга. Без микросегментации взлом одного микросервиса (например, фронтенда) открывает прямой путь к базе данных через внутреннюю сеть, минуя внешние фаерволы. В стандартной конфигурации Docker создает виртуальный коммутатор (bridge), к которому подключаются все запущенные контейнеры. Это создает единое доверенное пространство, где действуют следующие риски: Отсутствие изоляции: Если вы не создали отдельные сети для разных компонентов, фронтенд и база данных окажутся в одном «сегменте». Это позволяет им обмениваться любым трафиком, даже если это не предусмотрено логикой приложения. Обход внешних защит: Внешний фаервол (брандмауэр) защищает только вход в систему из интернета. Если хакер взломал фронтенд, он уже находится «внутри» периметра, и его атаки на базу данных по внутренней сети Docker никем не фильтруются. Lateral Movement (Горизонтальное перемещение): Злоумышленник может использовать инструменты сканирования (например, nmap) прямо из взломанного контейнера, чтобы найти другие уязвимые сервисы в этой же виртуальной сети.

    Глубокое погружение в Container Escape: пример с privileged-режимом

    Одним из самых опасных флагов в Docker является --privileged. Профессорская этика требует разобрать этот случай детально, так как он наглядно демонстрирует хрупкость контейнерной изоляции. Когда вы запускаете контейнер с этим флагом, Docker отключает почти все механизмы защиты: не применяются профили AppArmor/Seccomp, передаются все Linux Capabilities, и, что самое важное, контейнеру открывается доступ ко всем устройствам хоста в /dev.

    Рассмотрим механику атаки через release_agent. В старых версиях ядер и cgroups v1 это был классический способ побега:

  • Контейнер запускается с --privileged.
  • Злоумышленник монтирует контроллер cgroup rdma или memory с хоста.
  • Внутри cgroup создается директория, и в файл release_agent записывается путь к скрипту внутри контейнера.
  • Этот скрипт будет выполнен ядром хоста с правами root, когда последний процесс в данной cgroup завершится.
  • Злоумышленник провоцирует завершение процесса, и вуаля — скрипт (например, reverse shell) выполняется в контексте хостовой ОС.
  • Этот пример учит нас правилу: изоляция в контейнерах — это набор замков на одной двери. Если вы открыли один (дали CAP_SYS_ADMIN), остальные (Namespaces) могут не удержать грабителя.

    !widget: Рассмотрим механику атаки через release_agent: 1. Контейнер запускается с --privileged. 2. Злоумышленник монтирует контроллер cgroup rdma или memory с хоста. 3. Внутри cgroup создается директория, и в файл release_agent записывается путь к скрипту внутри контейнера. 4. Этот скрипт будет выполнен ядром хоста с правами root, когда последний процесс в данной cgroup завершится. 5. Злоумышленник провоцирует завершение процесса, и вуаля — скрипт (например, reverse shell) выполняется в контексте хостовой ОС.

    Сравнение моделей безопасности: Docker vs Podman

    Как специалисты по ИБ, мы должны оценивать не только удобство, но и архитектурную устойчивость. Здесь Podman предлагает радикально иной подход, основанный на принципе минимизации привилегий.

    | Характеристика | Docker (традиционный) | Podman (Rootless) | | :--- | :--- | :--- | | Архитектура | Клиент-сервер (Daemon-based) | Fork/Exec (Daemonless) | | Права процесса | Обычно root (демон всегда root) | Обычный пользователь (непривилегированный) | | Точка отказа | Docker Daemon (взлом демона = взлом хоста) | Отсутствует центральный демон | | Изоляция пользователей | Требует сложной настройки User Namespaces | User Namespaces включены по умолчанию | | Аудит | Сложно отследить, кто запустил процесс (все от root) | Интеграция с auditd (виден реальный UID пользователя) |

    В Docker, когда вы вводите команду, клиент общается с демоном через сокет. Демон порождает дочерний процесс. В системных логах владельцем процесса будет числиться root. В Podman процесс контейнера является прямым потомком процесса пользователя, запустившего его. Если пользователь ivan запускает контейнер, то и в htop на хосте мы увидим, что процесс принадлежит ivan. Это значительно упрощает расследование инцидентов и ограничивает радиус поражения: даже если злоумышленник «сбежит» из rootless-контейнера, он окажется на хосте с правами обычного пользователя ivan, а не суперпользователя.

    Векторы атак через общие ресурсы ядра

    Поскольку ядро — это общая библиотека функций для всех контейнеров, атаки на него являются наиболее разрушительными. Проблема в том, что поверхность атаки ядра огромна — более 300 системных вызовов.

    Эксплуатация системных вызовов

    Контейнер может попытаться вызвать функцию ядра, которая не была должным образом протестирована на предмет безопасности в контексте пространств имен. Классический пример — уязвимость Dirty COW (). Она позволяла непривилегированному пользователю (в том числе внутри контейнера) получить доступ на запись к read-only участкам памяти. Поскольку ядро общее, контейнер мог изменить системные файлы самого хоста, находящиеся в памяти.

    Атаки на файловую систему (Procfs и Sysfs)

    Виртуальные файловые системы /proc и /sys — это окна в душу ядра. Если они смонтированы в контейнер неправильно (или в режиме read-write), контейнер может изменить параметры работы железа хоста или прервать работу других контейнеров. Например, доступ к /proc/sysrq-trigger позволяет удаленно перезагрузить хост или вызвать kernel panic простым echo.

    Методология анализа по CIS Benchmark и OWASP

    Для проведения качественного аудита мы не можем полагаться на интуицию. Нам нужны стандарты.

  • CIS Docker Benchmark: это детальный чек-лист (более 100 страниц), который предписывает проверять всё: от прав доступа на конфигурационные файлы до настроек сетевого взаимодействия. Ключевой акцент делается на "Hardening" хоста. Если хостовая ОС (Debian/Alpine) не защищена, защита контейнеров бессмысленна.
  • OWASP Docker Security Cheat Sheet: фокусируется на безопасности жизненного цикла приложения. Здесь мы смотрим на то, как собираются образы, как управляются секреты (не через переменные окружения!) и как настроено сканирование на уязвимости.
  • Jet Container Security Framework: предлагает более высокоуровневый взгляд, разделяя защиту на этапы Build, Ship, Run.
  • Практический аспект: Аудит цепочки Build

    При аудите Dockerfile мы ищем нарушения "гигиены": * Использование sudo внутри контейнера. * Отсутствие инструкции USER. По умолчанию контейнер запускается от root. Хорошая практика — создать системного пользователя с минимальными правами:

    * Установка лишних пакетов (курл, гит, компиляторы). В идеале в образе должен быть только бинарный файл приложения и его зависимости. Образы на базе Alpine хороши своей компактностью, но и они могут быть избыточны. Для Go-приложений стандартом де-факто является использование scratch — абсолютно пустого образа.

    Сетевая безопасность: иллюзия изоляции

    Многие полагают, что если контейнер не пробросил порты наружу (-p), он недоступен. Это не так. Внутри Docker-хоста существует внутренняя сеть. Злоумышленник, захвативший один контейнер, может использовать его как плацдарм для сканирования внутренней сети (nmap по диапазону 172.17.0.0/16).

    Проблема усугубляется использованием host networking (--network host). В этом режиме контейнер вообще не получает своего сетевого пространства имен и использует сетевой стек хоста. Это дает ему возможность прослушивать трафик других интерфейсов хоста, менять таблицу маршрутизации и обходить локальные фаерволы, которые часто доверяют трафику с localhost.

    Управление секретами: где мы ошибаемся

    Одной из самых частых находок при аудите является передача паролей и API-ключей через переменные окружения (ENV в Dockerfile или -e в рантайме). Почему это плохо?

  • Переменные окружения видны в выводе docker inspect.
  • Они наследуются дочерними процессами.
  • Они часто попадают в логи систем мониторинга и отладки.
  • Правильный путь — использование механизмов Docker Secrets (в режиме Swarm) или монтирование секретов в tmpfs (память), чтобы они никогда не касались диска и не оставались в истории слоев образа.

    Резюмируя архитектурные риски

    Безопасность контейнеров — это не продукт, который можно купить, а процесс настройки ограничений. Мы должны исходить из принципа Zero Trust внутри хоста. Контейнер должен: * Работать от не-root пользователя. * Иметь файловую систему только для чтения (--read-only). * Иметь жестко ограниченные ресурсы (cgroups), чтобы предотвратить DoS-атаки на соседей. * Общаться только с теми сервисами, которые ему необходимы для работы (сетевая сегментация).

    В следующих главах мы детально разберем, как именно настраивать эти механизмы «под капотом» ядра, переходя от теории векторов атак к практике глубокой защиты. Помните: в мире контейнеров ваша главная защита — это не стены, а правильно настроенные фильтры системных вызовов и строгая дисциплина при сборке образов.

    2. Фундамент изоляции: механизмы Namespaces, Cgroups и Linux Capabilities

    Если вы запустите контейнер с командой sleep 9999, а затем откроете второй терминал на самом хосте и выполните ps aux | grep sleep, вы увидите этот процесс. У него будет свой PID, он будет выполняться от имени какого-то пользователя, и ядро ОС будет управлять им точно так же, как системным демоном sshd или вашим браузером. Эта простая проверка разрушает главную иллюзию новичков: контейнеры не являются виртуальными машинами. В них нет гостевого ядра, нет аппаратной виртуализации. Контейнер — это просто процесс (или группа процессов) на хосте, вокруг которого ядро Linux возвело логические стены.

    Чтобы проводить аудит безопасности инфраструктуры, недостаточно знать, что эти стены существуют. Необходимо понимать, из какого «материала» они построены, где в них заложены двери и как атакующий может эти двери взломать. Вся безопасность среды исполнения (runtime) базируется на трех фундаментальных механизмах ядра Linux: Namespaces (пространства имен), Cgroups (контрольные группы) и Capabilities (мандаты).

    Пространства имен (Namespaces): изоляция видимости

    Механизм Namespaces отвечает за то, что процесс внутри контейнера видит вокруг себя. Если процесс скомпрометирован, пространства имен гарантируют, что злоумышленник не увидит процессы других контейнеров, сетевые интерфейсы хоста или точки монтирования файловой системы.

    Ядро Linux реализует несколько типов пространств имен, каждый из которых изолирует определенный срез системных ресурсов. Для аудитора безопасности критически важно понимать, как ядро отслеживает эту изоляцию. Ядро использует специальные виртуальные файлы в директории /proc/<pid>/ns/. Каждое пространство имен представлено символической ссылкой, указывающей на уникальный номер inode.

    Если мы запустим контейнер на базе Linux Alpine и проверим его PID на хосте (допустим, это PID 4512), мы можем заглянуть в его пространства имен:

    Вывод покажет набор ссылок:

  • cgroup -> cgroup:[4026531835]
  • ipc -> ipc:[4026532253]
  • mnt -> mnt:[4026532251]
  • net -> net:[4026532256]
  • pid -> pid:[4026532254]
  • user -> user:[4026531837]
  • uts -> uts:[4026532252]
  • Если два процесса имеют одинаковый номер inode для определенного namespace, они находятся в одном пространстве. Если злоумышленник находит уязвимость, позволяющую выполнить системный вызов setns(), он может заставить свой процесс переключиться в пространство имен хоста (например, в net для прослушивания трафика), привязав себя к целевому inode.

    User Namespace и маппинг идентификаторов

    С точки зрения безопасности самым важным является User Namespace (пространство имен пользователей). Исторически контейнеры запускались от имени пользователя root (UID 0). Это создавало колоссальный риск: если атакующий совершал Container Escape (выход из контейнера), он оказывался на хосте с правами суперпользователя.

    User Namespace решает эту проблему путем трансляции (маппинга) идентификаторов. Механизм позволяет процессу иметь UID 0 внутри контейнера, но при этом быть непривилегированным пользователем снаружи (на хосте).

    В системах на базе Debian эта настройка опирается на файлы /etc/subuid и /etc/subgid. Рассмотрим типичную запись в /etc/subuid:

    dockremap:100000:65536

    Эта строка означает, что системному пользователю dockremap выделен пул из 65536 подчиненных идентификаторов, начиная с UID 100000.

    Когда контейнер запускается с включенным User Namespace, ядро применяет формулу смещения: UID на хосте = Базовый UID из пула + UID внутри контейнера.

    Если процесс внутри контейнера работает от имени root (UID 0), на хосте он будет выполняться с UID 100000. Если внутри контейнера процесс запущен от имени пользователя postgres (UID 70), на хосте это будет UID 100070.

    !Схема трансляции UID между хостом и контейнером

    Вектор атаки и аудит: При проведении аудита по CIS Benchmark необходимо всегда проверять, включен ли User Namespace. В Docker Daemon это требует явной настройки флага userns-remap в файле /etc/docker/daemon.json. Если эта настройка отсутствует, root в контейнере равен root на хосте. В случае эксплуатации уязвимости ядра (например, переполнения буфера в сетевом стеке), эксплойт будет выполнен с максимальными привилегиями.

    Контрольные группы (Cgroups): изоляция ресурсов

    Если Namespaces ограничивают то, что процесс видит, то Cgroups ограничивают то, что процесс может потребить. Без контрольных групп один скомпрометированный контейнер может исчерпать всю оперативную память хоста или занять 100% времени CPU, вызвав отказ в обслуживании (Denial of Service, DoS) для всех остальных сервисов.

    Современные дистрибутивы (начиная с Debian 11) по умолчанию используют Cgroups v2. В отличие от первой версии, где каждый ресурс (память, процессор, ввод-вывод) имел свою независимую иерархию, Cgroups v2 использует единую унифицированную иерархию. Это устраняет архитектурные конфликты, когда процесс мог принадлежать к разным группам для разных ресурсов.

    Иерархия Cgroups монтируется в виртуальную файловую систему по пути /sys/fs/cgroup/. Когда Docker или Podman создает контейнер, он формирует для него отдельную поддиректорию (например, /sys/fs/cgroup/system.slice/docker-<id>.scope/).

    Защита от Fork-бомб (pids.max)

    Классический вектор атаки на доступность — это Fork-бомба. Это вредоносный скрипт или программа, которая бесконечно порождает свои копии (через системный вызов fork()), пока в системе не закончатся доступные идентификаторы процессов (PID) или память.

    Без защиты один контейнер с Fork-бомбой повесит весь хост. Механизм Cgroups предоставляет контроллер pids, который ограничивает максимальное количество процессов в группе.

    Пример аудита и настройки: Если мы запускаем контейнер с флагом --pids-limit 50, Docker записывает число 50 в файл pids.max внутри директории cgroup этого контейнера.

    !Симуляция блокировки форк-бомбы механизмом pids.max

    Когда процесс внутри контейнера пытается создать 51-й процесс, ядро проверяет текущее значение в файле cgroup.procs (где перечислены все активные PID этой группы) и сравнивает его с pids.max. Обнаружив превышение, ядро блокирует системный вызов fork(), возвращая ошибку EAGAIN (Resource temporarily unavailable). Контейнер может аварийно завершиться, но инфраструктура хоста останется нетронутой.

    Вектор атаки и аудит: При анализе конфигураций (особенно в Kubernetes или при прямом запуске через Docker Compose) отсутствие лимитов на CPU, RAM и PIDs является критической уязвимостью. Рекомендуется использовать статические анализаторы (линтинг) для проверки манифестов на наличие директив deploy.resources.limits и pids_limit.

    Linux Capabilities: деконструкция суперпользователя

    Даже при включенных Namespaces и Cgroups, процесс с UID 0 внутри контейнера обладает слишком большой властью, если User Namespace не настроен (что является стандартом де-факто в большинстве legacy-инфраструктур).

    Исторически в UNIX-системах существовало бинарное разделение прав: либо вы обычный пользователь (UID > 0) и ваши права проверяются на каждом шаге, либо вы root (UID 0), и ядро обходит все проверки прав доступа. Это монолитная архитектура привилегий.

    Linux Capabilities (мандаты) разбивают всемогущество root на множество мелких, независимых привилегий (в современных ядрах их около 40). Вместо того чтобы давать процессу полный доступ, ядро позволяет выдать ему только конкретный мандат. Например, мандат CAP_NET_BIND_SERVICE позволяет процессу открывать привилегированные порты (ниже 1024), не имея при этом прав на чтение чужих файлов или перезагрузку системы.

    Наборы Capabilities (Sets)

    Чтобы ядро могло безопасно управлять мандатами при запуске новых программ, Capabilities реализованы в виде пяти независимых наборов для каждого потока (thread):

  • Permitted (Разрешенные): Максимальный набор мандатов, которые процесс может потенциально активировать.
  • Effective (Эффективные): Мандаты, которые ядро реально проверяет в данный момент времени при выполнении системных вызовов.
  • Inheritable (Наследуемые): Мандаты, которые могут быть переданы дочерним процессам при вызове execve().
  • Bounding (Ограничивающие): Жесткий лимит. Ни один процесс не может получить мандат, если его нет в Bounding set, даже если файл программы имеет SUID-бит.
  • Ambient (Окружающие): Механизм, добавленный в новых ядрах для бесшовной передачи мандатов непривилегированным дочерним процессам.
  • Для безопасности контейнеров важнейшим является Bounding set. Docker и Podman при запуске контейнера формируют этот набор, отсекая самые опасные мандаты.

    Аудит Capabilities в контейнере

    По умолчанию Docker сбрасывает (drop) большинство мандатов, оставляя базовый набор из 14 штук (таких как CAP_CHOWN, CAP_SETUID, CAP_NET_RAW).

    Как аудитор, вы должны уметь проверять, какие мандаты реально доступны процессу. Запустим контейнер Debian и прочитаем статус процесса PID 1:

    Вывод будет выглядеть примерно так:

  • CapInh: 00000000a80425fb
  • CapPrm: 00000000a80425fb
  • CapEff: 00000000a80425fb
  • CapBnd: 00000000a80425fb
  • CapAmb: 0000000000000000
  • Ядро хранит мандаты в виде 64-битной битовой маски, представленной в шестнадцатеричном формате. Чтобы расшифровать эту маску, используется утилита capsh (входит в пакет libcap2-bin):

    Вывод покажет реальные названия мандатов: cap_chown, cap_dac_override, cap_fowner, cap_fsetid, cap_kill...

    Критические мандаты и векторы атак

    При аудите Docker-инфраструктуры особое внимание уделяется поиску контейнеров, запущенных с избыточными мандатами. Рассмотрим самые опасные из них:

    1. CAP_SYS_ADMIN Это «новый root». Этот мандат дает право на выполнение вызовов mount() и umount(), управление пространствами имен (setns, unshare) и многое другое. Если атакующий захватывает контейнер с CAP_SYS_ADMIN, он может смонтировать файловую систему хоста /dev/sda1 прямо в контейнер и переписать /etc/shadow или внедрить SSH-ключ, совершив мгновенный Container Escape. Флаг --privileged в Docker автоматически выдает этот мандат.

    2. CAP_NET_ADMIN Позволяет управлять сетевыми интерфейсами, таблицами маршрутизации и правилами iptables/nftables. Скомпрометированный контейнер с этим мандатом может перехватывать трафик других контейнеров (ARP spoofing) или изменять правила фаервола хоста, открывая доступ извне.

    3. CAP_SYS_MODULE Позволяет загружать и выгружать модули ядра (kernel modules). Поскольку ядро у контейнера и хоста общее, загрузка вредоносного модуля (руткита) изнутри контейнера немедленно компрометирует весь хост на уровне кольца защиты Ring 0. Контейнер никогда не должен иметь этого мандата.

    4. CAP_NET_RAW Этот мандат включен в Docker по умолчанию. Он позволяет создавать «сырые» (raw) сокеты. Легитимное использование — утилита ping (которая использует ICMP-сокеты). Нелегитимное использование — генерация поддельных сетевых пакетов (IP spoofing), атаки типа SYN flood на внутреннюю сеть или создание скрытых каналов связи (ICMP tunneling). Best Practice: Согласно OWASP Docker Security Cheat Sheet, этот мандат необходимо отключать глобально (--cap-drop=NET_RAW), если приложение явно не требует отправки ICMP-запросов.

    Принцип наименьших привилегий на практике

    Методология защиты, описанная Лиз Райс, требует применения подхода «Запретить всё, разрешить необходимое». При запуске контейнера в production-среде строка запуска должна выглядеть так:

    В этом сценарии мы полностью обнуляем Bounding set (--cap-drop=ALL), а затем точечно добавляем только один мандат, необходимый веб-серверу для прослушивания порта 80 или 443. Даже если в веб-сервере будет найдена уязвимость удаленного выполнения кода (RCE), шелл-код атакующего унаследует пустой набор мандатов. Он не сможет изменить владельца файлов (CAP_CHOWN), не сможет убить другие процессы (CAP_KILL) и не сможет отправить ping-запрос на внешний C&C сервер (CAP_NET_RAW).

    Баланс механизмов изоляции

    Безопасность среды исполнения не достигается включением одной «галочки». Это эшелонированная оборона, где каждый слой компенсирует недостатки другого.

    Пространства имен (Namespaces) создают слепоту для процесса, скрывая от него объекты хоста. Однако, если процесс сохраняет административные мандаты (Capabilities), он может приказать ядру разрушить эти барьеры (например, через setns или монтирование). В свою очередь, даже лишенный мандатов процесс способен нанести ущерб инфраструктуре, если контрольные группы (Cgroups) не ограничивают его аппетиты к системным ресурсам.

    Понимание того, как ядро Linux оперирует inode-ссылками для пространств имен, древовидной структурой для контрольных групп и битовыми масками для мандатов, позволяет специалисту по безопасности перевести абстрактные требования стандартов CIS Benchmark в конкретные технические проверки и правила аудита.

    3. Сравнительный анализ безопасности Docker Daemon и архитектуры Rootless Podman

    Сравнительный анализ безопасности Docker Daemon и архитектуры Rootless Podman

    Аудитор безопасности находит в манифесте docker-compose.yml безобидную на первый взгляд конфигурацию тома: - /var/run/docker.sock:/var/run/docker.sock. Разработчик добавил эту строку, чтобы контейнер с CI-агентом мог собирать другие образы. На практике эта единственная строка означает полный крах изоляции хоста. Если злоумышленник найдет уязвимость в веб-приложении внутри этого CI-агента, ему не потребуются сложные эксплойты для побега через ядро (Container Escape). Достаточно отправить один HTTP-запрос к примонтированному сокету через curl, приказав демону запустить новый привилегированный контейнер с примонтированным корневым каталогом хоста /. Менее чем за секунду атакующий получает права root на физическом сервере. В среде Podman этот же вектор атаки физически невозможен.

    Разница в устойчивости к подобным атакам кроется не в реализации пространств имен или контрольных групп — обе системы используют одни и те же механизмы ядра Linux. Фундаментальное различие заключается в архитектурном дизайне: как именно создаются процессы и кто ими управляет.

    Архитектура клиент-сервер и монополия dockerd

    Docker исторически построен по классической клиент-серверной модели. Утилита командной строки docker, которую вызывает пользователь, не запускает контейнеры самостоятельно. Это лишь легковесный REST-клиент. Все команды транслируются в API-вызовы и отправляются фоновому процессу — Docker Daemon (dockerd).

    Демон dockerd работает с правами root. Он отвечает за загрузку образов, настройку сетей, создание томов и, в конечном итоге, передает инструкции высокоуровневому рантайму containerd, который вызывает низкоуровневый OCI-рантайм (обычно runc).

    !Сравнение архитектур Docker и Podman

    С точки зрения информационной безопасности такая архитектура создает массивную единую точку отказа (Single Point of Failure). Демон объединяет в себе слишком много функций, нарушая принцип единой ответственности. Ключевые риски архитектуры dockerd:

  • Монополия на сокет. Доступ к /var/run/docker.sock эквивалентен доступу к root без пароля. Любой пользователь, добавленный в группу docker, может беспрепятственно повысить свои привилегии до суперпользователя хоста.
  • Уязвимости API. Docker API представляет собой полноценный веб-сервис. Если порт демона (обычно 2375 или 2376) случайно выставлен в публичную сеть без взаимной TLS-аутентификации (mTLS), сервер будет скомпрометирован ботнетами (например, Kinsing) для майнинга криптовалют в течение нескольких минут.
  • SSRF-атаки (Server-Side Request Forgery). Если приложение внутри контейнера уязвимо к SSRF, и контейнер имеет доступ к сети хоста (host networking) или примонтированному сокету, атакующий может заставить само приложение отправить вредоносный запрос к Docker API.
  • Осознавая эти риски, разработчики Docker внедрили режим Rootless Docker. В этом режиме демон запускается от имени непривилегированного пользователя с использованием скрипта dockerd-rootless.sh. Однако это решение представляет собой надстройку над изначально монолитной архитектурой: демон всё ещё существует, он всё ещё управляет всем жизненным циклом, просто теперь он «упакован» в пользовательское пространство имен. Это влечет за собой накладные расходы на сеть и ограничения в поддержке некоторых драйверов хранения.

    Архитектура Fork-Exec: устранение единой точки отказа

    Архитектура Podman (Pod Manager) изначально проектировалась с оглядкой на безопасность и интеграцию с systemd. В ней полностью отсутствует фоновый демон. Podman использует традиционную для Unix модель fork-exec.

    Когда пользователь вводит команду podman run, процесс podman напрямую обращается к реестру образов, подготавливает файловую систему и делает системный вызов fork(), порождая дочерний процесс. Этот дочерний процесс затем вызывает exec() для запуска OCI-рантайма (в современных дистрибутивах Linux часто используется crun, написанный на C, так как он быстрее и потребляет меньше памяти в rootless-средах, чем Go-ориентированный runc).

    Отсутствие демона решает проблему единой точки отказа:

  • Нет сокета с правами root, который можно случайно примонтировать.
  • Нет постоянно висящего в памяти API-сервера, слушающего порты.
  • Контейнеры, запущенные разными пользователями, изолированы друг от друга на уровне операционной системы. Пользователь alice не видит и не может управлять контейнерами пользователя bob, так как у них нет общего демона-посредника.
  • Однако возникает инженерная проблема: если podman — это просто утилита, которая завершает работу после запуска контейнера (в режиме detached), кто будет следить за состоянием контейнера, собирать логи stdout/stderr и перехватывать код возврата (exit code) при завершении процесса?

    Для решения этой задачи в архитектуру был введен микрокомпонент conmon (Container Monitor).

    !Жизненный цикл процесса в Podman и роль conmon

    Процесс conmon создается для каждого отдельного контейнера. Его размер в оперативной памяти составляет всего несколько мегабайт. Как только OCI-рантайм инициализирует пространства имен и запускает полезную нагрузку контейнера, рантайм завершается, а conmon остается работать в качестве родительского процесса для контейнера (PID 1 внутри пространства имен). conmon не имеет прав на создание новых контейнеров или изменение конфигурации сети — его поверхность атаки микроскопична по сравнению с dockerd.

    Сетевая изоляция и хранилище в Rootless-средах

    Запуск контейнеров без прав суперпользователя требует обхода строгих ограничений ядра Linux. Непривилегированный пользователь не может выполнять две критически важные для контейнеров операции: создавать виртуальные сетевые интерфейсы (например, veth пары) и монтировать большинство файловых систем.

    Сетевой стек: от slirp4netns к pasta

    В классическом Docker с правами root демон создает мост docker0 и подключает к нему контейнеры через виртуальные Ethernet-кабели. В режиме rootless это действие заблокировано ядром (требуется мандат CAP_NET_ADMIN в первоначальном сетевом пространстве имен хоста).

    Для решения этой проблемы исторически применялся slirp4netns. Это сетевой стек TCP/IP, реализованный полностью в пространстве пользователя (user-space). Он перехватывает сетевые пакеты, исходящие из изолированного сетевого пространства имен контейнера, транслирует их в стандартные системные вызовы send() и recv() от имени пользователя на хосте, и отправляет наружу. С точки зрения безопасности slirp4netns — это компромисс:

  • Плюс: Сетевой трафик контейнера выглядит для хоста как обычный трафик пользовательского приложения. Никаких привилегий не требуется.
  • Минус: Обработка каждого пакета в user-space требует переключения контекста между ядром и пространством пользователя, что снижает пропускную способность. Кроме того, сложный парсер пакетов на C потенциально подвержен уязвимостям повреждения памяти.
  • В современных версиях Podman (начиная с версии 4.x) стандартом де-факто становится инструмент pasta (Packer to Socket Translator and Router). В отличие от slirp4netns, pasta минимизирует обработку пакетов в пространстве пользователя. Он использует системный вызов splice(), который позволяет передавать данные между файловыми дескрипторами (в данном случае — между сокетами) напрямую внутри ядра, без копирования данных в user-space. Это не только кратно повышает производительность сети в rootless-контейнерах, но и сокращает поверхность атаки, так как pasta не пытается полностью эмулировать TCP/IP стек.

    Файловые системы: fuse-overlayfs и native overlayfs

    Контейнеры используют каскадные файловые системы (OverlayFS) для объединения неизменяемых слоев образа и записываемого верхнего слоя. До версии ядра Linux 5.11 монтирование overlayfs требовало прав root.

    В ранних rootless-реализациях использовался fuse-overlayfs — реализация файловой системы через механизм FUSE (Filesystem in Userspace). Как и в случае с сетью, это означало перенос логики ядра в пользовательское пространство. Процесс FUSE работал от имени пользователя, что обеспечивало безопасность, но создавало узкое место в производительности при интенсивных операциях ввода-вывода.

    Современные ядра Linux (начиная с 5.11 для Ubuntu/Debian) разрешают использование нативного overlayfs внутри пользовательских пространств имен (User Namespaces). Podman и Rootless Docker автоматически детектируют поддержку нативного overlayfs и переключаются на него. Это устраняет необходимость в дополнительном процессе-посреднике, снижая количество движущихся частей в архитектуре и уменьшая вероятность эксплуатации уязвимостей в компонентах fuse.

    Векторы атак и пределы изоляции Rootless

    Переход на архитектуру Rootless Podman или Rootless Docker кардинально меняет модель угроз, но не делает систему абсолютно неуязвимой. Важно понимать, что именно получает злоумышленник в случае успешного Container Escape из rootless-контейнера.

    Если атакующий эксплуатирует уязвимость нулевого дня в ядре (например, в подсистеме eBPF или io_uring) и выходит за пределы контейнера, он оказывается в среде хоста. Однако, в отличие от daemon-based Docker, он не получает права root. Его привилегии ограничены правами того системного пользователя, от имени которого был запущен Podman.

    Чего атакующий не может сделать:

  • Загрузить вредоносные модули ядра (Rootkits).
  • Прочитать файлы /etc/shadow или изменить системные бинарные файлы в /usr/bin.
  • Прослушивать сетевой трафик других пользователей через tcpdump (отсутствует CAP_NET_RAW).
  • Форматировать диски или изменять правила межсетевого экрана iptables/nftables.
  • Что атакующий может сделать (векторы локального закрепления):

  • Доступ к секретам пользователя: Чтение приватных SSH-ключей (~/.ssh/id_rsa), токенов доступа к облачным провайдерам (~/.aws/credentials) или конфигураций kubeconfig.
  • Закрепление в системе (Persistence): Модификация файлов ~/.bashrc или ~/.profile для запуска вредоносного кода при следующем логине пользователя. Создание пользовательских таймеров systemd (~/.config/systemd/user/) для регулярного запуска бэкдора.
  • Горизонтальное перемещение: Использование найденных SSH-ключей для прыжков на другие серверы инфраструктуры, к которым у скомпрометированного пользователя есть доступ.
  • Таким образом, rootless-архитектура превращает критический инцидент (полная компрометация узла) в инцидент средней тяжести (компрометация локального пользователя). Для минимизации ущерба от таких атак в корпоративной среде применяются выделенные сервисные аккаунты. Каждое приложение запускается от имени уникального непривилегированного пользователя (например, svc-nginx, svc-postgres), у которого нет SSH-ключей, нет доступа к чужим директориям и запрещен интерактивный вход в систему (shell установлен в /usr/sbin/nologin).

    Выбор между Docker Daemon и Rootless Podman — это не просто выбор утилиты, это фундаментальное решение о дизайне инфраструктуры. Отказ от монолитного демона с правами суперпользователя в пользу децентрализованной модели fork-exec реализует принцип наименьших привилегий (Principle of Least Privilege) на самом базовом уровне исполнения, смещая фокус защиты с попыток «удержать злоумышленника в клетке» на математическое ограничение ущерба в случае, если клетка будет сломана.