Блог

Каким клиентам на самом деле нужна собственная VM? Многоуровневый план изоляции Docker

VM для каждого клиента — излишество. Вот как решить, какая степень изоляции нужна каждому арендатору, и автоматизировать это решение.

Резюме

Агентства часто паникуют, когда клиент спрашивает, насколько изолированы их данные от других арендаторов. Пространства имён и cgroups Docker дают реальную изоляцию, но это не то же самое, что аппаратная граница. Вместо того чтобы запускать каждого клиента на VM — или, что хуже, относиться ко всем клиентам одинаково — создайте небольшой набор уровней изоляции и подбирайте уровень для каждого клиента по чувствительности данных, доверию и требованиям соответствия. Заблокированный контейнер (не-root, отброшенные capabilities, seccomp, read-only root) покрывает большинство сайтов; регулируемые или враждебные нагрузки получают VM или гибрид «контейнер в VM». В этой статье приводится повторяемый процесс принятия решений, таблица сравнения и честный взгляд на то, когда дополнительная изоляция излишня.

Бывало ли у вас на продающем звонке, когда новый клиент говорит: «Мы — медицина, покажите, что наши данные изолированы от ваших других клиентов», а вы бы предпочли говорить о чём угодно другом?

Это проблема агентства: не одно идеальное развёртывание, а одно и то же надёжное развёртывание, повторённое для дюжины клиентов с разными бюджетами, профилями рисков и требованиями соответствия. Вот честная версия. Изоляция Docker реальна, но она специфична. Пространства имён дают каждому контейнеру собственный взгляд на процессы, сеть и файловую систему; cgroups ограничивают CPU, память и дисковый ввод-вывод, чтобы арендаторы не могли «заморить» друг друга голодом. Чего это вам не даёт — так это аппаратной стены между контейнером и ядром хоста. Если злоумышленник выберется из контейнера, он окажется внутри единственного ядра, которое у вас есть. Остальная часть этой статьи превращает этот неприятный факт в повторяемое решение: классифицируйте каждого клиента по чувствительности данных и доверию, применяйте базовый профиль защиты и прибегайте к VM только тогда, когда стоимость утечки выше стоимости VM.

Подождите, разве контейнеры уже не изолированы?

Docker работает на пространствах имён и cgroups Linux, и эти слова выполняют реальную работу. Пространства имён разделяют идентификаторы процессов, сетевые стеки, точки монтирования и пользователей, чтобы процесс в одном контейнере не мог видеть таблицу процессов другого. Cgroups устанавливают лимиты: дайте контейнеру 0,5 CPU, 512 МБ памяти и фиксированный вес ввода-вывода, и он получит ровно это. Вышедший из-под контроля цикл у одного арендатора будет ограничен, а не обрушит соседа. Если вы не настроили лимиты, вы пропустили самое основное, для чего нужны cgroups.

Возьмём простое PHP-приложение в контейнере A. Оно видит собственную файловую систему, собственный сетевой интерфейс, свой PID 1. Контейнер B имеет то же самое, но иное представление. Это пространства имён. Теперь отойдите и пропустите лимит памяти: контейнер A может заполнить всю память хоста и заставить контейнер B еле ползти. Именно для предотвращения этого существуют cgroups. Но два контейнера могут быть изолированы друг от друга пространствами имён и при этом разделять ядро хоста — именно об этом все истории о побеге из контейнера. Эксплойт, достигающий ядра, потенциально может достичь каждого арендатора на этом хосте.

Фраза «Docker изолирован» наполовину правдива. Точная версия: «Docker изолирует с помощью пространств имён и cgroups, а уязвимость ядра — это радиус поражения». Прежде чем доверить арендатору выполнение недоверенного кода, посидите с этой мыслью минуту. Ответ не в том, чтобы «никогда не использовать контейнеры» — это лёгкая паника. Ответ — система уровней.

Так почему некоторым клиентам нужно больше, чем пространства имён?

Честный ответ: изоляция — это не переключатель, а спектр. На одном конце у вас полностью общий контейнер, где все фактически находятся в одном приложении. На другом конце — отдельная VM для каждого арендатора со своим ядром. Большинство агентских проектов находится в неудобной середине, и середина — это не бинарный выбор между «Docker подходит» и «запустите VM для всех».

