Почти в каждом проекте рано или поздно возникает одна и та же непритязательная задача: перенести артефакт сборки из на сервер и запустить его. На это никто не закладывает время — «это же просто деплой», — поэтому решают её один раз, под дедлайном, самым быстрым из возможных способов. Обычно это приватный SSH-ключ в секретах CI и шелл-скрипт, который копирует файл по SCP и перезапускает сервис. Работает. Но не масштабируется дальше первого проекта — а мы пересобирали чуть разные версии этой связки для каждого клиента, пока наконец не сели и не написали инструмент, который нам действительно был нужен: Orosu.

От японского 降ろす (órosu) — «разгружать», «сгружать».

Orosu — небольшой сервис на Rust, orosu-server, который устанавливается один раз на машину. Вместо SSH-ключа, открывающего CI доступ к шеллу, CI получает ключ Ed25519, которым можно запустить ровно один из заранее заданного набора скриптов — через аутентифицированное -соединение. Эта статья о том, почему эта разница достаточно важна, чтобы ради неё написать отдельный инструмент, и как это выглядит на практике.

Ключевые выводы

  • Проблема не в самом SSH, а в том, что подразумевает SSH-доступ. Ключ, которым можно залогиниться и запустить один деплой-скрипт, может запустить что угодно. Любой креденшл, добравшийся до продакшн-машины, наследует весь радиус поражения шелла — нужен он задаче или нет.
  • Orosu сужает это до закрытого набора заранее заданных скриптов. CI выбирает скрипт по имени и передаёт аргументы; произвольную команду передать нельзя, что бы ни попало в скомпрометированный пайплайн.
  • Аутентификация — это подпись, а не секрет в канале. Каждая задача подписывается ключом Ed25519 клиента. Пароля или общего секрета, который можно перехватить, в канале не существует.
  • Это один бинарник на Rust без рантайм-зависимостей — ставится через apt или готовый бинарник из GitHub Releases и работает за любым обратным прокси, который уже терминирует TLS на этой машине.
  • Опциональный слой сквозного шифрования закрывает единственный разрыв, который оставляет TLS на прокси, — сохраняет аргументы скрипта и вывод конфиденциальными даже от самого этого прокси.
  • Orosu — open source-проект под лицензией Apache-2.0 — мы сами им пользуемся и предпочли бы, чтобы другие команды перестали изобретать это заново.

Форма проблемы

Уберите обёртку с «деплоя из CI» — и это всегда один и тот же запрос: выполнить скрипт на той машине с этими файлами. Способ, которым большинство команд закрывает этот запрос по умолчанию, накапливает риск по одному и тому же предсказуемому паттерну:

  • Приватный SSH-ключ попадает в секреты CI. Обычно он не ограничен одним скриптом — он даёт доступ к шеллу целиком, потому что ограничить SSH «только этой одной командой» достаточно хлопотно, чтобы почти никто этого не делал.
  • Тот же ключ, или почти его копия, переиспользуется во всех репозиториях и пайплайнах, которым нужен доступ к этому серверу, потому что генерировать и ротировать отдельный ключ на каждый пайплайн — это больше процесса, чем кто-либо готов на себя взять.
  • Деплой-скрипт копируется из прошлого проекта, мелкие различия накапливаются, и никто уже не помнит, какая версия — эталонная.
  • Продакшн становится напрямую доступен с CI-раннеров — а это ровно тот класс хостов, на который целится атакующий, потому что скомпрометированный раннер с SSH-доступом — это уже скомпрометированный сервер.
  • В день, когда ключ действительно утекает, его ротация означает поиск всех пайплайнов, которые могли на него ссылаться, — потому что реестра никогда не было, только секреты, разбросанные по всем репозиториям, которые успели обзавестись копией.

Это не проблема квалификации. Это стандартная форма задачи «выполнить скрипт на удалённой машине из пайплайна», когда инструмент, за которым тянутся, — это универсальный удалённый шелл. SSH не плох для того, чтобы залогиниться и подебажить машину руками. Он неверный примитив для CI-задачи, которой нужно запускать одни и те же три-четыре деплой-скрипта.

Чего мы на самом деле хотели

Ещё до того, как писать код, требования были не столько про криптографию, сколько про то, что должен и не должен уметь деплой-креденшл:

  1. Креденшл CI не должен уметь открывать шелл. Он должен уметь запускать скрипт по имени из списка, который контролирует владелец сервера, — и точка.
  2. Аутентификация не должна зависеть от секрета, который передаётся с каждым запросом. Подпись доказывает владение ключом, не раскрывая его.
  3. Добавление нового деплой-таргета не должно означать генерацию новой пары SSH-ключей и ручное протаскивание её через конфиг сервера — это должна быть запись в конфиге и команда keygen.
  4. Источником истины о том, что можно запускать, должен быть сервер, а не пайплайн. Скомпрометированная или небрежная CI-задача никогда не должна уметь расширить собственные права.

