Читать 6 мин

Мультиплексирование SSH-соединений: риски управляющих сокетов

Мультиплексирование SSH может сохранять аутентифицированный доступ после завершения задачи. Узнайте, как изолировать управляющие сокеты, закрывать сессии и сохранять доказательства.

Мультиплексирование SSH-соединений: риски управляющих сокетов

Мультиплексирование SSH-соединений может оставить открытым аутентифицированный путь к хосту после завершения команды, которая его создала. Это сделано намеренно. Но такое поведение легко неправильно понять, когда автоматизированная задача, запуск агента или задание развертывания объявляет себя завершенным.

Я видел, как команды расследовали якобы закрытое окно обслуживания и обнаруживали, что локальный master ssh по-прежнему удерживал активный транспорт, сокет принимал новые клиенты, а следующая команда повторно использовала его без новой интерактивной аутентификации. Ничего необычного не произошло. Конфигурация работала именно так, как описано в документации OpenSSH. Команда сочла завершение shell-команды завершением доступа.

Здесь важно различать завершенную команду клиента и остановленный аутентифицированный транспорт. Мультиплексирование разделяет эти события. Если автономная работа выполняется через SSH, очистка и сбор доказательств должны учитывать это различие.

Управляющий сокет может жить дольше открывшей его команды

SSH-мультиплексирование использует одно долго живущее SSH-соединение, называемое master, чтобы передавать работу последующим процессам SSH-клиента. В документации OpenSSH такие последующие клиенты часто называются slave. Если slave успешно обращается к локальному master через управляющий сокет, он не повторяет обычную настройку соединения и аутентификацию пользователя.

Типичная конфигурация выглядит безобидно:

Host build-box
    HostName 192.0.2.44
    User deploy
    ControlMaster auto
    ControlPath ~/.ssh/cm/%C
    ControlPersist 20m

Первый вызов ssh build-box создает сетевое соединение и Unix-сокет в ~/.ssh/cm/. Последующие команды, например ssh build-box 'uname -a', scp и sftp, могут использовать этот сокет. При ControlPersist 20m master остается доступным еще двадцать минут после закрытия последней сессии.

Поэтому следующая последовательность вполне обычна:

  1. Задача выполняет ssh build-box 'apply-change' и завершается с кодом 0.
  2. Master остается подключенным, потому что ControlPersist велит ему продолжать работу.
  3. Через девятнадцать минут другой локальный процесс выполняет ssh build-box 'read-status'.
  4. Процесс открывает канал через уже аутентифицированный транспорт.

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

В руководстве ssh_config OpenSSH говорится, что ControlMaster позволяет использовать несколько сессий через одно сетевое соединение, а ControlPersist оставляет master работать в фоновом режиме. Эти положения нужно читать вместе. ControlPersist это не просто настройка производительности. Она меняет период, в течение которого локальный процесс может запросить новые каналы на аутентифицированном соединении.

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

Одно событие аутентификации не равно одному событию команды

Команды часто смешивают понятия аутентификации, соединения, канала и команды. В SSH это разные вещи, и мультиплексирование хорошо показывает различие.

Исходный master создает TCP-соединение с сервером, проверяет хост, согласует криптографию и аутентифицирует учетную запись. После аутентификации он может открывать SSH-каналы. Команда shell, интерактивный shell, передача через SFTP, запрос локального перенаправления порта и запрос удаленного перенаправления используют каналы или связанные с ними запросы в этом транспорте.

Новый локальный вызов ssh, обнаруживший рабочий управляющий сокет, отправляет запрос через него. Master решает, открывать ли запрошенный канал. Ему не нужен приватный ключ и не требуется новый запрос к агенту. Серверу также не нужно получать еще одну попытку входа.

Это не означает, что OpenSSH хранит в сокете повторно используемый пароль. Такое описание неточно и приводит к неверному анализу. Проблема безопасности в уже аутентифицированном транспорте с локальным интерфейсом, через который можно попросить его выполнить действие. Атакующему, получившему доступ к сокету, исходные учетные данные могут вообще не понадобиться.

Это различие меняет вопросы при расследовании инцидента. Вопрос «Остался ли ключ на диске?» важен, но сам по себе не отвечает, сохранился ли доступ. Вместо этого спросите:

  • Остался ли master подключенным к удаленному назначению?
  • Мог ли другой локальный процесс обратиться к его управляющему сокету?
  • Принимал ли master дополнительные сессии, передачи файлов или запросы перенаправления?
  • Кто мог запуститься от имени владельца сокета или пройти через каталог сокета?
  • Когда master действительно завершил работу?

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