Что сдвигает клиента вправо — не его размер. Это четыре вопроса:

  • Хранят ли они регулируемые данные? Медицинские записи, данные платёжных карт, всё, что регулятор назвал бы чувствительным.
  • Существует ли реалистичный путь утечки с их арендатора на другого арендатора? Если они могут запускать произвольный код, то да.
  • Доверяете ли вы коду и людям, которые его развёртывают? Клиент, который нанимает самого дешёвого фрилансера, — это не тот же уровень доверия, что клиент, чью команду разработчиков вы знаете.
  • Упоминается ли в их договоре «выделенный», «изолированный» или «частный»? Если да, вы уже пообещали уровень; осталось только выбрать правильный.

Если вы ещё не можете ответить на эти вопросы, поместите клиента в базовый уровень и запишите предположения. Это не аудит безопасности; это проверка на вменяемость, которую вы повторяете при каждом онбординге.

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

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

Позиция клиентаЧто их реально разделяетКогда использовать
Уровень 1: Общее приложение/контейнерТолько логика приложенияВнутренние утилиты, низкорисковые данные, проекты, где все явно в одной системе входа
Уровень 2: Тот же хост, отдельные контейнерыПространства имён и cgroupsБольшинство маркетинговых сайтов, контактные формы, нет чувствительных данных
Уровень 3: Заблокированный контейнерУровень 2 + не-root, удалённые capabilities, seccomp, read-only root, сегментация сетиЭлектронная коммерция, PII, пользовательский код, которому вы не совсем доверяете
Уровень 4: VM на арендатораГипервизор и отдельное ядроЗдравоохранение, финансы, документы для соответствия, недоверенный код, шумные соседи

Вот как это работает на практике. Клиент-пекарня с контактной формой и ссылкой на Instagram попадает на Уровень 2: один контейнер на общем хосте, стандартная сеть Docker, лимиты ресурсов — дело сделано. Интернет-магазин, который хранит имена клиентов, адреса и перенаправления платежей, попадает на Уровень 3: тот же общий хост, но контейнер работает от пользователя non-root, не имеет лишних capabilities ядра, использует профиль seccomp и открывает только порт 443. Медицинский портал приёма пациентов, который хранит защищённую медицинскую информацию, попадает на Уровень 4: VM для каждого арендатора, потому что стоимость утечки — не «мы всё почистим», а «мы не можем показать клиенту, что отнеслись к нему серьёзно».

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

Как на самом деле выглядит заблокированный контейнер?

Давайте перестанем говорить «заблокирован» и перейдём к конкретике. Вот что Уровень 3 означает для типичного клиента на WordPress или PHP.

Во-первых, смените пользователя. Большинство официальных образов по умолчанию запускаются от root; в вашем Dockerfile создайте пользователя non-root и запускайте приложение от этого пользователя. Это сразу устраняет самый распространённый путь, при котором компрометация контейнера становится компрометацией хоста. Во-вторых, отбросьте ненужные capabilities. Запускайте с --cap-drop ALL и добавьте обратно только одну, обычно NET_BIND_SERVICE, чтобы приложение могло слушать порт 80. Уже это — более серьёзное изменение, чем большинство ожидает. В-третьих, сделайте корневую файловую систему доступной только для чтения с помощью --read-only и смонтируйте каталоги для записи (загрузки, каталог данных базы данных) как тома или tmpfs. В-четвёртых, примените профиль seccomp и, если ваш хост это поддерживает, AppArmor или SELinux. Наконец, поместите контейнер в выделенную сеть Docker и откройте только те порты, которые действительно должны быть доступны.

Давайте рассмотрим пример с WordPress. Базовый образ, вероятно, запускается от root, поэтому вы добавляете шаг useradd и директиву USER. Вы запускаете контейнер с лимитом памяти и лимитом CPU, чтобы всплеск трафика от плагинов не навредил соседу. Вы монтируете /var/www/html/wp-content/uploads как том для записи. Вы устанавливаете --read-only. Вы подключаете его к сети, в которой рядом нет флага --privileged. В результате контейнер, который раньше был «сайтом на WordPress», теперь «сайт на WordPress, который оказывается более заблокированным, чем большинство виртуальных частных серверов».

