unissh – современный, опенсорсный SSH клиент с selfhosted zero-knowledge сервером для синхронизации данных

от автора

Доброго времени суток, читатели!

Хочу представить вам unissh – современный, простой опенсорсный SSH клиент с selfhosted zero-knowledge сервером для синхронизации данных.

главный экран

главный экран

Заранее предупреждаю, что проект – чистый вайбкод на расте + таури, но мы с друзьями старались его хорошо протестировать.

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

Первой, и наверное основной причиной стало то, что я лично не смог найти нормальных кроссплатформенных ssh клиентов с синхронизацией данных между устройствами. Моя личная потребность на протяжении наверно уже десятка лет, заключается в том, что мне очень удобно иметь доступ к серверам и с телефона, и с ноутбука. Идея всегда носить с собой макбук, мне не слишком нравится 😉

Существующие решения с поддержкой синхронизации предлагают что-то из приведенного ниже:

  1. синхронизация данных через iCloud (работает только для устройств из экосистемы Apple)

  2. синхронизацию данных через внешние файлохранилища (например GitHub, гугл диск и прочие подобные решения)

  3. сомнительный дизайн с моей точки зрения (я считаю это довольно важным нюансом)

  4. отсутствие шифрования данных

  5. отсутствие возможности экспортировать собственные данные без подписки

Я считаю, что нейронки должны нести в том числе и какую-то пользу для большого количества людей. Поэтому, решая собственную проблему – я выбрал навайбкодить свой собственный SSH клиент и сервер с веб-панелькой к нему, чисто для синхронизации волтов между устройствами.

И вот, спустя два месяца разработки рад представить вам результаты вайбкуколдинга:

Возможности клиента

  1. Кроссплатформенность – Windows, macOS, Linux, iOS, Android

  2. Терминалы – сплитование вкладок в разных вариациях, дублирование, переименовывание и прочие базовые фичи

  3. SFTP – для работы с загрузкой / выгрузкой файлов с серверов; в настройках можно менять еще количество активных подключений для SFTP, если вы работаете с сотнями и тысячами файлов; также есть базовый текстовый редактор внутри

  4. Два режима массового выполнения команд:

    1. Broadcast – открытие множества ссш сессий к выбранным серверам и наблюдение за выводом команд

    2. Fleet exec – от первого варианта отличается тем, что команды просто посылаются на сервер и показывается exit code с каждого сервера

  5. Секреты – SSH-ключи, пароли для серверов, идентичности, заметки; также поддерживается импорт из ~/.ssh/config; имеются истории версий у секретов

  6. SSH-туннели (Local, Remote, Dynamic)

  7. Запись сессий в asciicast v2 формат

  8. Сниппеты – вы всегда можете создать необходимые вам сниппеты для быстрого их выполнения на сервере

  9. Гибкая кастомизация интерфейса и поддержка множества тем и их светлых / темных вариаций, и возможность создавать свои собственные темы. Сейчас из дефолтных тем приложений: Mono, Nebula, Barbie (да, барби тема 😉 и также несколько тем для терминала

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

Архитектура проекта

Общее кроссплатформенное ядро (rust-core) – это workspace из девяти крейтов, в котором живет вся крипта, волты на SQLCipher, SSH-стек (russh), встроенный in-memory ssh-агент и логика синхронизации. Клиенты подключаются к ядру через UniFFI, а веб-панелька для администраторов сервера – вообще через это же ядро, просто скомпилированное в wasm. Важное замечание: крипта и SSH не переписываются под каждую платформу. Клиент на iOS, десктопный клиент и админка в браузере дергают один и тот же код. Меньше возможностей для ошибок у слоп-машин 😉

Клиенты – Tauri v2 + React, один кодбейс на все пять платформ (macOS, Windows, Linux, iOS, Android). Клиент по сути тонкий: UI, xterm.js для терминала, а все чувствительное за FFI-границей в ядре.

Сервер – маленький бинарь на Rust (axum + sqlx, SQLite или PostgreSQL на выбор), который нужен только если вы хотите синхронизацию между устройствами или командную работу.

Главное про сервер: он ничего не знает

Сервер – это тупое хранилище шифроблобов с логикой членства и версий. Все данные шифруются на устройстве до отправки: ключи выводятся из Secret Key (это ваш Emergency Kit, как в 1Password) + пароля через Argon2id. Сервер хранит ciphertext, проверяет Ed25519-подписи записей и раздает дельты другим устройствам, но не выполняет никакой криптографии над содержимым. Он физически не может расшифровать волт, выписать себе доступ или подделать запись. Максимум, на что способен скомпрометированный сервер – не отдать или задержать данные.

И второй принципиальный момент: SSH-трафик никогда не ходит через сервер синхронизации. Соединение с вашими хостами всегда идет напрямую с устройства. Сервер синхронизации синкает только зашифрованные волты – если он упал, потерялся, взломали – к серверам вы все равно подключаетесь как ни в чем не бывало.

Отдельно порадовало, как получилось решить онбординг нового устройства: заходите с нового девайса через escrow sign-in (хэндл + пароль (при наличии) + Secret Key) – кейсет восстанавливается и расшифровывается прямо на устройстве, до сервера он не доезжает. Аккаунт один на все устройства, поэтому если коллеги выдали вам доступ к волту – он работает сразу везде. При этом у каждого девайса свой id, так что потерянный телефон можно отозвать отдельно, не убивая аккаунт.

Zero-knowledge не означает «сервер не видит ничего». Метаданные видны по дизайну: айди волтов и элементов, версии, tombstone’ы, публичные ключи участников, роли, sync-цели, размеры блобов и тайминги синхронизации. Не видит имен, содержимого, ключей волтов, элементов и приватных ключей. Тоесть скомпрометированный сервер узнает, что у вас 3 волта и 47 элементов, которые вы обновляли вчера в полночь, но не что в них.

И второе: не все гарантии криптографические, часть – server-trusted, то есть держится на корректном поведении сервера. Прежде всего это отзыв доступа: когда вы выкидываете участника, немедленный «отлуп» ему обеспечивает сервер. Криптографический отзыв тоже есть – через ротацию ключа волта и epoch floors на клиентах, но это следующая линия обороны, а не мгновенная.

Мы стараемся явно разделять эти два класса гарантий в документации, а полный разбор есть в репозитории в THREAT_MODEL.md и server/README.md.

Безопасность

Мы с слопусом не переизобретаем собственную криптографию – все построено на проверенной базе: RustCrypto, hpke, SQLCipher, Argon2id для вывода ключей, Ed25519 (verify_strict) для подписей.

Волты zero-knowledge: все содержимое шифруется на клиенте до отправки. На диске волт лежит в SQLCipher, а внутри per-item ключи, которые под per-vault ключом: компрометация одного элемента не раскрывает соседние, а сервер хранит только шифрованный текст, служебные метаданные и не выполняет никакой криптографии над содержимым.

Секреты не размазываются по памяти и диску: они зануляются после использования (zeroize), плейнтекст приватных ключей никогда не пишется на диск самим приложением, только через явный экспорт в менюшке секретов. Страницы памяти с ключами по возможности лочатся через mlock, чтобы не уехать в своп. Secret Key хранится только в системном кейчейне / Secure Enclave. Из приятных мелочей – настраиваемая авто-очистка буфера обмена после копирования пароля.

Секретная граница закрыта контрактом и тестом: секреты выходят из ядра только при вашем явном вызове: показать пароль или заметку, экспортировать ключ, сделать зашифрованный бэкап. Этот список зафиксирован в одном месте – тестом, который поименно перечисляет все методы, которые возвращают секреты и проверяет их type-gating вживую: попросить пароль у элемента-заметки или экспортировать ключ, который на самом деле пароль – ошибка. Новый метод, возвращающий секреты, обязан быть внесен в этот список явно – так поверхность утечки остается обозримой и не разрастается втихую. Туда честно внесен даже листинг сниппетов – с комментарием, что в сохраненных командах регулярно живут токены и хостнеймы.

Хост-ключи пинятся, классический TOFU: при первом подключении ключ хоста запоминается, и если он вдруг поменялся – соединение принудительно останавливается и вам показывается HostKeyMismatch с возможностью принять новый хост-ключ.

Целостность можно проверить локально: в ядре есть возможность проверки подписей по всем версиям записей, включая историю и tombstone’ы, — и check_consistency для структурной проверки базы.

Транспорт только TLS 1.3: либо in-process rustls, либо Caddy, либо ваш собственный reverse proxy.

Про неподписанные бинарники: я честно пока так и не придумал, что можно с этим сделать, поэтому релизы не подписаны сертификатами Apple/Microsoft и при первом запуске ОС на вас ругнется.

Вместо сертификата у каждого релиза три механизма проверки:

  1. файл SHA256SUMS по всем артефактам

  2. minisign-подпись поверх него – публичный ключ лежит в SECURITY.md, так что подмену чексумм прямо на странице релиза можно обнаружить

  3. SLSA-провенанс через GitHub attestations: команда gh attestation verify <файл> —repo goduni/unissh доказывает, что артефакт собран публичным CI из публичного коммита, а не у меня на коленке

Автообновления десктопного клиента проверяются отдельным Ed25519-ключом, вшитым в приложение. Ключ, подписывающий «что вы скачали руками», намеренно не совпадает с ключом, авторизующим «что исполнится на вашей машине без спроса» – сливать эти два радиуса поражения в один было бы плохой идеей.

Ну а самый базовый путь никто не отменял – собрать все из исходников и доверять только своему компилятору.

Селфхостим сервер за пять минут

Напомню еще раз: сервер опционален и совершенно необязателен. Клиент из коробки работает в локальном режиме, и если синхронизация вам не нужна – можно вообще ничего не поднимать и дальше этот раздел не читать 😉

Но мы-то с вами помним, ради чего все затевалось – синхронизация для телефона и ноутбука, поэтому поднимаем.

Самый быстрый способ поднять сервер:

curl -O https://raw.githubusercontent.com/goduni/unissh/main/compose.prod.ymlcurl -o .env https://raw.githubusercontent.com/goduni/unissh/main/deploy/.env.example# в .env вписать UNISSH_DOMAIN=ssh.example.comdocker compose -f compose.prod.yml up -d

В компоузе в том числе есть Caddy – он сам сходит за сертификатом в Let’s Encrypt, сам терминирует TLS и сам же раздает веб-админку.

Если у вас нет домена, и поднимаете в локалке: UNISSH_TLS_DIRECTIVE="tls internal" и Caddy выпишет самоподписанный серт.

База по умолчанию – SQLite в докер-вольюме, и для личного пользования этого хватит с запасом. Захотелось посерьезнее – Postgres включается парой строк в .env. Наружу торчат только 80/443, сервер бежит non-root на read-only rootfs, метрики и внутренний порт остаются внутри compose-сети. Есть надежды на то, что за такой деплой перед безопасниками (при их наличии) не будет стыдно 😉

Знаете эти прекрасные квесты «создайте первого админа через переменную окружения, потом удалите ее, потом поменяйте пароль»? Так вот, тут этого нет, но есть немного другой 😉

Сервер печатает в логи одноразовый setup code:

docker compose logs server 2>&1 | grep -i "setup code"

Открываете клиент или админку, вводите адрес своего сервера и этот код – теперь вы владелец инстанса. Если вам требуется кого-то пригласить в пространство, то это уже через инвайт-ссылки (или прикручиваете корпоративный SSO через OIDC, если у вас все серьезно) – код им уже не нужен. Автоматизируете деплой ансиблами-терраформами? Код можно запинить детерминированно через UNISSH__SETUP__CODE.

Проверить, что все ожило: curl -k https://your-domain/healthz

Админ-панель сервера

Веб-панель – SPA, внутри которой крутится то же самое Rust-ядро, что и в клиентах, только скомпилированное в wasm. Тоесть вся крипта выполняется у вас в браузере: заходите по хэндлу + паролю (при его наличии) + Secret Key, кейсет восстанавливается и расшифровывается прямо на странице и до сервера не доезжает. Кнопка Lock – и он вычищается из памяти. Никаких «импортируйте файлик с ключом» (но справедливости ради, мне пришлось над этим поработать, так как такое реально было): новый браузер подтверждается QR-кодом с уже доверенного устройства, как в мессенджерах.

Внутри же пространства, участники, устройства и сессии, инвайты, волты с грантами, аудит. Есть опциональный ops-токен для break-glass сценариев, но он открывает только инфраструктурные ручки и не расшифровывает ровным счетом ничего.

По поводу бэкапов подсвечу: если вы восстановили сервер из старого снапшота, клиенты это заметят и откажутся синкаться с ошибкой TransportRollback – пока вы явно не поднимете версию через seq-bump. А еще старый restore может воскресить удаленный элемент. Это не баги, а anti-rollback защита делает свою работу. Но лучше узнать про нее из README, а не во время аварии 😉

Вместо заключения

Два месяца назад я просто хотел SSH-клиент, который не заставляет выбирать между удобно, безопасно и не платить подписку за доступ к собственным серверам. Теперь он у меня есть – и у вас тоже, если захотите.

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

Проект полностью открыт под двойной лицензией MIT / Apache-2.0. Никакой платной версии, облачной подписки или энтерпрайз-фич за деньги нет и не планируется: это тулза, которую я делал для себя, а сервер вы и так сами поднимаете.

Буду рад, если вы потестируете клиент – все пять платформ лежит в релизах, сервер поднимается за пять минут из готовых образов или по инструкции выше.

По поводу найденных багов, пожеланий и всего такого – пишите в issues, либо в чатик проекта в телеграме. Уязвимости – не в ишьюсы, а в идеале на uni@goduni.me. Я серьезно: проект написан нейронкой, внешнего аудита криптографии не было, и каждый человек, который внимательно посмотрит в исходники, поможет сделать его лучше. Особенно приглашаю тех, кто в комментариях уже готовит тезис «вайбкоду нельзя доверять секреты» – вот вам открытый код, THREAT_MODEL.md и почта для репортов.

Ближайшие планы – собрать ваш фидбек, в надежде на то, что оно пригодится в том числе не мне одному, и допиливать согласно вашим пожеланиям.

Ссылки:

Спасибо, что дочитали! Буду рад вашим звездочкам на репозитории, комментариям и обратной связи!

ссылка на оригинал статьи https://habr.com/ru/articles/1068896/