Общие сокеты превращают границы процессов в границы доступа

Управляющий сокет это локальный Unix-сокет. Его расположение и права файловой системы определяют, какие локальные процессы могут попытаться связаться с master. Поэтому широкий ControlPath способен объединить независимые задачи, даже если псевдонимы удаленных хостов выглядят аккуратно.

Рассмотрим учетную запись CI-раннера с такой конфигурацией:

Host *
    ControlMaster auto
    ControlPath /tmp/ssh-%r@%h:%p
    ControlPersist 1h

Задание A подключается как deploy к app.internal. Оно создает master и сокет в /tmp, после чего завершается. Задание B под той же локальной учетной записью подключается к той же цели. Если оно может узнать имя сокета и получить к нему доступ, то переиспользует аутентифицированный транспорт задания A.

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

Каталог важен не меньше самого файла сокета. В Unix процессу нужно право поиска в каталоге, чтобы обратиться к пути. Сокет в каталоге с закрытым владельцем и режимом 0700 дает гораздо более понятную границу, чем общий временный каталог. Права самого сокета тоже важны, но не считайте их единственным средством контроля.

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

set -eu
run_id="release-4821"
cm_dir="$HOME/.ssh/task-control/$run_id"
mkdir -p "$cm_dir"
chmod 700 "$cm_dir"

socket="$cm_dir/%C"
ssh -o ControlMaster=auto \
    -o ControlPersist=5m \
    -o ControlPath="$socket" \
    build-box 'id && hostname'

Токен %C не дает имени стать слишком длинным и уменьшает число конфликтов между разными наборами параметров соединения. В руководстве ssh_config OpenSSH он определен как хеш параметров соединения. Это полезно, но в хеш не входят ваш тикет развертывания, сессия агента или идентификатор задачи. В этом примере недостающую границу создает родительский каталог.

Не помещайте буквальный управляющий сокет в рабочую копию репозитория, каталог с широкими правами записи или /tmp только потому, что это удобно. Именно удобство превращает локальный сокет в случайно общую возможность.

ControlPersist это политика хранения, а не план очистки

ControlPersist велит SSH оставлять master доступным после закрытия клиентских сессий. Параметр принимает yes, что оставляет процесс работать в фоне бессрочно, или значение времени, например 10m. И то и другое это варианты хранения соединения.

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

В некоторых случаях короткий тайм-аут оправдан. Контролируемый автоматизированный процесс может выполнять несколько команд подряд и получать пользу от уже установленного соединения. Тогда сделайте тайм-аут короче ожидаемого простоя, изолируйте сокет для этого запуска и завершайте master при штатном выходе. Тайм-аут будет запасным механизмом при сбое, а не обычным способом закрытия.

Настройке ControlPersist yes нужен ответственный, которому действительно требуется долгий доступ. У администратора за рабочей станцией такой случай может быть. У одноразовой задачи его нет.

Еще одна распространенная ошибка состоит в предположении, что сбой команды закрывает master. Shell может досрочно завершиться после неудачной удаленной команды, пока фоновый master продолжает работать. То же возможно при отмене задачи. Очистка должна выполняться после успеха, ошибки и прерывания, а ее результат нужно записывать.

Если автоматизация использует trap, ограничьте очистку нужными действиями и проверяйте ее цель. Не удаляйте сначала путь к сокету вслепую. Удаление пути может затруднить расследование, пока процесс master и соединение продолжают существовать.

cleanup() {
    ssh -S "$socket" -O exit build-box >/dev/null 2>&1 || true
    rmdir "$cm_dir" 2>/dev/null || true
}
trap cleanup EXIT HUP INT TERM

Этот шаблон сначала просит master завершиться и только потом пытается удалить каталог. Если ssh -O exit завершается с ошибкой, сохраните каталог и разберитесь в причине, не стирая доказательства. В рабочем коде запишите в журнал задачи ошибку, идентификатор процесса и путь к сокету.

exit и stop означают разные действия

Не передавайте ключи SSH-агентам
Направляйте SSH через sp-ssh, а Sallyport будет хранить SSH-ключ в зашифрованном хранилище.

OpenSSH предоставляет управляющие команды через ssh -O. Операторы часто выбирают не ту, поскольку оба названия звучат как очистка.

ssh -O check host проверяет, работает ли master, и сообщает его идентификатор процесса, если получает ответ. ssh -O exit host просит master завершиться. Для законченной задачи обычно нужна именно exit, потому что она завершает повторно используемый транспорт.

ssh -O stop host велит master перестать принимать новые мультиплексированные сессии. Уже открытые сессии продолжают работать. Это может пригодиться оператору, который хочет дождаться завершения активной работы, но не подтверждает, что весь SSH-доступ закончился вместе с задачей. Работающий master с активными каналами все еще имеет активное сетевое соединение.

Используйте путь к сокету явно, если диагностика не должна зависеть от текущей конфигурации пользователя:

ssh -S "$socket" -O check build-box
# Master running (pid=41782)

ssh -S "$socket" -O exit build-box
# Exit request sent.

ssh -S "$socket" -O check build-box
# Control socket connect(...): No such file or directory

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

Есть важный неоднозначный случай. Сокет может остаться после сбоя master, а процесс может исчезнуть между двумя проверками. Устаревший путь не доказывает, что доступ еще существует. И наоборот, неотвечающая проверка не доказывает завершение удаленного соединения, если путь был удален до осмотра. Перед удалением проверьте таблицу процессов и открытые Unix-сокеты.

В macOS самым быстрым локальным инструментом проверки часто оказывается lsof:

lsof -nP -U | grep '/.ssh/task-control/'
ps -p 41782 -o pid=,ppid=,lstart=,etime=,command=

Сохраняйте вывод вместе с записью о задаче. Первая команда связывает Unix-сокет с процессом. Вторая показывает родительский процесс, время запуска, прошедшее время и сведения о вызове. Используйте grep как интерактивную помощь, а не как средство аудита. Настоящий сборщик должен напрямую запрашивать и сохранять нужные записи.

Перенаправление портов делает бездействующий master важнее

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

Локальное перенаправление открывает локальный слушающий порт, который передает трафик через SSH-соединение. При удаленном перенаправлении сервер начинает слушать порт и отправляет соединения обратно через клиента. Динамическое перенаправление создает SOCKS-прокси. Каждое из этих состояний может пережить команду, его создавшую, в зависимости от способа запуска сессии и master.

Руководства OpenSSH описывают управляющие команды вроде forward и cancel для запросов перенаправления при активном мультиплексировании. Это полезно в работе. Но это также означает, что процесс, получивший доступ к сокету, может запросить сетевые пути за пределами обычной shell-команды, с учетом политики сервера и состояния master.

Для автоматизированных задач сделайте перенаправление отдельным исключением. Записывайте адрес привязки, локальный или удаленный порт, целевой хост и порт, а также результат очистки. Не позволяйте общей SSH-обертке незаметно унаследовать параметры LocalForward, RemoteForward или DynamicForward из широкого пользовательского блока Host *.

Проверяйте разрешенные настройки до того, как доверять псевдониму:

ssh -G build-box | grep -E '^(controlmaster|controlpath|controlpersist|localforward|remoteforward|dynamicforward) '

ssh -G выводит итоговую конфигурацию после применения правил выбора хоста и значений по умолчанию. Такая проверка обнаруживает удивительно частую проблему: задача использует простой псевдоним хоста, но подключенный файл конфигурации включает мультиплексирование или перенаправление далеко от собственной конфигурации задачи. В разных версиях команда может показывать дополнительные поля. Сохраняйте полный вывод ssh -G, а не только ожидаемые строки.

Удаленная система может записать одно исходное соединение, через которое пройдет несколько перенаправленных соединений приложений. Сетевые данные, журналы SSH-сервера и журналы задачи отвечают на разные части этой истории. Ни один источник не заменяет остальные.

Одних серверных журналов недостаточно для восстановления локальной истории решений

Поставить SSH под контроль одобрений
Используйте встроенный MCP-адаптер, чтобы MCP-совместимый агент запрашивал действия SSH через Sallyport.

Удаленный SSH-сервер видит транспорт и все, что его настройки журналирования записывают о каналах и командах. Он не может надежно показать, почему локальному процессу разрешили использовать управляющий сокет, какой задаче принадлежал каталог сокета или использовал ли его другой локальный процесс после завершения исходной задачи.

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

