Sergei Ozeranskii
Kata Containers: как каждый CI-job получает собственное ядро
Контейнер — это процесс на хосте, изолированный средствами ядра: namespaces, cgroups, capabilities, seccomp. Ядро при этом одно на всех — общее ядро хоста. Для CI это неудобная граница: job выполняет чужой, по сути недоверенный код (шаги сборки, зависимости из реестров, а часто и Docker-in-Docker), и вся эта конструкция стоит на том же ядре, что и соседние job-ы и сам хост. Один эксплойт ядра — и граница контейнера перестаёт что-либо значить.
Kata Containers меняет саму границу: каждый контейнер (в терминах Kubernetes — pod) исполняется внутри отдельной лёгкой виртуальной машины со своим гостевым ядром. Граница изоляции — не набор ядерных примитивов, а аппаратная виртуализация. При этом снаружи Kata выглядит как обычный OCI-runtime: те же образы, тот же containerd, тот же workflow.
Разберём, как это устроено в Kata 3.x, и почему это правильный уровень изоляции для CI.
Что такое Kata Containers
Kata Containers (открывается в новой вкладке) — open-source проект (сейчас под управлением OpenInfra / Linux Foundation), который «запускает лёгкую виртуальную машину и использует гостевое Linux-ядро для создания контейнерных нагрузок». Идея — закрыть исторический разрыв между двумя подходами:
- Контейнеры — быстрые и лёгкие, но делят ядро хоста (слабая граница изоляции).
- Виртуальные машины — сильная граница (свой гостевой kernel, аппаратная виртуализация), но тяжёлые и медленные в запуске.
Kata берёт свойство VM (отдельное ядро + аппаратная граница) и упаковывает его так, чтобы по весу и скорости запуска приблизиться к контейнеру — отсюда термин Лёгкая виртуальная машина: сильная граница VM (своё ядро, аппаратная виртуализация), но по весу и скорости старта близка к контейнеру..
Kata OCI-совместим. Это тот же стандарт, что у Docker-контейнеров, и он же лежит под CRI (Container Runtime Interface) в Kubernetes. Пересобирать образы не нужно — Kata работает как ещё один runtime под containerd или CRI-O. Для оркестратора это просто другой класс среды выполнения, а не другой формат контейнеров.
Как это устроено внутри
За то, что снаружи это выглядит контейнером, а внутри работает VM, отвечают несколько компонентов.
Hypervisor и гостевая VM
Kata поднимает VM через hypervisor поверх аппаратной виртуализации Kernel-based Virtual Machine — механизм аппаратной виртуализации в ядре Linux, поверх которого работают VMM, поднимаемые Kata.. В Kata 3.x поддерживается несколько Virtual Machine Monitor (hypervisor) — процесс, который создаёт и обслуживает VM. В Kata это QEMU, Cloud Hypervisor, Firecracker или Dragonball., и все они работают через KVM:
| VMM | Язык | Профиль |
|---|---|---|
| QEMU | C | Самый зрелый, максимум архитектур (x86_64, aarch64, ppc64le, s390x) и возможностей |
| Cloud Hypervisor | Rust | Современный, cloud-ориентированный; изменение CPU/памяти на лету, device passthrough |
| Firecracker | Rust | Минималистичный, «серверлесс»: быстрый старт, минимум overhead — ценой части функций (нет проброса virtio-fs, VFIO, hotplug) |
| Dragonball | Rust | Встроен прямо в Rust-runtime Kata: VMM работает в том же процессе, нулевой IPC-overhead |
У каждого VMM свои trade-offs между зрелостью, скоростью старта, overhead и набором функций. Выбор конкретного VMM — вопрос эксплуатации; технология позволяет любой из них.
Внутри VM работает своё гостевое ядро и лёгкий гостевой образ. Гостевой rootfs подключается эффективно, через Direct Access — гипервизор отображает гостевой образ прямо в память гостя (zero-copy), экономя память и время загрузки.-маппинг (zero-copy), что сокращает и память, и время старта — micro-VM грузится не как полноценная VM.
kata-agent внутри VM
В гостевой VM живёт kata-agent — долгоживущий процесс на Rust. Он управляет контейнерами внутри VM: создаёт их с нужными cgroups и namespaces (то есть привычная контейнерная изоляция никуда не девается — она просто применяется внутри VM, поверх уже аппаратной границы) и следит за жизненным циклом нагрузки.
Runtime на хосте общается с агентом по протоколу Компактный протокол в духе gRPC для низкоуровневого IPC, без HTTP/2 — рассчитан на минимальные накладные расходы. поверх VSOCK — виртуального сокета между хостом и VM. Обычной сети для управляющего канала не требуется.
Runtime и shim: containerd-shim-kata-v2
Со стороны хоста Kata реализована как Containerd Runtime V2 (Shim API): один долгоживущий процесс-shim на pod вместо запуска runtime на каждую операцию.-runtime — бинарник containerd-shim-kata-v2
(реализует Containerd Runtime V2 / Shim API). Вместо того чтобы вызывать runtime отдельным
процессом на каждую операцию, containerd создаёт один сокет и общается с одним экземпляром shim
на pod. Именно shim отвечает за запуск hypervisor и его VM и за связь с агентом.
virtio: как VM «дотягивается» до нужного
Гостевая VM изолирована, но контейнеру нужны файлы, сеть и I/O. Их пробрасывают паравиртуальные устройства virtio:
- virtio-fs — пробрасывает OCI-bundle (rootfs контейнера) в директорию внутри гостевого rootfs.
- virtio-net / vhost-net — сеть.
- virtio-block / virtio-scsi — блочные устройства.
- virtio-vsock — управляющий канал агента.
Это узкий, явно определённый набор интерфейсов между VM и хостом — а не «всё ядро хоста», как у обычного контейнера.
Sandbox-модель: pod — это VM
В терминах Kata один pod = одна VM = один sandbox. Kubernetes может запускать в поде несколько контейнеров — все они делят одну VM; их сопровождает служебный pause-контейнер (sandbox-контейнер), задающий общие namespaces пода. shimv2 обслуживает весь pod одним экземпляром shim — вместо отдельных процессов shim и proxy на каждый контейнер, как в прежней архитектуре.
Изоляция получается двухуровневой:
- Между подами — аппаратная. Разные поды — это разные VM с разными гостевыми ядрами.
- Внутри пода — namespaces. Контейнеры одного пода изолированы друг от друга через cgroups и namespaces, которые создаёт гостевое ядро внутри VM.
Для CI это ровно то, что нужно: один job — это один pod, значит одна VM. Соседний job — другая VM с другим ядром; общего у них только физическая машина под hypervisor.
Сеть: как сетевой namespace хоста связан с VM
Оркестратор через CNI создаёт для пода отдельный сетевой namespace и обычно veth-пару: один конец в namespace пода, другой — на хосте. Напрямую подключить veth к VM нельзя — VM нужен TAP-интерфейс. Kata связывает одно с другим прозрачно; способ задаёт модель подключения (Настройка Kata: как интерфейс пода из CNI связывается с TAP-устройством VM — tcfilter, macvtap или none.):
- tcfilter (по умолчанию) — правила Traffic Control (TC) перенаправляют трафик между veth в namespace пода и TAP-устройством VM. Работает и с ipvlan/macvlan.
- macvtap — интерфейс пода подключается к VM через MACVTAP-устройство.
- none — когда VM находится в сетевом namespace хоста.
Ключевое: hypervisor получает отдельный сетевой namespace, а гость видит только отданное ему TAP-устройство. Прямого доступа к NIC хоста у гостя нет — трафик проходит через контролируемую хостом границу TC/TAP.
Хранилище: virtio-fs, DAX и блочный rootfs
Гостевой rootfs самой VM подключается через DAX: hypervisor отображает гостевой образ в память гостя
(на устройство /dev/pmem*) zero-copy — это экономит и память, и время загрузки.
Файлы контейнера пробрасываются иначе. По умолчанию — Паравиртуальная общая ФС: демон virtiofsd на хосте отдаёт rootfs контейнера в гостя; хост и гость видят одно дерево файлов.: на хосте работает демон
virtiofsd, который отдаёт OCI-bundle (rootfs контейнера) в директорию внутри гостевого rootfs
(kataShared). virtio-fs — это shared-fs модель: хост и гость видят одно и то же дерево файлов.
Альтернатива — блочное устройство: rootfs или том пробрасываются в VM как virtio-block, например через devicemapper snapshotter, который отдаёт образ контейнера как блочное устройство.
Trade-off между двумя моделями:
- virtio-fs гибче — живой проброс директорий, bind-mount томов, overlay, — но это отдельный демон и слой когерентности кешей между хостом и гостем.
- блочный rootfs проще и обычно быстрее на «чистом» I/O, но менее гибок и требует блочного snapshotter.
Гостевой образ и kata-agent как init
Гостевые компоненты бывают двух видов:
- rootfs-образ — полноценная корневая ФС; в роли init либо systemd (гибко, легко кастомизировать), либо сам kata-agent.
- initrd — образ, распаковываемый в память; init — kata-agent.
Собирается это с флагом AGENT_INIT=yes: тогда kata-agent запускается как init-процесс (PID 1)
гостя — без systemd и лишнего userspace. Минимальный вариант, initrd + agent-as-init, — самый быстрый
и компактный. Смысл в том, что гостевой userspace сведён к необходимому: ядро, agent и то, что нужно
контейнеру. Меньше кода — меньше поверхность атаки и быстрее загрузка.
Память и быстрый старт
Быстрый старт micro-VM складывается из нескольких вещей:
- минимальное гостевое ядро и урезанный гостевой образ (см. выше);
- VM templating / factory — заранее поднятая, инициализированная и приостановленная VM-шаблон, из
которой новые VM клонируются быстрее, чем загружаются с нуля (
enable_template,kata-runtime factory init; режим требует initrd и несовместим с virtio-fs в качествеshared_fs).
С памятью — так же прагматично. У VM есть стартовый объём (default_memory), дальше работает
hotplug: когда у контейнера задан лимит памяти, Kata добавляет её гостю на лету — через
virtio-mem (enable_virtio_mem) либо memory hotplug / balloon, в зависимости от VMM и
архитектуры. Это избавляет от необходимости выделять каждой VM память «с запасом».
Overhead при этом честно остаётся: каждая VM несёт своё ядро и свой стартовый объём памяти — это цена аппаратной границы (см. trade-offs ниже).
Поток: от containerd до процесса в VM
Как Kata встраивается в стандартный конвейер запуска контейнера:
- Оркестратор выбирает Kata — в Kubernetes через RuntimeClass, в CRI-O через аннотации. Никаких изменений в самих образах.
- containerd по
runtime_type(напримерio.containerd.kata.v2) вычисляет имя бинарника shim, подставляя префикс:io.containerd.kata.v2→containerd-shim-kata-v2. Он создаёт сокет и передаёт его одному экземпляру shim. - shim запускает hypervisor, тот загружает micro-VM из гостевых компонентов (образ + гостевое ядро).
- shim вызывает API агента внутри VM (
CreateSandbox/ создание контейнера) по ttRPC/VSOCK. - kata-agent поднимает процесс контейнера внутри VM — с cgroups и namespaces, из rootfs, проброшенного через virtio-fs.
- Job завершился — VM уничтожается вместе со всем содержимым.
Регистрация Kata как runtime handler в containerd — это по сути одна секция в конфиге (иллюстрация из документации Kata, не наш прод-конфиг):
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
runtime_type = "io.containerd.kata.v2"
А в Kubernetes нагрузку направляют на этот runtime через RuntimeClass:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata
handler: kata
---
apiVersion: v1
kind: Pod
metadata:
name: untrusted-job
spec:
runtimeClassName: kata
containers:
- name: build
image: ghcr.io/example/runner:latest
Для того, кто пишет workflow, всё это невидимо: он не знает и не должен знать, что его job исполняется в VM, а не в контейнере.
Почему это правильная граница для недоверенного CI-кода
Модель угроз Kata формулирует цель прямо: не дать недоверенной контейнерной нагрузке получить контроль над хостом, вытащить с него информацию или что-то на нём изменить.
Разница с обычным контейнером принципиальная:
- Общее ядро исключено. У гостя своё ядро. Эксплойт ядра из недоверенного кода бьёт по гостевому ядру внутри VM, а не по ядру хоста напрямую.
- Побег из контейнера ≠ доступ к хосту. Вырвавшись из контейнера, код оказывается всё ещё внутри VM. Чтобы дотянуться до хоста или до соседней нагрузки, ему нужно пробить ещё и границу виртуализации — а это принципиально более узкая и жёсткая поверхность.
- Нет доступа к ФС и сети хоста по умолчанию. Гость не видит файловую систему хоста без явно проброшенного устройства; сеть изолирована через TAP, прямого доступа к NIC хоста нет.
- Исчерпание ресурсов ограничено рамками VM. Fork-бомба или утечка памяти упираются в квоту своей micro-VM.
Именно поэтому Kata — естественный слой для CI: job-ы по определению исполняют чужой код, и контейнерной границы для этого мало.
Trade-offs — честно
Аппаратная изоляция не бесплатна:
- Overhead виртуализации. Каждая micro-VM — это отдельное гостевое ядро и память под него; запуск и потребление ресурсов выше, чем у обычного контейнера. Kata и её лёгкие VMM (Firecracker, Dragonball, Cloud Hypervisor) минимизируют это, но не сводят к нулю.
- Hypervisor — это тоже поверхность атаки. Граница переехала с ядра хоста на hypervisor; уязвимость в QEMU/KVM или в virtio-бэкенде теоретически даёт VM escape. Поэтому имеет значение, какой VMM выбран и как настроены бэкенды (userspace vhost-user безопаснее in-kernel vhost).
- Гостевое ядро тоже надо обновлять. Своё ядро не делает гостя неуязвимым — его CVE никуда не деваются, просто перестают быть общими с хостом.
Вывод трезвый: Kata не «отменяет» контейнерную безопасность, а добавляет под неё вторую, независимую границу. Это defense-in-depth, а не серебряная пуля.
runc, gVisor и Kata: где проходит граница
Kata удобно позиционировать через два соседних runtime — обычный контейнерный и «песочный».
| Runtime | Граница изоляции | Overhead | Против чего защищает |
|---|---|---|---|
| runc | namespaces + cgroups, прямой доступ к ядру хоста | минимальный | базовая изоляция процессов; общее ядро — слабое место |
| gVisor | userspace-«ядро» Sentry перехватывает системные вызовы приложения и обрабатывает их само; к ядру хоста уходит лишь минимальный, явно ограниченный seccomp-набор syscall, а доступ к ФС — через отдельный процесс Gofer | средний (заметная просадка на syscall/I/O) | резко сужает kernel attack surface, но это не VM-граница |
| Kata | аппаратная VM со своим гостевым ядром поверх KVM | выше (своё ядро и стартовая память на каждую VM) | защищает ядро и инфраструктуру хоста от недоверенной нагрузки |
Коротко: runc максимально быстрый и максимально доверяющий; gVisor вклинивает Userspace-«ядро» gVisor: перехватывает системные вызовы приложения и обрабатывает их само, отдавая ядру хоста лишь узкий набор syscall. (userspace-ядро) между приложением и хостом; Kata поднимает полноценную аппаратную границу. Для недоверенного CI-кода важна именно последняя.
Confidential Containers: когда недоверенным становится и хост
Отдельная возможность технологии — не обязательная часть базовой изоляции — запуск гостя внутри аппаратного Trusted Execution Environment — аппаратно-изолированная и зашифрованная область исполнения (Intel TDX, AMD SEV-SNP, IBM Secure Execution).: Intel TDX, AMD SEV-SNP, IBM Secure Execution. В таком режиме память гостя шифруется и защищена даже от хоста и hypervisor, а Криптографическая проверка того, что нагрузка исполняется в подлинном, ожидаемом TEE — до того, как в него передадут секреты или код. позволяет криптографически проверить окружение до того, как в него передадут нагрузку.
Это инвертирует базовую модель угроз Kata: там хост защищается от недоверенного гостя — здесь, наоборот, гость защищается от недоверенного хоста и оператора. Это направление Confidential Containers. (QEMU — наиболее поддерживаемый VMM для TDX и SEV-SNP.)
Существенная оговорка: всё выше — свойство самой технологии Kata. В этой статье Confidential Containers описаны как возможность, а не как конкретная конфигурация tempus.build.
Как это работает у нас
В tempus.build каждый job раннера исполняется в собственной аппаратной micro-VM на Kata Containers. Недоверенный CI-код — и его Docker-in-Docker — не достаёт до ядра хоста и до VM другого job-а: граница между job-ами аппаратная, а не «доверьтесь контейнеру». VM создаётся под конкретный job и уничтожается по его завершении вместе со всем, что в неё попало.
VM у нас имеет фиксированную форму под конкретный SKU раннера: число vCPU и объём памяти задаются дефолтом гипервизора, а не hotplug’ом, — job получает ровно ту машину, за которую платит, без динамического роста памяти. Бэкенд — QEMU/KVM (совместим с привилегированным Docker-in-Docker внутри VM); Cloud Hypervisor рассматриваем как кандидата на ускорение старта в дальнейшем. Гостевое ядро и гипервизор держим на актуальном security-floor и обновляем при выходе исправлений класса guest→host: вторая граница ценна ровно пока она пропатчена.
Kata — только один из слоёв нашей модели изоляции; как устроены остальные и как самостоятельно проверить образ раннера — на странице «Безопасность и изоляция».