Этот список — WebSocket-протокол, а не старый SSH под новой обёрткой. Именно этим Orosu и стал.

Как это работает

orosu-server работает на целевой машине. Он слушает WebSocket-соединения, проверяет подпись Ed25519 у каждой входящей задачи, сверяет имя скрипта с настроенным для этого клиента allowlist'ом — и только если всё сошлось, запускает скрипт с теми аргументами и файлами, которые пришли вместе с задачей.

listen:
  tcp: "127.0.0.1:8081"
clients:
  - name: my-ci-client
    secret_file: /etc/orosu/my-ci-client.pub
    scripts:
      - name: deploy
        command:
          - "bash"
          - "/etc/orosu/scripts/deploy.sh"

Этот список scripts: — вся модель безопасности в одном месте. my-ci-client может запустить deploy и ничего больше — не другой скрипт на той же машине, не произвольную команду шелла, не скрипт другого клиента. Если пайплайн CI, у которого есть ключ этого клиента, скомпрометирован, атакующий наследует ровно возможности одного деплой-скрипта, а не логин.

Пара ключей клиента получается через orosu-keygen:

orosu-keygen --name my-ci-client --private-key-output my-ci-client.key --public-key-output my-ci-client.pub

Публичная половина идёт в конфиг сервера выше; приватная становится секретом CI и используется только для подписи запросов — сама по себе она не даёт сессию. Запуск скрипта из GitHub Actions — через сопутствующий Action:

- name: Deploy
  uses: orosu-ci/orosu@v0
  with:
    address: ${{ secrets.OROSU_SERVER_URL }}
    script: deploy
    key: ${{ secrets.OROSU_CLIENT_KEY }}
    arguments: ${{ github.sha }}

Вот и всё изменение в пайплайне: шаг с SSH становится шагом с Orosu, а серверная поверхность атаки сжимается от «всё, что может шелл этого ключа» до «этот один именованный скрипт».

Сам orosu-server обычно слушает localhost и стоит за обратным прокси, который терминирует и пробрасывает апгрейды WebSocket, — nginx, в типичном случае, та же машина, которая, скорее всего, уже делает это для сервиса, который перезапускает деплой-скрипт.

Почему не просто ограничить SSH-ключ через command=

SSH технически поддерживает ограничение command= в authorized_keys, и нас часто спрашивают, почему Orosu — не просто это. Две причины, по которым такого ограничения недостаточно на практике, и обе — о том, что происходит вокруг счастливого пути, а не о самом счастливом пути:

  • Это конфиг на стороне сервера, который правят вручную под каждый ключ, — и правку не видит автор пайплайна. Ничто не гарантирует, что каждый деплой-ключ в authorized_keys действительно несёт ограничение command= — у файла нет схемы, и неограниченный ключ рядом с девятью ограниченными выглядит абсолютно так же на первый взгляд.
  • Ограниченная SSH-сессия всё равно говорит на языке шелла. В зависимости от скрипта и того, как command= взаимодействует с аргументами, которые пробрасывает SSH, границу между «выполнить ровно это» и «выполнить ровно это, но с аргументами, куда можно что-то внедрить» легко провести неправильно, не заметив этого. Аргументы скрипта в Orosu — это поля протокола, а не токены шелла, собранные из командной строки SSH: шелла, из которого мог бы сбежать аргумент, в этом пути просто нет.

Orosu не утверждает, что SSH небезопасен. Он утверждает, что безопасная версия «ограничить SSH одной командой» требует достаточно дополнительной церемонии, чтобы почти никто не делал это последовательно, — а инструмент, единственная задача которого — запускать один из нескольких заранее заданных скриптов, может сделать это поведением по умолчанию, а не опцией, которую нужно включать вручную.

Конфиденциальность за обратным прокси

WSS закрывает канал, но когда TLS терминируется на обратном прокси перед orosu-server — а это и есть стандартная схема выше, — аргументы скрипта, загруженные файлы и вывод остаются открытым текстом на этом прокси. Для большинства деплой-скриптов это не проблема; для некоторых — проблема (аргумент деплоя может быть креденшлом, который скрипт передаёт дальше, токеном отката, идентификатором клиента). Ответ Orosu — опциональный слой : X25519 для согласования ключей, HKDF-SHA256 для деривации сессионных ключей, ChaCha20-Poly1305 для шифрования, — который работает поверх WebSocket-транспорта и включается независимо с обеих сторон:

orosu-keygen --kind server --private-key-output server.key --public-key-output server.pub

Публичный ключ сервера добавляется в его конфиг и в параметр server_key GitHub Action. Сервер с настроенным шифрованием по-прежнему обслуживает клиентов, не передающих server_key, точно так же, как раньше, — скоординированного перехода не требуется, дня «Х» не будет, и ничего не сломается у клиента, который ещё не обновился.

Честно о защите