Храните доказательства по обе стороны этой границы. Полезная запись о задаче включает:

  • полную итоговую конфигурацию клиента из ssh -G, без секретов;
  • удаленное имя хоста, адрес, учетную запись, результат проверки отпечатка хоста и исходный PID master;
  • путь к управляющему сокету, закрытый родительский каталог, наблюдаемые моменты создания и завершения;
  • каждую запрошенную удаленную команду, передачу или операцию перенаправления с ее кодом завершения;
  • результат check перед очисткой, результат exit, а также проверку процессов и сокетов после очистки.

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

Хеширование или подпись записи после сбора помогают обнаружить последующее редактирование, но не восполняют пропущенные события. Собирайте события жизненного цикла по мере их возникновения. Запись, составленная после инцидента из истории shell, служит слабым доказательством, особенно когда появляются фоновые master и повторные попытки.

Для работы под управлением агента различайте намерение агента и выполненное действие SSH. «Развернуть версию X» это намерение. ssh build-box 'sudo systemctl restart api' это действие. Запись повторного использования сокета показывает, получило ли действие новый транспорт или прошло через существующий. Это разные факты для аудита.

Сокет, ограниченный задачей, получает ответственного владельца

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

Практической обертке нужен понятный жизненный цикл. Она должна создать каталог с ограниченными правами, записать событие до первого соединения, выполнять команды с тем же явным ControlPath, остановить master, проверить результат и только после этого удалить пустой каталог. Если задача не может проверить очистку, она должна сообщить о ее сбое, даже если удаленная команда завершилась успешно.

В этом примере mktemp помогает не угадывать уникальное имя каталога:

set -eu
base="$HOME/.ssh/task-control"
mkdir -p "$base"
chmod 700 "$base"
cm_dir=$(mktemp -d "$base/run.XXXXXX")
chmod 700 "$cm_dir"
socket="$cm_dir/%C"

finish() {
    status=$?
    ssh -S "$socket" -O exit build-box >>"$cm_dir/cleanup.log" 2>&1 || \
        printf '%s\n' 'master exit request failed' >>"$cm_dir/cleanup.log"
    ssh -S "$socket" -O check build-box >>"$cm_dir/cleanup.log" 2>&1 || true
    exit "$status"
}
trap finish EXIT HUP INT TERM

ssh -o ControlMaster=auto \
    -o ControlPersist=2m \
    -o ControlPath="$socket" \
    build-box 'deployctl apply release-4821'

Короткая настройка ControlPersist помогает, если процесс завершился до срабатывания trap. Ее не следует воспринимать как разрешение другой задаче повторно использовать master. Случайный каталог блокирует такое повторное использование, поскольку вторая задача не знает путь первой и не наследует его.

Перед применением этого примера внесите одну поправку. Не записывайте в cleanup.log чувствительные аргументы команд, значения окружения или скопированные приватные данные. В журналах аудита должно быть достаточно сведений, чтобы установить, кто и что сделал, но они не должны превращаться в новое хранилище секретов. Редактируйте аргументы на границе обертки, пока еще понимаете их смысл.

Sallyport направляет действия SSH через встроенную stateless-утилиту sp-ssh, а SSH-ключи хранятся в его зашифрованном хранилище и не передаются агенту. Это устраняет распространенную проблему обращения с учетными данными, но командам все равно нужно определить границу действия и сохранять записи, показывающие, когда запуск завершился.

Глобальные настройки удобства разрушают границы задач

Одобрить запуск агента
Одобрите новый процесс агента один раз и увидьте его полномочия на подпись кода до начала работы.

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

Настройка может прийти из файлов Include, системы управления конфигурацией, личных dotfiles разработчика или образа сборки. Ее также можно переопределить в командной строке. Не судите о поведении по одному видимому файлу конфигурации. Используйте ssh -G с точным псевдонимом хоста и контекстом пользователя, которые будет использовать задача.

Если среда автоматизации не может гарантировать закрытый путь к сокету, отключите мультиплексирование для этого действия:

ssh -o ControlMaster=no \
    -o ControlPath=none \
    build-box 'maintenancectl status'

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

Не принимайте ControlMaster=auto за гарантию, что процесс будет повторно использовать только собственное соединение. auto означает, что клиент попытается найти master по настроенному пути и создаст новый, если не найдет. Именно настроенный путь определяет, чье соединение может быть найдено.

