MAATRIX / Блог / gVisor и Kata: контейнеры с изоляцией уровня ВМ

gVisor и Kata: контейнеры с изоляцией уровня ВМ

MAATRIX

Если вы запускаете код от чужих пользователей — сборки в CI/CD, ноутбуки в мультитенантной ML-платформе, плагины в SaaS-конструкторе — обычный docker run рано или поздно вызывает вопрос: а что если этот код найдёт уязвимость в ядре хоста? Контейнеры делят одно ядро с хостом и всеми соседями, и это не баг, а архитектура. Два разных проекта — gVisor от Google и Kata Containers — решают эту проблему принципиально разными способами, но оба сохраняют привычный интерфейс: снаружи это всё ещё docker run или под в Kubernetes.

Почему обычного контейнера здесь недостаточно

Контейнер — это не виртуальная машина с собственным ядром, а изолированный через namespaces и cgroups процесс на общем ядре хоста. Это лёгкая и быстрая изоляция, но её граница — сам код ядра Linux, а не аппаратный барьер. Если в обработчике системного вызова, файловой системе или сетевом стеке ядра есть уязвимость, процесс внутри контейнера при определённых условиях может её эксплуатировать и выбраться за пределы своего namespace — это называется container breakout. Мы разбирали механику этого заблуждения подробнее в статье про миф о том, что контейнер — это песочница: контейнер прекрасно решает задачи упаковки зависимостей и повторяемости окружения, но как границу безопасности для по-настоящему недоверенного кода использовать его напрямую рискованно.

Отсюда два практических пути. Можно давать каждому недоверенному воркеру полноценную виртуальную машину — так исторически поступали облачные провайдеры и CI-системы, но это дорого по ресурсам, медленно по времени старта (секунды против миллисекунд у контейнера) и плохо ложится на привычный workflow с Docker-образами и Kubernetes-манифестами. А можно взять сам контейнер и добавить ему дополнительный барьер изоляции, не меняя интерфейс запуска. Именно так поступают gVisor и Kata — но добавляют барьер в разных местах стека.

gVisor: userspace-ядро между контейнером и хостом

gVisor — проект Google, изначально созданный для изоляции нагрузок в Google Cloud (в частности, App Engine и Cloud Run). Идея: вместо того чтобы давать процессу контейнера напрямую дёргать системные вызовы настоящего ядра Linux, между процессом и хостом ставится прослойка — компонент под названием Sentry, реализованный полностью в userspace на языке Go. Sentry перехватывает системные вызовы контейнеризованного процесса через ptrace или через более быстрый механизм KVM-перехвата (platform kvm) и обрабатывает их сам: реализует собственную упрощённую реализацию файловой системы, сети, управления памятью и процессами — по сути, urezannoe ядро, написанное не на C и не выполняющееся в кольце 0.

Ключевой эффект: контейнеризованный процесс физически не имеет прямого канала к реальному ядру хоста. Даже если в коде процесса есть эксплойт под конкретную уязвимость ядра Linux, он просто не долетает до настоящего ядра — его перехватывает и переинтерпретирует Sentry. Это резко сокращает поверхность атаки: вместо тысяч системных вызовов ядра Linux, каждый со своей историей CVE, атакующий видит куда более узкий и написанный с нуля с оглядкой на безопасность интерфейс.

Практически gVisor подключается как runtime для containerd/Docker — runsc (run sandboxed container):

# Установка runsc (пример для Debian/Ubuntu)
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt-get update && sudo apt-get install -y runsc

# Регистрация runtime в Docker
sudo runsc install
sudo systemctl restart docker

# Запуск контейнера в gVisor вместо runc
docker run --runtime=runsc -it ubuntu bash

Внутри такого контейнера uname -a покажет специфичную строку gVisor вместо реальной версии ядра хоста — это и есть видимый след того, что syscalls обслуживает не Linux, а Sentry.

Плата за это — накладные расходы на каждый перехваченный системный вызов: путь удлиняется, добавляется контекстное переключение и обработка в userspace-ядре. Для нагрузок с интенсивным I/O или частыми syscalls (например, база данных с активной записью на диск) это ощутимо; для типичного веб-сервиса, который делает не так много syscalls на запрос, разница часто малозаметна. Точный процент оверхеда сильно зависит от профиля нагрузки и версии gVisor — не берёмся называть конкретные цифры, это стоит измерять на своём коде.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Kata Containers: контейнер внутри лёгкой VM

Kata Containers — проект под эгидой OpenStack Foundation (вырос из слияния Intel Clear Containers и Hyper runV) — решает ту же задачу принципиально иначе. Вместо userspace-прослойки поверх ядра хоста Kata запускает каждый под или контейнер внутри отдельной лёгкой виртуальной машины со своим собственным настоящим ядром гостя. Для этого используется гипервизор — либо облегчённый Firecracker, либо QEMU с KVM, либо Cloud Hypervisor.

