Logo Craft Homelab Docs Нейросети Хостинг Обучение Контакты
Как изолировать расширения клиента Kubernetes: iframe, WebAssembly и minisign Трендовые github проекты в нашем телеграм канале. Подпишись →
3 ноября 2026 г.

Чужой код рядом с kubeconfig: как устроена платформа расширений Sextant

Sextant — десктопный клиент Kubernetes на Rust и Tauri 2 со встроенным MCP-сервером для AI-агентов. За три недели в его ядро попало больше двух десятков интеграций с операторами: Istio, Argo Rollouts, CloudNativePG, Strimzi, KubeRay и другие. Каждая нужна немногим, а сборка, тесты и память растут у всех. Специфичные интеграции решили вынести в расширения, и первым в каталоге с версии 0.28 стал Rook Ceph.

Расширение выполняется в программе, у которой есть kubeconfig, токены и право удалять объекты. Автор расширения может ошибиться, его аккаунт могут угнать, в зависимостях может оказаться что угодно. Поэтому модель угроз запрещает пять вещей: доступ к IPC Tauri, файлам и токенам, чтение значений Secret без разрешения, изменение объектов без спроса, отправку данных кластера в сеть и тихое расширение прав при обновлении.

Страница расширения живёт в iframe без сети

В Lens расширение работает внутри окна приложения и получает всё, что умеет окно. В Tauri главное окно видит все команды бэкенда, а самописные команды capabilities не закрывают. В Sextant каждая страница, секция и диалог расширения — это фрейм:

<iframe sandbox="allow-scripts allow-forms allow-popups-to-escape-sandbox"
        src="sextant-ext://<id>/index.html"></iframe>

Главное здесь — отсутствие allow-same-origin. У фрейма непрозрачное происхождение, он шлёт Origin: null, и IPC Tauri такие запросы отклоняет. С allow-same-origin на собственной схеме фрейм получил бы тот же IPC, что и приложение: Tauri считает любую зарегистрированную схему локальным происхождением.

К каждому ответу добавляется CSP с connect-src 'none', поэтому fetch и WebSocket со страницы не работают.

С хостом фрейм общается через postMessage и только JSON. Отправитель проверяется по event.source === iframe.contentWindow, потому что origin у всех фреймов одинаковый.

Цена решения: расширение рисует только внутри своего фрейма. Отказ IPC для Origin: null проверен вручную только на Linux, macOS и Windows покрыты CI.

Права проверяет Rust прямо перед действием

Права объявляются в манифесте в той же форме, что и доступ для AI-агентов: кластеры, пространства имён, виды объектов и действия. Действий пять: read, modify, delete, logs, secretValues. Exec, port-forward и Helm расширениям недоступны совсем, поэтому даже Rook Ceph не может запустить ceph в toolbox.

Перед установкой пользователь видит права обычными словами: «Читать Pods во всех пространствах имён всех кластеров; изменять ConfigMaps в team-a — с подтверждением». Выданные права хранит бэкенд. Обновление, которое просит больше, снова запрашивает согласие.

Rust перед побочным эффектом проверяет, что расширение включено, кластер выдан, а каждый затронутый объект разрешён правилами. Для modify и delete подтверждение включено по умолчанию. Без ответа запрос отклоняется через 120 секунд, кнопка «Разрешить» первую секунду не реагирует на клавиатуру, а пятый ожидающий запрос от одного расширения отклоняется без вопроса.

Страж общий для расширений и MCP-сервера: две копии со временем разошлись бы. Значения Secret вырезаются в ядре до любого ответа, и без secretValues расширение видит только имена ключей. То же касается секретов вне Secret, например ключа TLS в Route OpenShift.

Кнопки и колонки описаны в манифесте

Всё, что расширение встраивает в интерфейс, лежит в sextant-extension.json. Sextant регистрирует пункты меню, действия и колонки сам, не запуская код расширения. Упавшее расширение не ломает отрисовку, окно согласия заранее показывает все вклады, а выключение снимает их одним вызовом без перезапуска.

Если бы значение колонки считал код расширения, таблице на десять тысяч подов пришлось бы гонять каждую строку через postMessage. Вместо этого хост вычисляет JSONPath сам, как printer columns у CRD. Каждая новая точка встраивания требует изменения схемы манифеста.

Бэкенд работает в wasmtime с пустым WASI