Мы относимся к этому как к части инструмента первого класса, а не как к галочке в чек-листе. Несколько конкретных фактов — потому что заявление «нам важна безопасность» лучше подкреплять тем, что реально изменилось: релиз 0.7.0 добавил проверку атаки low-order-point на обмене X25519 (закрывающую известный класс атак на эллиптические кривые), защитил извлечение вложений так, что сформированный вручную или чрезмерно большой zip не может выйти за пределы каталога извлечения или исчерпать диск через распаковку, и заставил orosu-keygen писать файлы приватных ключей с правами 0600 независимо от текущего umask. Все три изменения пришли как совместимое обновление — тот же конфиг, тот же протокол, тот же CLI. Полная история — в CHANGELOG.md.

Чем Orosu не является

Ограниченность здесь — фича, а не пробел, но стоит сказать об этом прямо. Orosu не оркестрирует раскатку релизов, не управляет откатами и не понимает топологию вашего деплоя — он запускает скрипт, который вы уже написали сами, и именно скрипт решает, что такое деплой. Это не Kubernetes-нативный инструмент: если ваш таргет уже кластер с настоящим контроллером деплоя, этот контроллер почти наверняка лучший выбор, а Orosu решает проблему, которой у вас уже нет. Он находит своё место там, где машина не нода кластера — одиночный VPS, голое железо, сервер «когда-нибудь мы его контейнеризируем», на котором до сих пор крутится часть стека у многих агентств и стартапов.

Быстрый старт

orosu-server и orosu-keygen поставляются как пакеты для Debian/Ubuntu:

curl -fsSL https://packages.nerdy.pro/NerdyPro.gpg | sudo gpg --dearmor -o /usr/share/keyrings/nerdy-pro.gpg
echo "deb [signed-by=/usr/share/keyrings/nerdy-pro.gpg] https://packages.nerdy.pro/ stable main" | sudo tee /etc/apt/sources.list.d/nerdy-pro.list
sudo apt update
sudo apt install orosu

GitHub Releases также публикует готовые бинарники — для машин, куда нельзя добавить apt-репозиторий. Полное руководство по настройке — генерация ключей, конфиг сервера, подключение GitHub Action — на странице проекта.

Orosu написан на Rust — том же языке, на котором написан dxpdf, наш движок конвертации DOCX в PDF. Это язык, за которым мы тянемся, когда инструмент должен быть быстрым, предсказуемым и безопасным по отношению к не полностью доверенному вводу — например, приложенному клиентом файлу, пришедшему по сети.

Часто задаваемые вопросы

Ограничение command= в authorized_keys может привязать ключ к одной команде, но это конфиг сервера, который правят вручную под каждый ключ и без схемы — ничто не мешает неограниченному ключу незаметно оказаться рядом с корректно ограниченными. Ограниченная сессия всё равно говорит на языке шелла, поэтому обработку аргументов приходится выверять вручную. Orosu делает allowlist скриптов поведением протокола по умолчанию, а не опциональной договорённостью, и передаёт аргументы как поля протокола, из которых некуда сбежать через шелл.
Атакующий наследует ровно те скрипты, на которые настроен ключ этого клиента, с теми аргументами, которые допускает протокол для этого скрипта, — никогда не произвольную команду шелла и никогда не скрипты другого клиента. Это заметно меньший радиус поражения, чем у скомпрометированного SSH-ключа, который обычно даёт полноценный логин-шелл.
Нет. Orosu — для машин, которыми ещё не управляет контроллер кластера: VPS, голое железо, сервер со смесью сервисов вне Kubernetes. Если у вашего таргета уже есть настоящий контроллер деплоя, он и есть лучший инструмент.
По умолчанию соединение защищено WSS/TLS, обычно терминируемым на обратном прокси. Опциональный слой сквозного шифрования (X25519 + HKDF-SHA256 + ChaCha20-Poly1305) дополнительно защищает аргументы скрипта, файлы и вывод от самого этого прокси и включается независимо на клиенте и сервере.
Да, под лицензией Apache-2.0. Сервер, CLI orosu-keygen и GitHub Action — всё это на GitHub, вместе с apt-репозиторием и готовыми бинарниками релизов.

Хотите перестать логиниться по SSH на сервер, куда деплоите?

Именно для этого нужен Orosu. Прочитайте полную документацию проекта — там всё про настройку, посмотрите остальные наши open-source проекты или свяжитесь с нами, если хотите, чтобы мы посмотрели, как ваша команда доставляет код в продакшн, — деплой-пайплайны, собранные под дедлайном, — это конкретная и частая находка в аудите AI-кода, а Orosu — тот самый фикс, который мы применяем не реже, чем рекомендуем.


Илья Никсан — основатель и ведущий разработчик Nerdy Production, Flutter-first агентства, которая строит и поддерживает в том числе инфраструктурные инструменты — такие как Orosu и dxpdf — на которых работает её собственная поставка.