Если вы уже разбирались с Firecracker как основой лёгких microVM, то Kata здесь — это ровно тот сценарий, где эта технология применяется не напрямую (как в serverless-платформах), а как runtime внутри контейнерной экосистемы: Kata поднимает microVM, внутри неё — минималистичное гостевое ядро и агент (kata-agent), который принимает от containerd те же команды, что обычно уходят в runc, и выполняет их уже внутри VM.

Разница с gVisor принципиальная: у Kata-контейнера есть настоящее отдельное ядро гостевой VM. Уязвимость в ядре хоста не помогает атакующему напрямую — ему сначала нужно пробить границу гипервизора (аппаратную виртуализацию), а это исторически куда более узкая и реже эксплуатируемая поверхность, чем сам код ядра Linux. Это не userspace-эмуляция системных вызовов — это полноценная, пусть и урезанная, виртуализация с honest-то независимым ядром.

Подключение похоже на gVisor — через containerd/CRI как отдельный runtime:

# Установка Kata Containers (пример через скрипт-установщик проекта)
curl -fsSL https://raw.githubusercontent.com/kata-containers/kata-containers/main/utils/kata-manager.sh | bash -s -- install

# Проверка окружения (виртуализация должна быть доступна: /dev/kvm)
kata-runtime check

# Регистрация в containerd (пример фрагмента config.toml)
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
  runtime_type = "io.containerd.kata.v2"
# Запуск через containerd/crictl с runtime-классом kata
# или напрямую через ctr:
sudo ctr run --runtime io.containerd.kata.v2 -t docker.io/library/ubuntu:22.04 test-kata bash

В Kubernetes это выглядит как отдельный RuntimeClass:

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata
handler: kata
---
apiVersion: v1
kind: Pod
metadata:
  name: untrusted-workload
spec:
  runtimeClassName: kata
  containers:
  - name: app
    image: myregistry/untrusted-app:latest

Важное требование: узел должен поддерживать аппаратную виртуализацию (вложенная виртуализация, если вы уже внутри VM — например, на арендованном VPS — тоже возможна, но требует, чтобы провайдер явно включил nested virtualization и передал /dev/kvm внутрь гостя; на обычном VPS без этой опции Kata просто не запустится).

Оверхед и производительность: практическое сравнение

Общее правило (без точных цифр, которые нужно проверять на своей нагрузке): gVisor обычно легче по накладным расходам, потому что не поднимает полноценную VM — не тратится время и память на загрузку гостевого ядра, инициализацию виртуального устройства и виртуальной сети. Время старта контейнера в gVisor ближе к обычному Docker-контейнеру. Kata платит за более сильную изоляцию временем на загрузку microVM (обычно доли секунды у Firecracker-backend, заметно больше у QEMU) и дополнительным потреблением памяти на гостевое ядро и его сервисы — это фиксированный "налог" на каждый под, который нужно закладывать в capacity planning отдельно от самого приложения.

КритерийgVisorKata Containers
Механизм изоляцииUserspace-ядро (Sentry) перехватывает syscallsОтдельная лёгкая VM (Firecracker/QEMU/KVM) с своим ядром
Реальное ядро хостаОбщее, но контейнер не имеет к нему прямого доступаИзолировано границей гипервизора
Оверхед на syscall-интенсивных нагрузкахЗаметный (каждый syscall перехватывается)Минимальный внутри VM (обычные syscalls к своему ядру)
Оверхед на старте / памятиНизкий, близко к обычному контейнеруВыше: загрузка гостевого ядра и агента
Требования к хостуРаботает без аппаратной виртуализации (ptrace-режим) или с ней (kvm-режим)Требует KVM / аппаратную виртуализацию (/dev/kvm)
Совместимость приложенийОграничена набором поддерживаемых syscallsПолная — внутри настоящее Linux-ядро
Типичный кейсServerless-платформы, sandboxing одиночных функцийCI-раннеры, мультитенантный Kubernetes, недоверенные поды

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

Совместимость и грабли: где ломается

У gVisor основная грабля — не все системные вызовы Linux реализованы или реализованы не на 100% идентично настоящему ядру. Sentry поддерживает подавляющее большинство распространённых syscalls, но приложения, которые лезут в редкие углы API ядра — специфичные ioctl, некоторые режимы io_uring, отдельные сетевые опции, прямую работу с определёнными файловыми системами или модулями ядра — могут падать с ENOSYS или вести себя иначе, чем на обычном runc. Практический совет: прежде чем переводить прод-нагрузку на gVisor, прогоните её тестовый набор именно под --runtime=runsc — если приложение использует что-то экзотическое, это вскроется быстро.