Страницам сеть не нужна, но расширению иногда надо сходить в API облака или трекер задач. Для этого есть бэкенд — компонент WebAssembly в том же процессе, где лежат kubeconfig и токен MCP-сервера. Побег из песочницы открыл бы все кластеры сразу, поэтому ограничения жёсткие:

  • WASI пуст. Нет переменных окружения, аргументов и каталогов, любой адрес сокета отклоняется. Работают только часы и случайные числа.
  • Импорты ограничены. Компонент с импортами кроме wasi:* и sextant:extension/* отклоняется при установке.
  • Сеть только к объявленным хостам. До 20 имён без масок, только HTTPS на 443. При каждом разрешении имени проверяется каждый адрес: loopback, link-local, частные сети и сервис метаданных облака отклоняются, так что DNS не направит объявленное имя внутрь вашей сети.
  • Токены в системной связке ключей. Во фрейм, логи и тексты ошибок они не попадают.
  • Каждый вызов с чистого листа. Новый store, 10 секунд, 64 МиБ памяти, 1 МиБ JSON на вход и выход, до 4 вызовов одновременно. Три падения подряд ставят бэкенд на паузу.

Нативный процесс-спутник отбросили: ему нужна своя песочница на каждой ОС (seccomp и Landlock, App Sandbox, AppContainer). QuickJS написан на C, где ошибка памяти означает побег. В wasmtime есть WIT с типизированной границей, прерывание по эпохам и ограничитель ресурсов, на которых построены лимиты времени и памяти.

Размеры компонентов сильно зависят от языка. Rust под wasm32-wasip2 даёт около 0,3 МБ, TypeScript через componentize-js — около 12 МБ из-за встроенного движка JS. Python не поддержан: компонент с CPython весит 20–40 МБ при лимите 16 МиБ. Фоновой работы у бэкенда нет, он живёт, пока открыт фрейм, и ещё 60 секунд.

Фоновые задачи заменены декларациями

Правила проблем расширение описывает данными, а считаются они на watch самого Sextant:

{
  "id": "health-error",
  "kind": { "group": "ceph.rook.io", "kind": "CephCluster" },
  "when": { "type": "equals", "path": ".status.ceph.health", "value": "HEALTH_ERR" }
}

Набор условий фиксирован, ничего не исполняется как код. Правило, которое заглядывает в data секрета, отклоняется ещё при чтении манифеста. Правило смотрит только на один объект.

С метриками та же схема. Запросы PromQL лежат в манифесте, подписаны вместе с пакетом и показаны в окне согласия дословно. Расширение передаёт id запроса и типизированные параметры, а Rust подставляет их с экранированием. Изменение одного символа запроса снова требует согласия. Слабое место остаётся: Prometheus может вернуть ряды пространств имён, которые расширению читать нельзя. Окно согласия об этом предупреждает, запретить такое декларация не может.

Каталог доверяет ключу издателя

Каталог — публичный репозиторий, где каждое расширение описано файлом extensions/<id>.json с репозиторием автора и публичным ключом minisign. Пакеты лежат в релизах автора. Раз в сутки и после каждого мержа workflow проверяет подпись последнего релиза и собирает index.json.

Подпись на самом индексе отбросили: ключ лежал бы в секретах CI, куда доберётся атака на каталог. При установке бэкенд сам скачивает пакет и .minisig, до распаковки сверяет размер, SHA-256 и подпись, а окно согласия собирает из скачанного манифеста.

После установки Sextant запоминает репозиторий и ключ автора. Если в индексе они изменились, обновление блокируется до переустановки, поэтому взлом каталога не подменит уже стоящее расширение. Автообновлений нет.

Цена такая: потерянный ключ издателя не восстановить. Пакеты, поставленные из файла или по ссылке, не подписаны, и их защищают только песочница и окно согласия.

Что показал Rook Ceph

Расширение написано только на публичном API, без правок ядра под него. Вынос встроенных интеграций пока заблокирован тремя пробелами API: счётчиком в меню, правилами по нескольким объектам и диагностикой для агентов.

Для операций записи расширение использует только объекты самого Rook. Merge patch несёт resourceVersion показанного объекта, поэтому параллельная правка даст конфликт. Списки меняются через JSON patch с операцией test, потому что merge patch заменил бы список целиком. Обновление Ceph запрещено при HEALTH_ERR и при прыжке через мажорный релиз. Удаление OSD идёт через остановку, проверку active+clean и Job очистки без --force-osd-removal. Каждый патч прогоняется как server-side dry run в тестовом кластере kind, а запросы метрик проверяются через promtool.

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