Если создавать всё это вручную кажется ненадёжным, есть более лёгкий средний путь: расширенная изоляция контейнеров Docker, которая использует изоляцию пользовательских пространств имён и безопасную среду выполнения контейнеров. Это законный ярлык, но не бесплатный пропуск, позволяющий пропустить non-root или отбрасывание capabilities. Арендатору всё равно нужен разумный образ. Разница в том, что поверхность атаки на ядро становится меньше, не требуя от вас стать экспертом по seccomp за одну ночь. Если вам нужна точная последовательность для одного арендатора, пошаговое руководство по укреплению изоляции превращает этот раздел в команды копирования-вставки.

Когда прекращать наслаивать и просто выдавать им VM?

Вот противоречащая интуиции часть: дополнительная изоляция не всегда лучше. VM дают аппаратную изоляцию, отдельное ядро и гораздо меньшую поверхность атаки в случае падения гостевого ядра. Именно этого ожидают клиенты из сфер здравоохранения и финансов, когда говорят: «Мы хотим быть изолированы». Но каждая VM добавляет затраты на патчи, резервное копирование и вычисления, а также умножает работу по поддержанию парка обновлённым. Если вы выдаёте VM каждому клиенту только потому, что один клиент как-то сказал, что Docker его пугает, вы купили театр безопасности за реальные деньги.

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

Но VM не оправдывает небрежный контейнер. Распространённая ловушка — поместить клиента в VM и затем пропустить укрепление, потому что «VM их защищает». VM защищает хост от арендатора, а не арендатора от его собственного плохого образа. Внутри этой VM вам по-прежнему нужны non-root, отброшенные capabilities и seccomp. Гибридный подход — контейнеры внутри VM — часто является лучшим вариантом: VM обеспечивает границу для разговоров о соответствии, а контейнер даёт вам уже знакомый процесс развёртывания. Более длинная версия этой дискуссии — в статье Должен ли каждый арендатор получать собственную VM?, но короткий ответ: VM нужна для договора, а не для страха.

Как сделать это повторяемым для каждого клиента?

Вы делаете это повторяемым, превращая систему уровней в шаблон, а не в память. Храните каталог Compose-файлов, по одному на уровень: tier2-baseline, tier3-locked, tier4-vm-hybrid. Когда появляется новый клиент, скопируйте шаблон, измените переменные окружения — и вы уже знаете форму изоляции, не написав ни строчки новой инфраструктуры.

Затем запишите решение. Не отчёт по безопасности на 400 страниц, а короткий абзац в репозитории клиента: какие данные они хранят, на каком уровне находятся, почему и что могло бы перевести их на уровень выше. Этот абзац стоит больше сотни правил файрвола, потому что это то, что вы можете показать следующему аудитору или следующему обеспокоенному клиенту. Это также избавляет вас от необходимости вспоминать, почему пекарня получила Уровень 2, а интернет-магазин — Уровень 3, когда первоначальный звонок уже стёрся из памяти.

Автоматизируйте скучные проверки. Пусть ваш CI сканирует каждый образ клиента и блокирует сборку, если она запускается от root, имеет все capabilities или пытается опубликовать порт, отличный от разрешённых уровнем. Ничего экзотического; это просто гарантия того, что шаблон случайно не сломан благонамеренным разработчиком. Если вы всё равно создаёте сопутствующий процесс хостинга, статья о стратегиях Docker-хостинга для продакшена покрывает часть, которая идёт после определения контейнеров.

Ничто из этого не гламурно. Ни одна запись в блоге не сделает «изоляцию арендаторов» такой же захватывающей, как диаграмма архитектуры с нуля. Но это разница между агентством, которое отвечает на вопрос «насколько мы изолированы?» со скрещёнными пальцами «полностью», и агентством, которое может показать уровень, конфигурацию и причину. Контейнеры — не волшебная стена. VM — не серебряная пуля. Система уровней — это просто решение, которое вы записываете и используете повторно — и для агентства повторяемость — это вся игра.

Sources (5)