У Kata грабля другого рода — не про совместимость системных вызовов (там настоящее ядро, всё работает как обычно), а про инфраструктурные требования. Нужна аппаратная виртуализация на хосте, что на некоторых бюджетных VPS без вложенной виртуализации просто недоступно — арендуя сервер под Kata, уточняйте у провайдера поддержку nested virtualization заранее. Вторая грабля — проброс GPU и других PCI-устройств внутрь Kata-контейнера сложнее, чем в обычный Docker: нужен VFIO-passthrough, который работает не со всяким железом. Третья — сетевая модель Kata сложнее (виртуальные NIC внутри microVM плюс фильтрация на уровне хоста), из-за чего некоторые CNI-плагины Kubernetes требуют отдельной поддержки Kata — проверяйте совместимость конкретного плагина перед миграцией прод-кластера.

Общая для обоих подходов грабля — мониторинг и отладка становятся не такими прозрачными: strace на хосте ведёт себя иначе против gVisor-контейнера (там уже есть своя прослойка перехвата), а внутрь Kata-контейнера классические host-side инструменты профилирования просто не видят, потому что процесс живёт в отдельной VM — нужны средства, которые понимают эту границу, либо диагностика изнутри самого контейнера.

Где это реально нужно на практике

Обе технологии решают одну и ту же бизнес-задачу: запуск кода, которому вы не доверяете полностью, но которому по-прежнему хочется дать интерфейс "обычный контейнер", а не "выделенная VM на каждый чих". Типичные сценарии:

  • Мультитенантные SaaS-платформы, где клиенты загружают свой код (плагины, скрипты, кастомную бизнес-логику) и он выполняется на общей инфраструктуре рядом с кодом других клиентов.
  • CI/CD-раннеры, выполняющие пул-реквесты от внешних контрибьюторов или сборки с произвольными скриптами — классический вектор атаки на GitHub Actions self-hosted раннеры и аналогичные системы. Если у вас уже есть VPS под разработку и CI/CD, усиление изоляции раннеров — логичный следующий шаг, когда среди задач появляются сборки от внешних участников.
  • Serverless и FaaS-платформы, где одна физическая машина обслуживает функции множества разных клиентов, а холодный старт должен оставаться быстрым — именно этот кейс и был исходной мотивацией для gVisor в Google Cloud.
  • Kubernetes-кластеры с недоверенными подами — например, внутренняя PaaS-платформа, где разработчики сами описывают манифесты, а платформа не может гарантировать, что там нет случайного или намеренного эксплойта.

Если вы уже задумывались про Docker или LXC для сервера или настраивали изоляцию сервисов через Docker для безопасности, gVisor и Kata — следующий уровень того же вопроса: они не заменяют базовую гигиену (непривилегированные пользователи в контейнере, seccomp-профили, минимальные образы), а добавляют границу поверх неё именно там, где угроза — недоверенный код, а не собственное неаккуратно написанное приложение.

Важно и обратное: если весь код в контейнерах — ваш собственный, проверенный, без внешних загрузок произвольных данных для исполнения, дополнительная изоляция часто избыточна — она добавляет операционную сложность (два runtime вместо одного, отдельный мониторинг, отдельное тестирование совместимости) без реальной пользы. gVisor и Kata — это инструмент под конкретную угрозу, а не универсальное "давайте на всякий случай".

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли использовать gVisor и Kata одновременно на одном кластере?

Да, Kubernetes поддерживает несколько RuntimeClass на узле — можно держать runc для доверенных подов и gVisor/Kata для недоверенных, выбирая нужный через runtimeClassName.

Что проще внедрить в существующую инфраструктуру на Docker?

gVisor обычно проще — не требует аппаратной виртуализации в базовом ptrace-режиме. Kata требует поддержки KVM и аккуратной проверки сетевого стека и CNI-плагинов перед миграцией.

Теряется ли изоляция полностью, если атакующий найдёт баг в самом Sentry или в гипервизоре Kata?

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

Подходит ли gVisor или Kata для баз данных и других I/O-интенсивных сервисов?

Технически да, но именно там оверхед обеих технологий ощущается сильнее всего — стоит тестировать конкретную нагрузку заранее.

Нужна ли вложенная виртуализация, если инфраструктура уже работает на VPS, а не на bare metal?

Для Kata — да, обязательно: без неё /dev/kvm внутри гостевой VPS недоступен. Для gVisor в ptrace-режиме не требуется, но опциональный kvm-режим gVisor тоже её просит.

Что выбрать для одиночного проекта без мультитенантности и чужого кода?

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

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →