Обычный день дежурного: терминал с тремя SSH-сессиями, Lens с продом, вкладка Grafana, вкладка ArgoCD, DBeaver, KeePassXC и заметки, где записано, как в прошлый раз чинили то же самое. Семь окон, и нужное всегда под шестью остальными. OpsDeck обещает собрать всё это в одно нативное приложение для Linux, Windows и macOS.
Обещание амбициозное, а проекту две недели: первый коммит сделан 25 сентября 2026 года, версия 0.6.1 вышла 8 октября. За это время — 194 коммита, 8 релизов, 154 звезды. Приложение просит доступ ко всему, что у инженера есть ценного: kubeconfig, пароли, SSH, базы. Поэтому я не стал смотреть скриншоты, а пошёл читать код.
Сразу про метод: я разбирал исходники версии 0.6.1 и само приложение не запускал. Всё, что ниже, — это то, что написано в коде, а не то, что я увидел на экране. Главный вопрос у меня был один: можно ли такому приложению доверить доступы. Ответ в конце, и он не «да» и не «нет».
Что это и из чего состоит
OpsDeck — приложение на Tauri 2. Это значит: бэкенд на Rust, интерфейс — веб-страница в системном webview, никакого Electron с собственным Chromium. Отсюда размер: установщик для Windows весит 10 МБ, .deb — 18 МБ, .dmg — 14–16 МБ. Только AppImage тянет на 90 МБ, потому что возит с собой WebKitGTK.
Внутри две части почти равного размера:
| Часть | Где лежит | Объём |
|---|---|---|
| Бэкенд | src-tauri/src/, 32 файла на Rust |
12 405 строк |
| Интерфейс | src/, TypeScript без фреймворка |
10 852 строки |
| Интеграция с shell | src-tauri/shell/ |
bash, zsh, PowerShell |
| Тесты | tests/, плюс тесты в файлах Rust |
68 на Rust, 139 на TypeScript |
Интерфейс общается с бэкендом через команды Tauri — в lib.rs их зарегистрирована 171. Самые толстые модули: базы данных (db.rs, 1504 строки), Kubernetes (k8s.rs, 1454) и алерты (alerts.rs, 1196).
Разделов много: терминал, Kubernetes, базы, редактор кода с git, веб-панели, алерты, KeePass, SSH, MikroTik, заметки с задачами, сетевые утилиты, доска мониторинга хостов. По умолчанию включены пять — терминал, Kubernetes, SSH, KeePass и заметки, остальные ставятся галочкой.
Примечательно, чего в зависимостях нет: SSH-библиотеки. OpsDeck не изобретает SSH заново, а запускает системный ssh в своём терминале; так же он поступает с kubectl для shell в под и port-forward. Kubernetes API, наоборот, он читает сам, через библиотеку kube-rs, и даже список Helm-релизов достаёт напрямую из секретов кластера типа helm.sh/release.v1 — системный helm нужен только для отката. Это разумная граница: таблицы и логи быстрее рисовать из API, а интерактивный exec проще отдать kubectl.
Kubernetes: собственное хранилище kubeconfig
Первое, что мне понравилось. OpsDeck не трогает ~/.kube/config. Выбранные контексты копируются в его собственное хранилище, по одному файлу на контекст. А терминалы внутри приложения получают переменную KUBECONFIG, которая указывает только на это хранилище:
/// KUBECONFIG for terminals started by OpsDeck: only OpsDeck's store, so kubectl/helm/…
/// there never touch the shared ~/.kube/config. None when the user opted into system configs.
pub fn terminal_kubeconfig() -> Option<String> {
Если контекстов ещё нет, подставляется пустой конфиг — чтобы kubectl не откатился на общий файл. Последствие, о котором надо знать заранее: kubectl в терминале OpsDeck не видит кластеры, которые есть в обычном терминале, пока их не импортировали. Это не баг, а выбранное поведение, и отключается оно настройкой.
Второе — импорт kubeconfig с блоком exec. Такой конфиг при каждом обращении запускает внешнюю команду, чтобы получить токен, и чужой файл с exec — классический способ выполнить код на машине инженера. OpsDeck при импорте вытаскивает эти команды и показывает их. Мелочь, но я не помню, когда в последний раз видел её в GUI для Kubernetes.
Режимы «только чтение» и их границы
README говорит: для продовых контекстов есть режим «только чтение», и изменения блокирует бэкенд. Проверяю. В k8s.rs есть функция ensure_writable, и она вызывается в пяти местах: применение YAML, удаление, масштабирование, перезапуск и действия Argo CD.
fn ensure_writable(ctx: &Ctx) -> Result<(), String> {
if k8s_prefs_get().readonly.contains(&ctx_key(ctx)) {
return Err(format!("контекст «{}» в режиме только чтения — снимите 🔒, чтобы менять ресурсы", ctx.context));
}
Ok(())
}
Проверка действительно на стороне Rust, а не в интерфейсе — обойти её через DevTools не получится. Но замок закрывает только кнопки самого OpsDeck. Shell в под и kubectl в терминале приложения работают с теми же учётными данными, и команда k8s_shell_config, которая готовит для них kubeconfig, режим «только чтение» не проверяет. То есть замок защищает от случайного клика по «Delete», а не от kubectl delete в соседней вкладке. Для настоящего запрета записи нужен отдельный пользователь с ролью на чтение — и никакой клиент этого не заменит.
С базами картина разная, и README это честно оговаривает — проверил по db.rs:
| СУБД | Как сделано «только чтение» | Чья это защита |
|---|---|---|
| PostgreSQL | параметр сессии default_transaction_read_only=on |
сервера |
| ClickHouse | параметр запроса readonly=2 |
сервера |
| MySQL / MariaDB | SET SESSION TRANSACTION READ ONLY и фильтр по первому слову запроса |
сервера и клиента |
| Redis | список разрешённых команд | клиента |
| MongoDB | список разрешённых команд, запрет $out и $merge |
клиента |
Фильтр для SQL — это список из десяти разрешённых первых слов (SELECT, SHOW, EXPLAIN, WITH и так далее) плюс запрет INTO OUTFILE и FOR UPDATE. На тестах он ловит select 1; drop table t. Как страховка от опечатки — годится. Как граница безопасности — нет: это разбор SQL по первому слову.
Там же есть вещь, которую легко проглядеть в форме подключения. У TLS три режима: off, require и verify. require шифрует соединение, но сертификат сервера не проверяет — в коде для него заведён отдельный верификатор, который принимает любой сертификат. Имя режима взято из PostgreSQL, где sslmode=require значит то же самое, так что это не ошибка. Но для прода я бы ставил только verify.
Где лежат секреты
README обещает: секреты — только в системном хранилище или KeePass. По коду это почти так.
Пароли коннекторов, баз, SSH и ключ к внешней языковой модели уходят в системное хранилище через библиотеку keyring: Secret Service на Linux, Keychain на macOS, Credential Manager на Windows. Настройки без секретов лежат в JSON в ~/.config/opsdeck/ с правами 0600.
KeePass открывается только на чтение, база расшифрована в памяти, мастер-пароль затирается через zeroize. Скопированный пароль стирается из буфера через 30 секунд. Файл базы отслеживается: если его изменили в KeePassXC, OpsDeck перечитает его тем же ключом. Автоблокировка по времени есть, проверяется каждые три секунды.
Теперь про «почти». Три оговорки:
- Токен приёма алертов лежит в
connectors.json, а не в системном хранилище. Файл закрыт правами0600, а токен годится только для отправки алертов на локальный порт, так что ущерб от утечки невелик. Но утверждение «только в хранилище» буквально неверно. - На Windows права на файлы не выставляются. Функция
restrictтам пустая, расчёт на то, что профиль пользователя и так закрыт. Строка README про «права 600/700» относится к Linux и macOS. - Kubeconfig с токенами лежат файлами в том же каталоге. Иначе и быть не может, но при переносе настроек на другой компьютер это важно. Экспорт сделан аккуратно: пароли в архив не попадают вовсе, а раздел с kubeconfig по умолчанию выключен и снабжён предупреждением.
И одна вещь, которую README называет сам: WinBox принимает пароль только аргументом командной строки. Пока он запущен, пароль роутера виден в списке процессов. Автор не стал это прятать, и за это ему плюс.
Веб-панели с автоматическим входом
Grafana, ArgoCD и GitLab открываются вкладками внутри окна и сами логинятся. Звучит опасно: приложение подставляет пароль в чужую страницу. Смотрю, как сделано.
Каждая панель — отдельный webview, и в него перед скриптами страницы внедряется небольшой скрипт входа. Первая его строка:
if (location.origin !== cfg.origin) return;
Учётные данные лежат в замыкании, а не в window, и скрипт сохраняет ссылку на родной fetch до того, как страница успеет его подменить. Попытка входа — не чаще раза в минуту, чтобы неверный пароль не заблокировал учётку. Адрес, на который панель переводят из алерта, сверяется с origin коннектора. На экранирование есть тест с паролем вида p"a\ss</script><script>alert(1).
Вторая половина защиты — файл разрешений Tauri. В нём одна запись: доступ к командам приложения есть только у окна main. У webview с Grafana его нет:
{
"description": "IPC for the main OpsDeck window only; connector windows (Grafana/ArgoCD/GitLab) get no IPC.",
"windows": ["main"],
"permissions": ["core:default"]
}
Это правильная архитектура. Взломанная Grafana получит то же, что получила бы в браузере, — свой пароль. До kubeconfig и KeePass она не дотянется.
На Linux, кстати, встраивание сделано через GTK: главный webview заворачивается в GtkOverlay, и панели перекладываются поверх него. Автор сам называет это хрупким местом и оставил переменную OPSDECK_NO_EMBED=1, с которой панели открываются отдельными окнами.
Какие порты открывает приложение
README: «OpsDeck не слушает внешние интерфейсы». Это правда — оба слушателя привязаны к 127.0.0.1. Но их два, и оба работают с первого запуска.
Приём алертов, порт 9095. HTTP-сервер для локальных анализаторов логов. Включён по умолчанию (ingest_enabled: true) и запускается при старте приложения безусловно — в lib.rs нет проверки, включён ли раздел алертов. Защищён он прилично: токен в заголовке, сравнение за постоянное время, тело не больше 2 МБ и проверка заголовка Host против DNS rebinding — страница из браузера, чей домен указывает на 127.0.0.1, получит отказ.
Мост для ИИ-агента, случайный порт. OpsDeck притворяется редактором кода: поднимает WebSocket-сервер по протоколу MCP и кладёт в домашний каталог lock-файл с номером порта и токеном. Консольный агент, запущенный в терминале приложения, находит его и подключается. Запросы с заголовком Origin отклоняются — так отсекаются браузеры.
Мост стартует всегда, даже если агентом никто не пользуется. Рабочей папкой он объявляет весь домашний каталог. А его набор инструментов наполовину состоит из заглушек, и некоторые из них отвечают успехом, ничего не делая:
"saveDocument" => text(json!({ "success": true, "filePath": args["filePath"], "saved": true, "message": "Document saved" }).to_string()),
Реально работают выделение текста в заметке, открытие заметки и ссылка на файл. Вреда от заглушек нет — файлы агент и так пишет сам. Но ответ «сохранено» на несохранённый документ я бы не оставлял: агент ему поверит.
Локальная языковая модель
По Ctrl+Shift+K можно описать задачу словами и получить команду. Модель ставится по желанию и работает на llama.cpp.
Что уходит модели, видно в ai_command: текущая папка, тип оболочки, десять последних команд и до восьми похожих команд из заметок. Команды собирает отдельный индекс — он читает .bash_history, .zsh_history, историю fish и блоки кода из заметок.
С локальной моделью всё остаётся на машине: llama-server запускается на 127.0.0.1 на случайном порту и гасится через 15 минут простоя. Но в настройках можно указать внешний сервер с OpenAI-совместимым API. Тогда тот же контекст — папка, история, команды из заметок с именами хостов и неймспейсов — уезжает туда. И если адрес введён без схемы, подставляется http:// и порт 11434. Для Ollama на соседней машине в своей сети это нормально. Для сервера в интернете — нет.
Загрузки проверяются, но по-разному:
- Модели — четыре варианта Qwen с Hugging Face, контрольные суммы SHA-256 зашиты в код. Подменить файл на стороне хостинга не выйдет.
- Движок — архив llama.cpp из релиза
b11351на GitHub. Его контрольная сумма берётся из метаданных того же релиза. Это защищает от битой загрузки, а не от подмены релиза. А если поля с суммой в ответе нет, архив ставится без проверки.
Я бы зашил хеши движка в код так же, как хеши моделей: тег всё равно зафиксирован.
Хорошая деталь: команда, которую предложила модель, сама не выполняется. Она подставляется в терминал, и нажать Enter должен человек.
Что уходит в сеть
Я выписал все адреса из кода бэкенда. Кроме того, что настроил сам пользователь, приложение обращается к трём местам:
github.com/LeoAlecksey/opsdeck— проверка обновлений и список релизов;api.github.com/repos/ggml-org/llama.cpp— движок модели, только при установке;huggingface.co— файл модели, только при установке.
Телеметрии и счётчиков нет. Журнал пишется в файл на диске — пять файлов по 5 МБ с ротацией.
Интерфейс зажат политикой CSP: скрипты только свои (script-src 'self'), сетевые запросы из интерфейса запрещены вовсе (connect-src — только IPC), фреймы и отправка форм закрыты. Единственная щель — картинки: img-src разрешает https: и http:. Заметка с внешней картинкой при открытии сходит за ней в сеть. Для совместимости с Obsidian это понятно, но знать об этом стоит.
Интерфейс написан без фреймворка и собирает разметку строками: в src/ 153 обращения к innerHTML и 310 вызовов функции экранирования esc. Markdown заметок проходит через DOMPurify. Проверить руками все 153 места я не возьмусь, но CSP без unsafe-inline для скриптов здесь страхует: пропущенное экранирование не превратится в выполнение кода.
Обновления и подпись
Приложение обновляется само, из GitHub Releases. Обновления подписаны ключом minisign, открытый ключ зашит в tauri.conf.json, файл с чужой подписью не установится. Релиз публикуется, только если прошли все сборки и тесты; до этого он остаётся черновиком.
Есть и аварийный откат: отдельный workflow помечает версию как pre-release, и установленные копии предлагают вернуться на предыдущую. Для проекта, который выпускает по релизу раз в полтора дня, это нужная вещь.
Чего нет: подписи кода для операционных систем. Сборки для macOS подписаны «ничем» ("signingIdentity": "-"), и README предлагает снимать карантин командой xattr -cr. Это честно описано, но означает, что подлинность установщика при первой установке подтверждает только то, что он скачан со страницы релизов на GitHub.
В CI — npm audit, cargo audit, CodeQL и clippy. Сторонние действия закреплены наполовину: rust-toolchain и rust-cache — по хешу коммита, а tauri-action@v0, который собирает и подписывает релиз, — по плавающему тегу. Я бы начал закреплять именно с него.
Установка и запуск
Готовые сборки лежат в Releases: .deb, .rpm, .AppImage, .msi, .exe, .dmg. Требования жёстче, чем кажется: на Linux нужны glibc 2.35 и WebKitGTK 4.1, поэтому Ubuntu 20.04, Debian 11 и RHEL 9 с клонами не подходят.
Из исходников, по README:
git clone https://github.com/LeoAlecksey/opsdeck.git && cd opsdeck
npm install
npm run tauri dev # режим разработки
npm run tauri build # установщики → src-tauri/target/release/bundle/
Нужны Rust stable и Node.js 20+, на Linux — пакеты разработчика WebKitGTK и GTK. Зависимости ставятся из npm и crates.io, оба lock-файла в репозитории есть.
Что остаётся работать, пока приложение открыто: два локальных порта, опрос источников алертов раз в минуту, а если установлена модель — процесс llama-server. При выходе lock-файл моста удаляется, сервер модели останавливается.
Внешние программы — kubectl, helm, ssh, mtr, dig, WinBox — не обязательны: OpsDeck использует их, если найдёт. Сетевые утилиты запускаются без shell, а адрес проверяется функцией valid_host: всё, что начинается с дефиса, отвергается, чтобы имя хоста не стало флагом.
Лицензии
Проект под MIT. Зависимости обычные для такого стека: Tauri, kube-rs, xterm.js, CodeMirror, DOMPurify. Движок llama.cpp и модели Qwen в поставку не входят и скачиваются отдельно, со своими лицензиями. Собранных без исходников частей в репозитории нет.
Ограничения и риски
- Возраст. Две недели и восемь релизов. Код меняется быстрее, чем его успевают читать.
- Один автор. Из 194 коммитов 177 сделал один человек. Сторонние участники есть — четверо, 15 коммитов, — но проект держится на одном разработчике.
- Всё в одном процессе. Kubeconfig, пароли, SSH и базы доступны одному приложению. Уязвимость в любом разделе — это уязвимость рядом со всеми секретами сразу. Разделение на webview и CSP это смягчают, но не отменяют.
- Режим «только чтение» — защита от случайности, а не разграничение доступа.
- Запись сессии терминала пишет вывод в текстовый файл в папке документов. Всё, что напечатал
cat .env, окажется там. - Обновления безопасности — только для последней версии, так сказано в
SECURITY.md. - Сквозных тестов на macOS нет: настоящее приложение прогоняется только на Linux и Windows.
Кому подойдёт
Возвращаюсь к вопросу из начала: можно ли доверить этому приложению доступы.
По коду видно, что автор понимает, с чем работает. Изоляция webview от команд приложения, собственное хранилище kubeconfig, показ exec-команд при импорте, сравнение токенов за постоянное время, проверка Host, подписанные обновления, честный список известных проблем — так не пишут случайно. Для двухнедельного проекта это необычно аккуратно.
Но двухнедельный проект остаётся двухнедельным. Поэтому мой ответ такой:
- Стенды, домашняя лаборатория, свои кластеры — да, и стоит попробовать уже ради терминала с блоками команд и логов сразу со всех подов.
- Прод — только с учётными записями на чтение, выданными на стороне кластера и базы, а не замком в интерфейсе. И без внешнего сервера модели.
- Компании с требованиями к рабочим станциям — рано. Нет подписи кода, нет аудита, один мейнтейнер.
- Тем, кто пишет своё на Tauri, — репозиторий стоит прочитать как пример: встраивание чужих страниц без доступа к IPC, автологин с привязкой к origin и откат релизов сделаны здесь нагляднее, чем в документации.
Через полгода к проекту стоит вернуться. Если темп сохранится, а хеши движка и подпись кода появятся, разговор будет уже другим.