Блог

Заблуждение о доверии: как наша мультиарендная настройка Docker привела к утечке данных (и как мы это исправили)

Узнайте, как наивная настройка Docker одной команды привела к межарендной утечке данных и какая многослойная стратегия изоляции предотвратила её.

Краткое содержание

Контейнеры Docker по умолчанию не изолированы — они используют ядро хоста, и без целенаправленной настройки арендаторы могут мешать друг другу. В этой статье рассматривается реальный сценарий, в котором провайдер мультиарендного хостинга обнаружил, что клиентские контейнеры могут получить доступ к базам данных друг друга из-за общего сетевого окружения и слабых настроек безопасности по умолчанию. Мы показываем пошаговые изменения, которые устранили уязвимость: пользовательские сети для каждого арендатора, запуск от непривилегированного пользователя, удаление избыточных возможностей, файловые системы только для чтения и профили seccomp. Распространенное заблуждение — считать, что контейнеры по своей сути обеспечивают надежную изоляцию; мы оспариваем это, объясняя, почему виртуальные машины все еще обеспечивают более жесткую границу и когда следует рассмотреть гибридный подход. Заключение подчеркивает, что изоляция — это многослойный процесс, а не единичная настройка.

Инцидент: когда контейнеры слишком много говорят

Вы настроили Docker на одном хосте для запуска нескольких клиентских веб-сайтов. У каждого клиента свой контейнер — аккуратная изолированная среда, верно? Мы тоже так думали. Пока плановая проверка безопасности не показала, что контейнер Клиента А читал сокет MySQL контейнера Клиента Б на том же хосте. Они использовали общую сеть bridge. Хуже того, контейнеры работали от root, поэтому злоумышленник, взломавший один, мог вмешаться в Docker-сокет хоста или файловую систему другого контейнера. Уязвимость не была сложной эксплуатацией; это была базовая ошибка конфигурации. Данные утекли. Доверие исчезло.

Такой сценарий неудачи не редкость. Многие команды предполагают, что пространства имен и cgroups Docker автоматически изолируют арендаторов, но они недооценивают, сколько лазеек остается открытыми по умолчанию. Стандартные сети bridge не обеспечивают сетевой изоляции между контейнерами. Запуск от root дает контейнеру больше возможностей, чем нужно. А без явных ограничений ресурсов один шумный сосед может лишить другие контейнеры процессора или памяти.

Шаг 1: прекратите использовать одну сеть

Нашим первым исправлением было предоставление каждому арендатору собственной пользовательской сети Docker. Это предотвращает доступ контейнеров друг к другу, если вы явно не соедините их. Мы создали сценарий, который для каждого арендатора разворачивает выделенную сеть и подключает к ней его контейнер приложения. Контейнер базы данных находится в той же сети арендатора, но мы также добавили внутреннюю сеть только для внутриарендного общения. Больше никакого межарендного подглядывания.

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

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

Шаг 2: избавьтесь от ненужных привилегий

По умолчанию контейнеры Docker работают с ограниченным набором возможностей Linux, но их все же больше, чем нужно большинству приложений. Наши контейнеры работали от root, что позволяло процессам внутри выполнять такие действия, как монтирование файловых систем или изменение параметров ядра. Мы переключились на запуск приложения от непривилегированного пользователя внутри контейнера (с помощью директивы USER в Dockerfile) и удалили все возможности, кроме абсолютно необходимых. Для типичного веб-приложения это могут быть только NET_BIND_SERVICE (для привязки к портам ниже 1024) и CHOWN (для записи в каталоги). Мы также добавили --security-opt no-new-privileges для предотвращения повышения привилегий.

Только этот шаг устранил множество распространенных векторов побега из контейнера. Злоумышленник, скомпрометировавший веб-сервер, не может устанавливать пакеты, изменять системные двоичные файлы или получать доступ к Docker-сокету хоста, поскольку процессу не хватает возможностей CAP_SYS_ADMIN или CAP_DAC_OVERRIDE.

Шаг 3: заблокируйте файловую систему

Записываемые файловые системы — распространенная поверхность атаки. Мы сделали корневую файловую систему доступной только для чтения (--read-only) для всех контейнеров, а затем смонтировали временные файловые системы (tmpfs) для каталогов, которым требуется доступ на запись, таких как /tmp и каталог кэша приложения. Это предотвращает изменение кода приложения или сохранение вредоносных двоичных файлов злоумышленником.

Кроме того, мы использовали параметр --mount Docker для монтирования чувствительных каталогов, таких как Docker-сокет, только когда это абсолютно необходимо, и никогда — в производственных контейнерах. Принцип: если контейнеру не нужно записывать в путь, сделайте его доступным только для чтения.

Шаг 4: примените профили Seccomp и AppArmor

Стандартные профили seccomp уже блокируют многие опасные системные вызовы, но мы дополнительно настроили их, чтобы разрешить только те системные вызовы, которые действительно нужны нашему приложению. Это компромисс, так как требует профилирования приложения. Более простой подход — использовать стандартный профиль seccomp Docker, а затем добавить --security-opt seccomp=путь/к/профилю.json, если нужны более строгие правила. Аналогично, профили AppArmor могут ограничивать процессы контейнера определенными путями к файлам и возможностями. Мы включили AppArmor и использовали пользовательский профиль, ограничивающий доступ только к каталогам данных приложения.

Для подробного руководства по этим шагам усиления см. Усиление контейнеров Docker для мультиарендного хостинга: пошаговое руководство по изоляции.

Противоположное мнение: иногда нужны виртуальные машины

Как бы сильно вы ни усиливали защиту, контейнеры используют общее ядро хоста. Уязвимость ядра может мгновенно нарушить всю изоляцию. Вот почему многие платформы, ориентированные на безопасность, запускают контейнеры внутри легковесных виртуальных машин — каждый арендатор получает собственное ядро. Это добавляет накладные расходы, но обеспечивает аппаратную границу, которую не могут дать одни контейнеры. Если ваши арендаторы обрабатывают данные кредитных карт или медицинские записи, гибридный подход (контейнеры внутри виртуальных машин) может быть правильным решением. Не предполагайте, что изоляции контейнеров достаточно для вашей модели угроз; оцените чувствительность данных и нормативные требования.

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

Заключение: изоляция — это стек, а не переключатель

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

Sources (5)