Daily MaverickBangladesh garment workers stage fourth day of wage protestsThe Jerusalem PostEisenkot may get the mandate, but haredi parties hold key to Israel's next government - analysisPunchNigeria, France seek increased energy investmentsBollywood HungamaKaran Tacker to begin shooting for Bhay: The Gaurav Tiwari Mystery Season 2 in MarchInquirerCloudy Wednesday, rain showers expected due to ‘amihan’, easterliesBBC News BrasilEleições de meio mandato nos EUA: o que está em jogo para o governo de Donald TrumpGhaflaChebet Ronoh Reveals How She Blew Her First KSh1.5 Million on Alcohol at 19ZDF heuteEntdecken Sie das ZDF-NachrichtenstudioEuronewsIran warns of ‘new battlefield’ after reported US strike on Qeshmسكاي نيوز عربيةمصر.. تحذير من دخول أفاعي الكوبرا إلى المنازل شتاءn-tvHohlfiguren günstiger zu haben: Schoko-Weihnachtsmänner trotzen der InflationVanguardEl Nino Watch: The impacts around the world this week
The Daily Newsstand · Free, Always
Tuesday, November 3, 2026

Как мы спроектировали расширения для клиента Kubernetes: песочница, WebAssembly и подписанный каталог

Translate

Sextant — десктопный клиент Kubernetes на Rust и Tauri 2, замена Lens без аккаунта и телеметрии, со встроенным MCP-сервером для AI-агентов. С версии 0.28 у него есть каталог расширений, и первым в нём стало расширение для Rook Ceph.

Расширение — это чужой код внутри программы, у которой есть kubeconfig и токены от всех ваших кластеров. В статье — шесть архитектурных решений, которые делают такой код безопасным: изоляция интерфейса, проверка прав в Rust, вклады как данные, бэкенд на WebAssembly, декларации вместо фонового кода и каталог с подписями. Для каждого — какие варианты мы рассматривали, почему выбрали именно так и чем за это платим. В конце — как платформа выдержала первое настоящее расширение, для Rook Ceph.

Sextant бесплатный и таким останется. Весь его код сгенерирован Claude Code: человек ставит задачи, принимает архитектурные решения и делает ревью, а каждое изменение проходит больше 6500 тестов на Linux, macOS и Windows.

Задача

За три недели жизни проекта в ядро Sextant попало больше двух десятков интеграций с операторами: Gateway API, Istio, Argo Rollouts, CloudNativePG, Strimzi, Kueue, KubeRay, Traefik, OpenShift, k3s и другие. Каждая нужна немногим, а весит на всех: в сборке, в тестах, в памяти и в каждом релизе.