Некоторые команды считают общий сокет допустимым, потому что все задания работают под одной сервисной учетной записью. Это верно только в том случае, если каждое задание с этим Unix-идентификатором имеет одинаковые права доступа к каждой цели через эту учетную запись и команда принимает возможность наследования живого транспорта одного задания другим. В зрелых средах обычно этого не хотят.

Чистое завершение задачи требует доказательства закрытия транспорта

Не объявляйте SSH-задачу завершенной, когда возвращена последняя удаленная команда. Объявляйте ее завершенной, когда задача либо закрыла master и записала доказательство, либо сообщила, что сделать это не удалось.

В итоговой записи должно быть понятно, могло ли последующее действие повторно использовать соединение. Нужны идентификатор задачи, разрешенная конфигурация SSH, путь к сокету, сведения о процессе master, записи об активности, результаты явных управляющих команд и наблюдения после очистки. Также нужны удаленные доказательства изменений, продолжавшихся независимо от SSH, например перезапуска службы или отсоединенного процесса.

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

Если работа настолько чувствительна, что оставшийся аутентифицированный транспорт недопустим, не ускоряйте ее общим master. Откройте новое соединение, выполните действие, закройте его и сохраните запись. Несколько дополнительных секунд дешевле, чем объяснение, почему путь доступа сохранился после предполагаемого завершения задачи.

Вопросы и ответы

Может ли SSH ControlMaster оставить аутентифицированное соединение после завершения моей команды?

Да. Соединение ControlMaster проходит аутентификацию один раз, после чего другие SSH-клиенты могут просить локальный master открыть новые каналы. Если ControlPersist оставляет master работающим, такие каналы могут открываться уже после завершения исходной команды и задачи, которая ее запустила.

Чем отличаются SSH master и slave-соединение?

Процесс master является исходным SSH-соединением, которое владеет сетевым транспортом и локальным управляющим сокетом. Процесс slave это последующая команда ssh, которая обращается к этому сокету и просит master открыть сессию, перенаправление или другой канал.

Как безопасно закрыть общий управляющий сокет SSH?

Используйте ssh -O exit host-alias, если ваша конфигурация сопоставляет этот псевдоним с нужным ControlPath. Если нужно указать сокет напрямую, выполните ssh -S /path/to/socket -O exit host. Сначала проверьте соединение через -O check, чтобы случайно не завершить не тот процесс.

Безопасно ли использовать SSH-мультиплексирование в автоматизации?

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

Показывают ли журналы SSH-сервера каждую команду, переданную через мультиплексирование?

Удаленный SSH-сервер часто записывает один вход через транспорт и может не показывать каждую последующую локальную команду как новое событие аутентификации. Если нужен надежный отчет о действиях, собирайте записи о клиентских процессах, конфигурацию мультиплексирования, события жизненного цикла управляющего сокета и журналы команд на уровне задачи.

Можно ли полагаться на тайм-аут ControlPersist при очистке SSH?

Тайм-аут задает верхнюю границу, но не доказывает, что соединение завершилось одновременно с задачей. Используйте ssh -O exit при очистке, затем убедитесь, что сокет исчез, а ssh -O check завершается с ошибкой. Тайм-аут остается полезным запасным вариантом для аварийно завершившихся клиентов.

Где хранить управляющие сокеты SSH?

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

Что означает %C в SSH ControlPath?

ControlPath задает путь к Unix-сокету. %C раскрывается в хеш, вычисленный по атрибутам соединения, и помогает избежать ограничений длины пути и многих конфликтов имен. Однако сам по себе он не создает отдельный идентификатор задачи.

Отменяет ли закрытие SSH master уже начатую работу на удаленном хосте?

Нет. Закрытие мультиплексированного транспорта завершает аутентифицированное соединение на стороне клиента, но не отменяет удаленные изменения файлов, не завершает отсоединенный процесс и не отзывает учетные данные, которыми может пользоваться другая сессия. Удаленную работу нужно очищать отдельно.

Какие доказательства нужно сохранить после завершения автоматизированной SSH-задачи?

Начните с воспроизводимой записи: полностью разрешенная конфигурация SSH, идентификатор процесса master, путь к сокету, удаленный адрес, время запуска и завершения, а также результат явной команды остановки. Храните ее вместе с расшифровкой команд задачи и относящимися к делу журналами удаленного хоста.

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov