Надежная изоляция в Docker с использованием Kata и MicroVM
Table of Contents
Контейнеры (и в частности docker) сейчас практически основа современной доставки ПО в инфраструктуру. Если верить опросам Docker (он предвзят, но я не нашел цифр лучше) – 92% разработчиков использует контейнеры в той или иной форме. Контейнеры невероятно удобны – это герметичные образы, которые все свое возят с собой. Их не нужно устанавливать и почти не нужно настраивать, у них нет зависимостей (и проблем с конфликтами зависимостей). Выгрузка таких контейнеров в любую инфраструктуру элементарна, скачал и запустил. Контейнер иммутабелен, у него нет своего состояния и потому не бывает проблем, связанных с потерей состояния. Контейнер изолирован – все, что живет внутри – живет внутри. Не бывает конфликта PID/имен процессов/блокировок в файловой системе/сокетов или портов. Контейнеру можно ограничить ресурсы на уровне ОС и он никогда за пределы этих ресурсов не выйдет. Это делает управление ресурсами в общем делом тривиальным – контейнеры можно паковать в сервер как коробки в кладовку, выбирая профиль нагрузки и under/overcommitement по своему вкусу. Накладные расходы на контейнер минимальны и почти незаметны, с точки зрения операционной системы контейнер – это просто процесс и немного магии ядра для ограничения ресурсов. Контейнер стартует так же быстро, как обычный процесс (ведь по сути это и есть процесс). Контейнер безопасен – процесс, живущий внутри, наружу выберется только через явно разрешенные интерфейсы – сетевые или файловую систему. Или нет?
Спойлер: Нет
##
Проблема
Контейнер по умолчанию счиатется изолированым и зрительно он хорошо изображает изоляцию процесса. Хост видит всю внутрянку контейнера (дерево процессов, ресурсы, планировщик), но процессы изнутри контейнера видят только сам контейнер. В списке процессов будут только процессы самого контейнера, файловая система будет только своя (файловая система хоста доступна только если подключить ее явно, например, для сохранения состояния между перезапусками или пересозданиями контейнера). Попытка выскочить за пределы ограничений ресурсов кончится или OOM, который прибьет процесс-нарушитель (для памяти) или троттлингом (если это CPU). Управление тем, что процесс видит сделано через ядерный механизм Namespaces, а ограничение ресурсов выполнено через CGroups. Эти механизмы работают на уровне самого ядра и в целом так же надежны, как само ядро. Вот только ядро не абсолютно надежно. Даже изоляция “не настоящая” – это просто набор трюков ядра, который позволяет спрятать одни процессы от других, хотя система у них общая и ядро тоже общее. Для linux containers (упрощенно говоря – для docker, хотя их целое семейство с единым протоколом) есть целый набор трюков разной степени хитрости, чтобы из контейнера убежать. Привелигированный контейнер позволяет выскочить из изоляции одной командой (никогда не используйте --priveleged). Неправильно ограниченый набор доступных системных вызовов (capabilities) тоже открывает широкий простор для злоупотреблений. Попавшая внутрь контейнера devfs – это еще один способ вылезти из контейнера, потому что /dev общий для всего ядра и через него можно увидеть пространства имен других конейтенров (и через нее же убежать). Немалое количество атак на повышение привилегий в ядре срабатывают из контейнера тоже (и позволяют выскочить в хост-систему). Кроме того, есть атаки на конкретно побег из контейнера в хост. Так как контейнер использует то же ядро, что и хост – у контейнера общий с хостом набор API для взаимодействия с ядром. Чем больше точек взаимодействия у атакующего – тем проще найти уязвимость и использовать ее для побега и захвата целевой системы. Так как контейнер взаимодействует с ядром через все те же системные вызовы – есть попытки улучшить безопасность контейнеров через фильтрацию этих системных вызовов. Один из таких проектов придумали в google, называется он gVisor. gVisor следит за тем, какие именно системные вызовы процесс внутри контейнера пытается использовать и отклоняет потенциально небезопасные вызовы. Настраивать его очень непросто, потому что если вы где-то ошибетесь в настройках – процесс просто молча схлопнется и контейнер перезапустится. Причем контейнер может работать месяцами, а потом сработает конкретный syscall и хлопнет вам процесс. Отладка такого блуждающего бага – удовольствие на больших любителей.
Процессы можно изолировать надежно и безопасно, причем этой технологии лет даже больше, чем контейнерам. И называется эта технология полной виртуализацией. Виртуальная машина максимально изолирована от физической машины, на которой она запущена. У нее полностью свое ядро, свой набор процессов, изолированный сегмент оперативной памяти. Даже переферию можно сделать свою. Степень изоляции на уровне банковского сейфа. Количество атак на гипервизор (систему управления виртуальными машинами) изнутри самой виртуалки можно пересчитать буквально по пальцам одной руки. Например, автору известно 2 атаки на гипервизоры от VMWare, 3 на KVM и 2 на QEMU. Не за год, вообще. Плюс 3 атаки, специфичных для реализации железа (они не зависят от модели виртуальной машины, но зависят от типа и модели CPU или контроллера памяти). Такие атаки – огромная редкость, встречаются они раз в десятилетие и каждый раз производят эффект взорвавшейся бомбы. То есть их практически мгновенно чинят и виртуализация снова становится безопасной. Виртуальная машина кажется идеальным решением проблемы изоляции – у нее минимум общих мест с хост-системой, максимально независимые ресурсы и стопроцентная изоляция. К сожалению – минусы тоже есть. Виртуальные машины медленные. Старт полноценной ОС занимает несколько минут (это полноценная инициализация оборудования и после этого – создание дерева процессов для полноразмерного сервера).
Вторая проблема виртуальных машин - это их состояние. Внутри виртуальной машины живет полноценная операционная система, безо всяких скидок. Эту операционную систему нужно установить и настроить, а потом – обновлять и поддерживать. С точки зрения поддержки и управления безопасностью виртуальная машина почти не отличается от физического сервера. Это огромные накладные расходы, особенно когда у вас таких машин сотни, а то и тысячи. Контейнер легковесный, он создает один целевой процесс, в этом основа контейнерного дизайна. У контейнера нет состояния, его не нужно обновлять и поддерживать в актуальном состоянии, потому что его состояние “запекается” в момент создания. Тяжелая и неповоротливая ОС общего назначения совсем не похожа на легковесный процесс внутри контейнера.
##
Решение: micro-vm
Проблема “сделать быстро, как контейнер, но безопасно, как VM” известна многим вендорам и известна давно. Google и RedHat пытались решить эту проблему через более жесткие ограничения для процессов (с помощью gVisor и SELinux соответственно). Amazon, Intel, nVidia и некоторые другие компании пошли по иному пути. Если VM достаточно надежна и безопасна, просто прожорлива – почему бы не сделать очень компактную VM, приспособленную для единственной цели? Так появились MicroVM. Идея в том, что из таких VM выкинуто все “лишнее”, не нужное для запуска контейнеров. Например, там нет полноценного экрана, только символьный терминал. Виртуальная клавиатура там имеет ровно одну кнопку (на случай, если потребуется press any key). Эмуляция и нестандартные архитектуры обычно не поддерживаются – только нативная виртуализация. Такая микро-машина поддерживает только одну ОС (Linux), и Linux этот тоже обезжирен до предела. Благодаря такой агрессивной оптимизации microVM стартует практически мгновенно, считанные секунды (сравните с минутами, а то и десятками минут для нормальных ОС). MicroVM компактны и имеют минимальные накладные расходы по ресурсам. Это позволяет получить “лучшее из двух миров” – скорость, удобство и поддержку контейнеров и безопасность вирутальных машин. MicroVM бесшовно интегрируется в container runtime и с точки зрения пользователя ничем от обычного контейнера не отличается. Можно использовать все тот же podman run или docker-compose up, образы контейнеров будут работать так же, как раньше. Просто теперь – внутри виртуальных машин. Kata-containers – один из вариантов таких виртуальных машин.
#
Ограничения
Kata-containers – это полноценная виртуализация, хотя и притворяется контейнером. Kata-containers требует поддержки виртуализации на стороне процессора (инструкций VMX/SVM для x86, ARM поддерживает виртуализацию начиная с ARMv8). В облаке или виртуальной машине у вас должна быть включена вложенная виртуализация (Nested Virtualisation, не все провайдеры облаков ее предоставляют и не для всех типов машин). Эмуляция и кросс-архитектурный запуск не поддерживаются. Так как kata - виртуальная машина, внутренностей контейнера-гостя из хоста доступно не будет, с точки зрения ОС это будет один процесс монолитный процесс. Это не слишком удобно, но расплата за безопасную изоляцию.
#
Kata внутри Docker
Kata-containers изначально придуманы для работы внутри Kubernetes. Штатный способ установки – helm-chart и DaemonSet, который все и ставит. Это очень удобно, но не получится повторить для одиночного Docker, да и такая сложная установка избыточна для простого одиночного хоста. Можно, конечно, поставить одиночный kubernetes из одного хоста (k3s или k0s прекрасно для этого подойдет), но я предпочитаю простые решения. Kata-containers распространяется как готовый набор статических бинарных файлов и подключается к docker как container runtime. Я человек очень ленивый и набросал простенький runbook для Ansible, который скачивает и распаковывает для меня эти файлы:
- name: install Kata Containers (new ver)
hosts:
- your.host.address
vars:
kata_version: "3.32.0"
kata_prefix: "/var/lib/kata/{{ kata_version }}"
arch_map:
x86_64: "amd64"
aarch64: "arm64"
armv7l: "armhf"
tasks:
- name: Set architecture variable
set_fact:
arch: "{{ arch_map[ansible_facts['architecture']] }}"
- name: Check if alredy downloaded
ansible.builtin.stat:
path: "{{ kata_prefix }}/kata/bin/kata-runtime"
register: kata_file
- name: Download kata release
get_url:
url: "https://github.com/kata-containers/kata-containers/releases/download/{{ kata_version }}/kata-static-{{ kata_version }}-{{ arch }}.tar.zst"
dest: "/{{ kata_prefix }}/kata-static-{{ kata_version }}-{{ arch }}.tar.zst"
mode: '0644'
register: download_result
until: download_result is success
retries: 5
delay: 10
when: not kata_file.stat.exists
- name: create version folder
ansible.builtin.file:
path: "{{ kata_prefix }}"
state: directory
- name: Extract kata release
unarchive:
src: "/{{ kata_prefix }}/kata-static-{{ kata_version }}-{{ arch }}.tar.zst"
dest: "{{ kata_prefix }}"
remote_src: yes
mode: '0755'
creates: "{{ kata_prefix }}/kata/bin/kata-runtime"
extra_opts:
- "--strip-components=2"
- name: Remove kata archive file
ansible.builtin.file:
path: "/{{ kata_prefix }}/kata-static-{{ kata_version }}-{{ arch }}.tar.zst"
state: absent
Этот runbook скачивает архив с github (для большей надежности добавлен retry, если github будет работать нестабильно, последнее время у него такое случается). Файл распаковывается в {{ kata_prefix }}, по умолчанию - /var/lib/kata/{{ kata_version }}. Этот трюк поможет бесшовно обновить kata-containers. Достаточно поменять версию в {{ kata_version }}, перезапустить runbook, поменять путь в docker-config (см ниже) и перегрузить docker service. При перезапуске контейнера он будет использовать обновленную версию runtime и можно делать это контейнер за контейнером.
После установки runtime нужно проверить, можно ли его использовать:
/var/lib/kata/3.32.0/kata/bin/kata-runtime check --verbose
WARN[0000] Not running network checks as super user arch=amd64 name=kata-runtime pid=21088 source=runtime
INFO[0000] CPU property found arch=amd64 description="Intel Architecture CPU" name=GenuineIntel pid=21088 source=runtime type=attribute
INFO[0000] CPU property found arch=amd64 description="Virtualization support" name=vmx pid=21088 source=runtime type=flag
INFO[0000] CPU property found arch=amd64 description="64Bit CPU" name=lm pid=21088 source=runtime type=flag
INFO[0000] CPU property found arch=amd64 description=SSE4.1 name=sse4_1 pid=21088 source=runtime type=flag
INFO[0000] kernel property found arch=amd64 description="Host kernel accelerator for virtio network" name=vhost_net pid=21088 source=runtime type=module
INFO[0000] kernel property found arch=amd64 description="Host Support for Linux VM Sockets" name=vhost_vsock pid=21088 source=runtime type=module
INFO[0000] kernel property found arch=amd64 description="Intel KVM" name=kvm_intel pid=21088 source=runtime type=module
INFO[0000] kernel property found arch=amd64 description="Kernel-based Virtual Machine" name=kvm pid=21088 source=runtime type=module
INFO[0000] kernel property found arch=amd64 description="Host kernel accelerator for virtio" name=vhost pid=21088 source=runtime type=module
System is capable of running Kata Containers
INFO[0000] device available arch=amd64 check-type=full device=/dev/kvm name=kata-runtime pid=21088 source=runtime
INFO[0000] feature available arch=amd64 check-type=full feature=create-vm name=kata-runtime pid=21088 source=runtime
System can currently create Kata Containers
Ядерный модуль KVM, который отвечает за поддержку виртуализации идет в комплекте с ОС и дополнительно ставить ничего не нужно, достаточно загрузить его командой modprobe kvm.
Чтобы Docker мог использовать kata как runtime, ему нужно рассказать об этом рантайме. Пример конфигурационного файла /etc/docker/daemon.json:
{
"log-driver": "local",
"features": {
"time-namespaces": false
},
"runtimes": {
"kata": {
"runtimeType": "/var/lib/kata/3.32.0/kata/bin/containerd-shim-kata-v2"
}
}
}
После перезапуска docker-daemon можно создать контейнер с использованием нового runtime: docker run --runtime kata --rm nginx. В такой настройке runtime опциональный и позволит поэкспериментировать с kata, не ломая контейнеры, запущенные нормальным путем. Проверить работу проще всего, провалившись в контейнер и посмотрев на версию ядра, она у хоста и контейнера будет, очевидно, разной:
host# uname -a
Linux host 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 GNU/Linux
guest# uname -a
Linux 01d00b2b5c9b 6.18.28 #1 SMP Mon May 18 17:38:40 UTC 2026 x86_64 Linux
Очень важно отключить фичу time-namespaces для kata runtime – это новая фича docker engine и если этого не сделать – контейнер упадет при запуске с невразумительной ошибкой. Еще одна проблема - DNS Resolving. Docker подставляет свой собственный резолвер внутрь контейнера на адресе 127.0.0.11 (этот адрес “не настоящий” и фактически запрос на него перенаправляется из контейнера в хост-систему). Это очень удобный фокус, потому что резолвер общий и сам docker добавляет туда имена всех контейнеров. Это позволяет общаться контейнерам по именам, без знания IP-адресов друг друга. К сожалению, в случае с Kata этот фокус работать не будет, потому что Kata полноценная виртуальная машина (просто маленькая) и localhost у нее свой. Авторы Kata не планируют решать эту проблему, потому что основная ЦА для Kata - Kubernetes, DNS resolving там решен на уровне кластера и трюки с подделкой localhost не требуются. Решений у этой проблемы три:
- просто игнорировать проблему. Ну нету DNS и нету. Если вашему контейнеру не надо ходить в сеть (или процесс, который ходит в сеть ходит туда сам и системный резолвинг не использует) – вполне себе опция. Не стоит считать это защитой от побега из контейнера, но если никому не мешает – чинить не нужно
- Использовать внешний резолвер, подменив
resolv.confвнутри контейнера. Подменять его в образе бессмысленно, потому что docker при старте контейнера подложит свой. Но можно просто подмонтировать внешний том (volume), примерно вот так:
docker run --runtime kata --rm -ti --network host_service -v /etc/resolv.conf:/etc/resolv.conf:ro alpine
Контейнер будет использовать тот же resolv.conf (и те же DNS-сервера) что и хост-система. Это позволит контейнеру попасть в интернет, но не поможет найти соседей
- Третий вариант – использовать специализированный резолвер, запустив контейнер с ним на фиксированном адресе и передать адрес в
/etc/resolv.confостальных контейнеров (помним про тягу docker крутить resolv когда не просят – придется подготовить отдельныйresolv.confдля контейнеров и пробрасывать его внутрь всех контейнеров). CoreDNS, который используется в Kubernetes вполне подойдет. Проблема в том, что резолверу потребуется доступ к Docker API. У Docker API полностью отсутствует понятие раздельных прав доступа – доступ к API дает доступ везде. Такой resolver полностью нивелирует ваши усилия по повышению безопасности всей инсталляции, потому что успешный взлом resolver-а дает взломщику “ключи от королевства”, включая выход в хост-систему. Я бы оставил такой вариант на самый крайний случай, если вам совсем уж некуда деваться.
#
Kata и Kubernetes
Kata создавалась с оглядкой на Kubernetes, так что интеграция совершенно бесшовная и проблем не вызывает. Kata-runtime устанавливается из helmChart oci://ghcr.io/kata-containers/kata-deploy-charts, values.yaml подробно документирован. Kata использует довольно сложный chart с внутренней логикой (pre-hook, post-hook), по этому чарт нельзя использовать с GitOps инструментами, такими как Flux или ArgoCD, даже Kustomize просто сломает чарт. Hook-и не являются частью шаблона, это логика, интерпретируемая Helm при установке. Вне helm они перестанут работать и установить ничего не получится. Все, что нужно сделать, чтобы контейнер использовал Kata как основной runtime - поменять runtimeClass (для Deployment он расположен по адресу spec.template.spec.runtimeClassName) на kata-qemu. По словам авторов Kata qemu – основной runtime и поддерживается лучше всего (сейчас разработчики мигрируют на qemu-rs, реализации на rust). Остальные рантаймы экспериментальные и поддержка не гарантирована.
##
Выводы
Контейнеры сами по себе не очень безопасны и не должны использоваться как граница безопасности. Kata позволяет получть надежную изоляцию уровня виртуальной машины и удобство обычных контейнеров. Накладные расходы на виртуализацию минимальны, так что заменить обычный контейнер на microVM может быть вполне разумной идеей, если вам нужно более безопасная изоляция или больше контроля над ресурсами. Проблемы в работе KataVM встречаются, но они решаемы. MicroVM надежно изолирует внутренний мир контейнера от хоста и снижает шанс успешного побега. Нужно только помнить, что изоляция, даже самая лучшая не может быть единственной мерой безопасности в системе.