Курс такой: в ядре остаётся то, что нужно любому кластеру, а всё специфичное для оператора или инструмента постепенно переезжает в расширения (#320).

  • В ядре: кластеры и kubeconfig, таблицы и детали ресурсов, редактор, логи, терминалы, port-forward, Helm, метрики, проблемы и уведомления, MCP-сервер и сама платформа расширений.

  • Кандидаты на вынос: CloudNativePG, Strimzi, Kueue, KubeRay, KServe, Traefik, Contour, Crossplane, Knative, OLM, k3s.

У расширения свои версии: исправление для Ceph не ждёт релиза всего клиента. И расширение может написать не только команда Sextant: ваш внутренний оператор заслуживает такого же удобного экрана, как и популярные.

Но из ядра ничего не уходит, пока API расширений не умеет всё, что умеет встроенная фича: проблемы, уведомления, метрики, переводы. Поэтому первое расширение, Rook Ceph, написано с нуля и только на публичном API — как проверка, что стороннему автору хватит того же, что и нам.

Требования и модель угроз

У десктопного клиента Kubernetes есть ваши kubeconfig, токены, доступ к секретам и право удалять объекты. Любое расширение нужно считать потенциально враждебным: автор может ошибиться, его аккаунт могут угнать, а в зависимостях может завестись что угодно.

В Lens расширение работает внутри окна приложения и получает всё, что умеет само окно. В Tauri главное окно видит все команды бэкенда, а самописные команды не закрываются capabilities. Код расширения в окне смог бы читать kubeconfig и секреты в обход любых настроек.

Отсюда список того, что не должно случиться никогда:

  • расширение дотянулось до IPC Tauri, файлов, kubeconfig или токенов;

  • прочитало значение Secret, если пользователь этого не разрешил;

  • изменило или удалило объект без спроса;

  • отправило данные кластера куда-то в сеть;

  • тихо расширило свои права при обновлении.

Плюс два требования к самой платформе. Одна сборка должна работать на Linux (x86_64 и arm64), macOS и Windows. А расширение должно уметь всё, что умеет встроенная интеграция, иначе из ядра нечего будет выносить.

Любой вызов расширения проходит через стража в Rust

Любой вызов расширения проходит через стража в Rust

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

Решение 1. Интерфейс расширения — iframe в песочнице

Каждое окно расширения — страница, секция деталей, диалог действия, вкладка дока — это <iframe> с атрибутом sandbox и без allow-same-origin:

<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 такие запросы не принимает. У фрейма нет доступа ни к окну Sextant, ни к командам бэкенда, ни к cookies. Это тот атрибут, который нельзя ослаблять никогда, и тест это сторожит.

Файлы отдаёт наш собственный протокол sextant-ext (на Windows — http://sextant-ext.localhost). Он отдаёт только файлы внутри пакета включённого расширения. Символические ссылки, .. и чужие пути дают 404.

К каждому ответу протокол собирает CSP под адрес именно этого расширения:

default-src 'none';
script-src sextant-ext://<id>/ 'unsafe-inline';
style-src  sextant-ext://<id>/ 'unsafe-inline';
img-src    sextant-ext://<id>/ data:;
connect-src 'none'; form-action 'none';
sandbox allow-scripts allow-forms allow-popups-to-escape-sandbox

connect-src 'none' значит, что у страницы расширения нет сети: ни fetch, ни WebSocket. 'self' не используется: у песочницы непрозрачное происхождение, а на Windows все расширения делят один хост. Директива sandbox продублирована в заголовке, чтобы документ, открытый не в нашем фрейме, тоже оказался в песочнице.

С Sextant фрейм разговаривает только сообщениями postMessage:

{ "sextant": 1, "doc": "k3Jf…", "id": 7, "method": "resources.list", "params": { … } }

Происхождение у всех фреймов одинаковое ("null"), поэтому хост проверяет отправителя по event.source === iframe.contentWindow. Поле doc — случайный 128-битный идентификатор документа: фрейм, который перезагрузили, не получит ответы предыдущего документа. На один документ — не больше 32 подписок на watch, на фрейм — не больше 8 документов.

Расширение не может передать хосту функцию или объект — только JSON. Зато и сломать отрисовку самого Sextant ему нечем.

Что рассматривали и отбросили:

  • Код расширения в окне приложения, как в Lens. Самый гибкий вариант, но расширение получает всё, что может окно, то есть все команды бэкенда.

  • iframe с allow-same-origin на собственной схеме. Tauri считает любую зарегистрированную схему локальным происхождением, и такой фрейм получил бы тот же IPC, что и приложение.

  • Локальный HTTP-сервер на случайном порту. Проблему локального происхождения решает, но добавляет приложению слушающий порт и frame-src под конкретный порт.

  • Своё происхождение для каждого расширения через имя хоста. На Windows все расширения живут на одном хосте sextant-ext.localhost, так что изоляции бы не было. Песочница изолирует на всех ОС одинаково.

  • CSP с 'self'. На одной платформе ничего не грузится, на другой разрешены файлы всех расширений сразу.

Чем платим. Через границу идёт только JSON, поэтому расширение рисует только внутри своего фрейма и не может подставить свой компонент в таблицу Sextant. А то, что IPC Tauri отклоняет Origin: null, мы прочитали в исходниках Tauri и проверили на Linux; macOS и Windows покрыты только CI.

Решение 2. Одна модель прав и один страж с AI-агентами

Расширение объявляет права в манифесте в той же форме, в которой в Sextant настраивается доступ для AI-агентов: кластеры, пространства имён, виды объектов и действия.

"permissions": {
  "clusters": { "type": "all" },
  "rules": [{
    "namespaces": { "type": "all" },
    "kinds": { "type": "only", "kinds": [
      { "group": "ceph.rook.io", "kind": "CephCluster" },
      { "group": "", "kind": "Pod" }
    ]},
    "actions": ["read"]
  }]
}

Действий всего пять: read, modify, delete, logs и secretValues. Exec, port-forward и Helm расширениям недоступны совсем.

Перед установкой Sextant показывает права обычными словами: «Читать Pods во всех пространствах имён всех кластеров; изменять ConfigMaps в team-a — с подтверждением». На значения Secret есть отдельное предупреждение. До кнопки «Установить» ничего не выдано.

Окно согласия перед установкой Rook Ceph из каталога

Окно согласия перед установкой Rook Ceph из каталога

Окно согласия перед установкой из каталога: источник, ключ издателя, всё, что расширение добавит, его правила проблем и запросы метрик. Ниже — права обычными словами.

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

Каждый вызов проверяет Rust

iframe не доверяем, поэтому проверка живёт не в окне, а в бэкенде. Окно пересылает запрос командой extension_call, а Rust прямо перед побочным эффектом проверяет:

  1. расширение установлено и включено;

  2. кластер входит в выданные;

  3. каждый затронутый объект разрешён правилами;

  4. если правило требует подтверждения, спрашивает пользователя.

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

Страж один на расширения и MCP-сервер: один диалог, одна очередь подтверждений и одинаковые жёсткие правила. Поды node-shell нельзя изменить или удалить. apply проверяется на тех же разобранных документах, которые потом применяются, чтобы нельзя было проверить одно, а применить другое.

Значения Secret вырезаются ещё в ядре, до любого ответа: без secretValues расширение видит только имена ключей. То же с секретами, которые лежат не в Secret: ключ TLS в Route OpenShift, values в HelmChart k3s, аргументы узла RKE2. Для них есть одна таблица полей, общая для Rust и TypeScript.

Каждый вызов попадает в журнал активности (последние 2000 записей). Там только сводки: кто, что, над чем и с каким итогом. Ни значений секретов, ни YAML.

Что рассматривали и отбросили:

  • Отдельный словарь прав для расширений. Пользователи уже знают модель прав для агентов, а функция решения и редактор правил давно покрыты тестами. Вторая модель дала бы второй набор ошибок.

  • Крупные разрешения вроде «может читать кластер», как в мобильных ОС. Ими нельзя сказать «менять ConfigMaps только в team-a».

  • Проверка во фронтенде. Тогда единственной преградой был бы код окна, а из окна доступна любая команда бэкенда. Одна ошибка в окне превратилась бы в полный доступ.

  • Отдельный страж, очередь и журнал для расширений. Две копии разошлись бы, и правила «отказ при сомнении» и «проверка прямо перед действием» пришлось бы писать дважды. Да и пользователю удобнее одно место, где появляется «Разрешить» с подписью, кто спрашивает.

Чем платим. Права точные, но окно согласия длиннее, чем список из трёх галочек. А exec, port-forward и Helm расширения не получают вовсе. Даже Rook Ceph не может запустить ceph в toolbox — это делает сам пользователь в терминале Sextant.

Решение 3. Вклады в интерфейс — данные, а не код

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

Вклад

Где появляется

Чем рисуется

Страница

пункт бокового меню

iframe расширения

Секция или вкладка деталей

карточка объекта нужного вида

iframe расширения

Действие

меню объекта

диалог с iframe

Колонка

таблица ресурсов

JSONPath, как у printer columns CRD

Вкладка дока

нижняя панель, рядом с логами и терминалами

iframe расширения

Раздел настроек

настройки Sextant

iframe расширения

Правила проблем

страница «Проблемы» и уведомления

декларация, считает Sextant

Запросы метрик

графики внутри расширения

PromQL-шаблон из манифеста

Бэкенд

невидим, его зовут страницы расширения

компонент WebAssembly

Идентификаторы вкладов получают префикс ext:<id>:, поэтому расширение не подменит чужую кнопку. Когда расширение выключают или удаляют, все его пункты, вкладки и открытая страница исчезают сразу, без перезапуска.

Из iframe расширению доступны такие методы:

  • clusters.list, kinds — какие кластеры и виды объектов есть;

  • resources.list, get, watch — чтение, в том числе живая подписка;

  • resources.apply, patch, delete — изменения, через подтверждение;

  • logs.tail, secret.value, metrics.query — логи, один ключ секрета, метрики;

  • storage.get, set, remove — своё хранилище, до 1 МиБ JSON на расширение;

  • backend.call — вызов своего бэкенда;

  • ui.openDetails, ui.navigate, ui.notify, ui.close — открыть карточку объекта или страницу Sextant, показать короткое уведомление.

Окно не передаёт фрейму данных объектов напрямую. В контексте есть только имена выбранных объектов, а сами объекты расширение читает через resources.get, и этот вызов проверяет страж. Ещё в контексте лежат CSS-переменные темы, и страница расширения выглядит как родная в светлой и тёмной теме.

Почему так. Хост регистрирует кнопки, колонки и пункты меню сам, по манифесту, и для этого ему не нужно запускать расширение. Отсюда три свойства. Расширение, которое упало, не ломает отрисовку Sextant. Окно согласия показывает всё, что расширение добавит, до установки. А выключение срабатывает мгновенно: один вызов снимает все его вклады.

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

Чем платим. Расширение не может перерисовать чужой экран или добавить пункт туда, где для него нет точки встраивания. Каждая новая точка — это изменение схемы манифеста и новый ADR. Счётчика на разделе бокового меню, например, пока нет.

Решение 4. Бэкенд — компонент WebAssembly, а не нативный процесс

У страницы расширения нет сети, и для просмотра объектов кластера она и не нужна. Но иногда нужно сходить в системы вокруг кластера: API облака, дежурный пейджер, трекер задач. Для этого в 0.26 появился бэкенд расширения: компонент WebAssembly, который Sextant запускает в wasmtime.

Риск тут выше: бэкенд работает в том же процессе, где лежат kubeconfig, токены и токен MCP-сервера. Побег из песочницы означал бы доступ ко всем вашим кластерам сразу. Поэтому у компонента нет ничего «по умолчанию»:

  • Интерфейсы WASI подключены, но пусты. Нет переменных окружения, аргументов и каталогов, любой адрес сокета отклоняется. Работают только часы и случайные числа.

  • Лишние импорты — отказ при установке. Компонент может импортировать только wasi:* и sextant:extension/*, иначе дальше окна согласия дело не дойдёт.

  • Кластеры — через того же стража. host.call даёт те же методы, что и странице, с теми же правами. Второго словаря прав нет.

  • Сеть — только к объявленным хостам. До 20 имён без масок, только HTTPS на 443. При каждом разрешении имени проверяется каждый адрес: loopback, link-local, частные сети и сервис метаданных облака отклоняются. Так DNS не сможет направить объявленное имя внутрь вашей сети. Прокси из окружения и cookies не используются.

  • Секреты — в системной связке ключей. Токен пользователь вводит в форме Sextant, а не в iframe. Прочитать его может только бэкенд этого расширения; во фрейм, логи и тексты ошибок он не попадает.

  • Каждый вызов с чистого листа. Новый store на каждый вызов: 10 секунд, 64 МиБ памяти, 1 МиБ JSON на вход и выход, не больше 4 вызовов сразу. Три падения подряд ставят бэкенд на паузу.

Бэкенд зовут только страницы его же расширения. AI-агенты и другие расширения до него не дотягиваются. Своих таймеров и фоновой работы у него нет: он живёт, пока открыт фрейм, и ещё 60 секунд после. В журнал пишется каждый HTTP-запрос: хост, метод, статус, размер и время, без тела и строки запроса.

Писать бэкенд можно на TypeScript или на Rust. TypeScript собирается через componentize-js в компонент со встроенным движком JS, и обычный fetch работает в рамках сетевой политики. Rust собирается под wasm32-wasip2 и выходит около 0,3 МБ. Вот целиком бэкенд из примера SDK, чуть сокращённый:

import { defineBackend, host } from "@sextant/extension-sdk/backend";

export const handle = defineBackend({
  async summary() {
    const clusters = host.clusters.list().length;          // только выданные кластеры
    const token = host.credentials.get("github-token");    // из связки ключей
    const answer = await fetch(
      "https://api.github.com/repos/FriendsOfKubernetes/sextant/releases/latest",
      { headers: token ? { authorization: `Bearer ${token}` } : {} },
    );
    const release = await answer.json();
    return { clusters, latest: release.tag_name };
  },
});

А в манифесте — единственный разрешённый хост и один секрет:

"backend": {
  "module": "backend.wasm",
  "network": { "hosts": ["api.github.com"] },
  "credentials": [{ "id": "github-token", "label": "GitHub token (optional)" }]
}

Запрос к любому другому хосту из этого fetch закончится ошибкой. Go через TinyGo и C/C++ через wasi-sdk тоже собираются под наш WIT, но без гарантий. Python не поддержан: компонент с CPython весит 20–40 МБ и не проходит по лимиту в 16 МиБ.

Что рассматривали и отбросили:

  • Нативный процесс-спутник. Для него нужна своя песочница на каждой ОС: seccomp и Landlock на Linux, App Sandbox с подписью на macOS, AppContainer на Windows. Пока их нет, нативный код может всё, что может пользователь. Да и в каждом пакете пришлось бы везти по бинарнику на платформу.

  • Встроенный движок JavaScript (QuickJS). Авторам привычнее, но это движок на C, где любая ошибка памяти — побег из песочницы, и без типизированной границы.

  • wasmi. Маленький интерпретатор, но без component model: границу пришлось бы писать руками, перекладывая байты в памяти модуля.

  • wasmer. Тот же JIT, но слабее поддержка component model.

Выбрали wasmtime. Он написан на Rust, постоянно фаззится и имеет процесс работы с уязвимостями. Он реализует component model и WIT, поэтому граница типизирована: каждый вызов хоста — одна проверяемая функция. И в нём есть прерывание по эпохам и ограничитель ресурсов — именно ими сделаны лимиты времени и памяти. Движок живёт в отдельном крейте, и ядро Sextant от него не зависит.

Чем платим. У бэкенда нет фоновой работы: опрашивать API и слать уведомления, когда окно закрыто, он не может. Это отдельное решение о том, что работает незаметно и как его остановить. Новый store на каждый вызов стоит времени на старт, а компонент на TypeScript весит около 12 МБ, потому что везёт с собой движок JS.

Решение 5. Декларации вместо фонового кода: проблемы, метрики, переводы

Чтобы расширение могло заменить встроенную интеграцию, ему не хватало трёх вещей. Оно не могло сообщить о проблеме, пока его страница закрыта. Не могло прочитать метрики оператора. И говорило только на одном языке, а Sextant говорит на четырнадцати. В 0.28 все три пробела закрыты одним принципом: расширение описывает, что ему нужно, а делает Sextant.

Проблемы — правила, которые считает Sextant

Держать код расширения живым в фоне ради уведомлений — это лишняя память и код, который работает, когда никто не смотрит. Поэтому расширение приносит правила, а считает их сам Sextant на своих же watch:

{
  "id": "health-error",
  "title": "Ceph health error",
  "kind": { "group": "ceph.rook.io", "kind": "CephCluster" },
  "when": { "type": "equals", "path": ".status.ceph.health", "value": "HEALTH_ERR" },
  "since": ".status.ceph.lastChanged",
  "message": "Ceph reports HEALTH_ERR; the cluster's Ceph status lists the health checks"
}

Условия — фиксированный набор: equals, in, exists, absent, condition (запись из .status.conditions), above, below, olderThan и all/any глубиной до трёх уровней. Ничего не исполняется как код. Пути проверяются ещё при чтении манифеста: правило, которое заглядывает в data секрета или в встроенный секрет другого вида, отклоняется.

Где правило работает, решает Rust: только в тех пространствах имён, где расширению разрешено чтение, и только для видов, на которые пользователь согласился. Проблемы попадают на страницу «Проблемы» и в уведомления в своей категории с отдельным выключателем. А вот AI-агентам они не отдаются. Само расширение ничего из прочитанного для правил не получает.

Метрики — только запросы из манифеста

Дать расширению произвольный PromQL значило бы дать ему любой ряд любого пространства имён, и из окна согласия нельзя было бы понять, что оно читает. Поэтому запросы лежат в манифесте, подписаны вместе с пакетом и показаны в окне согласия слово в слово:

{
  "id": "health",
  "title": "Ceph health (0 OK, 1 WARN, 2 ERR)",
  "query": "max(ceph_health_status{namespace=\"{{namespace}}\"})",
  "params": [{ "name": "namespace", "type": "namespace" }],
  "range": { "maxHours": 168 }
}

Расширение шлёт только id запроса и значения параметров. Параметры типизированы (namespace, name, labelValue, duration), и подставляет их Rust с экранированием. При проверке манифеста шаблон разбирается на токены: текстовый параметр вне строки в кавычках и комментарии # отклоняются. Источник — Prometheus или VictoriaMetrics, который Sextant уже нашёл в кластере, через service proxy API-сервера. Ответ ограничен: 8 МиБ, 500 рядов, 1500 точек, диапазон не длиннее недели.

Обновление, которое меняет хоть один символ запроса, снова спрашивает согласие. Окно согласия честно говорит и о слабом месте: Prometheus может вернуть ряды пространств имён, которые расширению читать нельзя. В журнал пишется идентификатор запроса с параметрами, но не ответ.

Переводы — в манифесте и в SDK

Тексты, которые показывает сам Sextant (название, заголовки страниц, колонок, действий, сообщения проблем), переводятся прямо в манифесте, полем translations. Переведённое сообщение проблемы обязано содержать ровно те же {{…}}, что и английское: перевод не может тайком читать лишнее поле. Запросы метрик и права не переводятся никогда, и в окне согласия они всегда такие, как написаны.

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

Для текстов на страницах расширения в SDK есть createI18n: каталоги вшиваются в страницу (сети ведь нет), множественные числа выбираются по CLDR, числа и даты форматируются по языку интерфейса. catalogProblems проверяет каталоги по тем же правилам, что и тест локалей самого Sextant.

Чем платим. Язык правил намеренно маленький: правило смотрит на один объект, и правил по нескольким объектам сразу пока нет. Что на нём не скажешь, остаётся на страницах расширения. Каждое новое условие или тип параметра меняет схему сразу в Rust и в SDK. А фиксированный запрос метрик всё равно может вернуть ряды чужих пространств имён: декларация делает это видимым, но не запрещает.

Решение 6. Каталог: доверие к ключу издателя, а не к индексу

Поставить расширение можно из папки, файла или по ссылке на релиз GitHub, но главный путь — каталог: Настройки → Расширения → Каталог. За ним стоит публичный репозиторий FriendsOfKubernetes/sextant-extensions.

Каталог не хранит пакеты. Каждое расширение — это один файл extensions/<id>.json, который добавляется пулл-реквестом и проходит ревью. В нём имя, описание, репозиторий автора на GitHub и его публичный ключ minisign. Сами пакеты лежат в релизах автора.

Раз в сутки и после каждого мержа workflow каталога собирает index.json. Для каждой записи он берёт последний релиз автора, проверяет подпись ключом из записи и прогоняет пакет через валидатор SDK. В индекс попадают версия, размер, SHA-256, права и вклады. Вручную индекс никто не правит.

Sextant скачивает индекс с одного зашитого адреса, только когда вы открываете каталог и копии больше часа, или по кнопке «Обновить». Фонового опроса нет, и одна галочка в настройках выключает каталог целиком. Индекс — недоверенный текст: картинки из него не грузятся, поля обрезаются и чистятся, поиск идёт локально.

Когда вы нажимаете «Установить», окно передаёт бэкенду только id. Дальше бэкенд делает всё сам:

  1. Строит адрес релиза из проверенных полей записи и скачивает пакет и .minisig.

  2. До распаковки сверяет размер и SHA-256 с индексом и проверяет подпись ключом автора.

  3. Сверяет id и версию из манифеста с записью.

  4. Показывает окно согласия, собранное из скачанного манифеста, а не из копии в индексе.

  5. После установки запоминает репозиторий и ключ автора.

Пакет приходит от издателя, а доверие держится на его ключе

Пакет приходит от издателя, а доверие держится на его ключе

Индекс и пакет идут разными путями: индекс только подсказывает, что и откуда качать, а решает подпись издателя, которую Sextant проверяет сам.

Из-за последнего шага взлом самого каталога не даёт подменить уже установленное расширение. Если в индексе появился другой ключ или репозиторий, Sextant откажется обновлять: «Ключ автора в каталоге изменился. Удалите расширение и установите заново». Версия ниже установленной не предлагается никогда. Автоматических обновлений нет: каждое проходит через окно согласия.

Если в расширении нашли проблему, запись можно пометить отозванной с причиной. Sextant перестанет его предлагать и покажет причину тем, у кого оно уже стоит. Выключить или удалить его решает пользователь: сам Sextant не трогает ничего.

Ревью в каталоге проверяет запись, автора и запрошенные права, а не код каждого релиза. На каждое право автор даёт объяснение в пулл-реквесте, а запрос значений секретов или удаления везде должен убедить ревьюера. Релиз, который просит больше, всё равно упрётся в окно согласия на машине каждого пользователя.

Что рассматривали и отбросили:

  • Спрашивать GitHub API прямо из приложения. Без авторизации это 60 запросов в час, по запросу на каждое расширение, и ещё один хост в сетевой поверхности.

  • Свой сервер каталога. Его кто-то должен держать, а репозиторий на GitHub бесплатно даёт ещё и историю ревью.

  • Подпись на самом индексе. Индекс пишет workflow, значит ключ лежал бы в секретах CI — именно там, куда доберётся атака на каталог.

  • X.509 или Sigstore. Тянут за собой цепочку сертификатов или онлайн-журнал, до которого приложению пришлось бы достучаться. minisign уже используется для обновлений самого Sextant, а его ключ помещается в одну строку пулл-реквеста.

  • Пулл-реквест на каждую версию. Ревьюеры стали бы узким местом, а подпись мало что добавила бы к проверенной контрольной сумме.

  • Удалённый рубильник. Кнопка, которая из файла в репозитории выключает код на машинах пользователей, — это власть, которой у индекса быть не должно.

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

Инструменты автора: одна схема для Rust и TypeScript

Типичная беда платформ расширений — SDK и приложение по-разному понимают манифест: пакет собрался, а у пользователя не ставится. Поэтому правила манифеста живут в одном месте — в JSON-схеме ядра, и её читают и Rust, и упаковщик SDK. То, что схемой не выразить, — пути к полям в правилах, токены PromQL-шаблонов, плейсхолдеры переводов, — проверяют две реализации на общем файле тестовых случаев manifest-cases.json.

SDK лежит в репозитории Sextant, в папке sdk/, и не публикуется в npm. Его типы сверяются с типами, сгенерированными из Rust, а шаблон и четыре примера (hello-page, crd-view, backend-ts, backend-rust) собираются и тестируются в CI вместе с приложением. Изменение API, которое ломает пример, не пройдёт проверки.

Автор получает тестовый хост для тестов без Sextant, режим разработки, в котором Sextant раз в секунду перечитывает папку с расширением, и команды для ключа и подписи в формате minisign. Путь от шаблона до записи в каталоге описан в вики Writing extensions и в CONTRIBUTING каталога.

Проверка на реальной задаче: Rook Ceph

Rook Ceph — это тест всей платформы на настоящей задаче. Оно написано только на публичном API, без единого изменения в ядре ради него, и выходит из каталога со своими версиями и своим ключом подписи. Если чего-то в API не хватало, это становилось задачей и ADR, а не частным хуком. Так и появились правила проблем, запросы метрик и переводы.

Страница Rook Ceph в Sextant

Страница Rook Ceph в Sextant

Страница расширения в разделе Storage: здоровье и ёмкость каждого CephCluster, пулы, файловые системы и хранилища со StorageClass на них. Всё это рисует iframe расширения.

Что оно показывает:

  • страницу в разделе Storage: здоровье каждого CephCluster, ёмкость, пулы, файловые системы и объектные хранилища со StorageClass на них;

  • каждую проверку здоровья Ceph обычными словами, по документации Ceph;

  • отметку «устарело», если оператор 15 минут не обновлял статус;

  • демоны: оператор, мониторы, менеджеры, OSD по узлам, MDS, RGW и драйверы CSI;

  • графики здоровья, ёмкости, OSD и placement groups из метрик Ceph mgr;

  • дерево CRUSH и состояние каждого OSD;

  • 10 правил проблем: HEALTH_ERR сразу, HEALTH_WARN через 15 минут, застрявшие пулы через 10.

Секция Ceph status в деталях CephCluster

Секция Ceph status в деталях CephCluster

Секция расширения в деталях CephCluster и его вкладки Daemons, CRUSH and OSDs, Metrics рядом со своими вкладками Sextant. Проверки здоровья объяснены обычными словами.

Что оно меняет (с версии 1.1.0): обновление Ceph, замену диска, остановку и удаление OSD, расположение узла в CRUSH, настройки пулов, MDS и шлюзов, создание пулов, файловых систем, хранилищ и StorageClass, заглушенные проверки здоровья, развёртывание toolbox.

И всё это — только через объекты самого Rook: его поля, метки, аннотации, toolbox и Job очистки OSD из его примеров. Команд ceph расширение не запускает никогда, да и не может: exec расширениям не дан. Перемещения в CRUSH и reweight остаются в toolbox, в терминале Sextant.

Правила безопасности тут строгие:

  • Сначала показать, потом отправить. Каждое изменение показывается точным патчем или YAML, а потом Sextant спрашивает подтверждение.

  • Минимальный патч против той версии, что вы видели. Merge patch несёт resourceVersion показанного объекта. Если Rook или коллега успели его поменять, будет конфликт. Списки меняются только JSON patch с операцией test: merge patch заменил бы список целиком.

  • Обновление Ceph проверяется по таблице версий Rook. Запрещены неподдерживаемая версия, даунгрейд, прыжок через мажорный релиз, обновление при HEALTH_ERR и при демонах, уже работающих на двух версиях.

  • Удаление OSD идёт по руководству Rook. Сначала остановка, потом проверка, что все placement groups active+clean, потом Job очистки без --force-osd-removal.

  • Каждый патч проверен на CRD Rook. Скрипт отправляет каждое изменение в тестовый кластер kind как server-side dry run. Запросы метрик проверены promtool.

Подтверждение Sextant перед изменением пула

Подтверждение Sextant перед изменением пула

Расширение просит поменять число копий в пуле, и страж Sextant показывает точный патч с resourceVersion. Без ответа запрос сам отклонится через две минуты.

Расширение говорит на всех 14 языках Sextant, включая 210 текстов своих страниц. Ставится из каталога в два клика — после окна согласия с полным списком прав и запросов метрик.

Итог: требования и чем они закрыты

Требование

Чем закрыто

Нет доступа к IPC, файлам, kubeconfig и токенам

iframe без allow-same-origin; бэкенд в wasmtime с пустым WASI и проверкой импортов

Нет значений Secret без разрешения

значения вырезаются в ядре; secretValues — отдельное право; встроенные секреты других видов скрыты

Нет изменений без спроса

проверка в Rust прямо перед действием; подтверждение для modify и delete; один страж с AI-агентами

Нет утечки данных в сеть

connect-src 'none' у страниц; бэкенд — только объявленные HTTPS-хосты, адреса проверяются после DNS

Нет тихого расширения прав

новые права, хосты, правила и запросы метрик снова спрашивают согласие; ключ издателя закреплён

Расширение умеет то же, что встроенная интеграция

вклады, правила проблем, запросы метрик, переводы; проверено на Rook Ceph

Что осталось открытым:

  • отказ IPC Tauri для Origin: null проверен руками только на Linux, macOS и Windows покрыты CI;

  • нет счётчиков на разделах меню, правил по нескольким объектам и диагностики для агентов — без них не вынести из ядра часть интеграций;

  • у бэкенда нет фоновой работы, а Python не помещается в лимиты;

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

Как это делалось

Платформа расширений росла три ступени за две недели:

Дата

Версия

Что появилось

31 октября

0.28

правила проблем, запросы метрик, переводы, Rook Ceph в каталоге

29 октября

0.26

каталог с подписями minisign, бэкенды на WebAssembly

15 октября

0.6

расширения в песочнице, права, страж, SDK

Как и весь Sextant, код написал Claude Code. Человек ставил задачи, выбирал между вариантами и смотрел каждое изменение. Для расширений это особенно важно: что расширению нельзя, решал человек, а не модель.

Каждое серьёзное решение записано в ADR: контекст, решение, отброшенные варианты и последствия. Про расширения их 18: от ADR-0047 про песочницу до ADR-0141 про то, что Rook Ceph имеет право менять. Разделы «что рассматривали» в этой статье взяты оттуда.

Сейчас у Sextant 1790 тестов на Rust, 4355 на vitest и 376 в SDK. Все они идут на каждом изменении на трёх ОС.

Что дальше

Следующий шаг — вынести из ядра одну небольшую существующую интеграцию как пилот. Для этого API ещё не хватает нескольких вещей: счётчика на разделе бокового меню, правил по нескольким объектам сразу и диагностики для AI-агентов. Каждая дыра станет отдельным ADR. План целиком — в #320.

Если вы держите свой оператор или вам нужен экран для чужого, попробуйте написать расширение. Если чего-то в API не хватит, напишите в issues: так и появились правила проблем и запросы метрик.

Ссылки:

Sextant бесплатный и таким останется: все возможности для всех, без платной версии. Если он экономит вам время, можно поставить звезду на GitHub или угостить автора кофе.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.