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

Автономные агенты, работающие с кодом, не должны переносить SSH-сеанс из одного запуска в следующий. Новый локальный контекст выполнения для каждого действия не позволяет предыдущему запуску передать следующему свои полномочия, личность или необъяснимые побочные эффекты. Проверяющим проще работать с такими записями: этот процесс запросил эту команду для этого хоста, а помощник вернул такой результат.
Но это не делает SSH безвредным. Новое соединение не отменит развертывание, не остановит фоновый процесс и не защитит учетную запись сервера, у которой слишком много прав. Stateless-выполнение решает более узкую, но очень практичную задачу: убирает локальное наследование состояния, которым агенты особенно легко пользуются случайно.
Stateless SSH-помощники убирают локальное наследование, но не состояние на сервере
Stateless SSH-помощник создает соединение для одного действия, выполняет его, возвращает результат и завершается, не сохраняя аутентифицированный транспорт для следующих вызовов. Следующее действие снова запускается в новом процессе помощника и начинает новую попытку подключения.
Это различие важно, потому что под «stateless SSH» часто понимают «ничего нигде не сохраняется». Это неверно. У SSH есть несколько мест, где может сохраняться состояние:
- Клиент может хранить master-соединение, сокет мультиплексирования, запись в known_hosts, сокет агента, конфигурацию или временные файлы.
- Удаленный хост может хранить рабочий каталог, историю оболочки, загруженный артефакт, lock-файл, процесс службы, кэш пакетов и измененную базу данных.
- Система авторизации может хранить учетную запись, SSH-сертификат, открытый ключ и разрешения.
Stateless-помощник работает с первой категорией. Он не позволяет следующему вызову агента незаметно унаследовать живое соединение или локальный контекст учетных данных предыдущего вызова. Удаленная сторона при этом остается настоящим компьютером с настоящими последствиями.
Поэтому в работе лучше говорить «новое соединение», а не «новый сервер». Помощник новый. Сервер нет.
Представим, что агент запускает проверку развертывания, а затем завершает работу после того, как проверяющий отклоняет следующую предложенную правку. Если первый запуск оставил сокет мультиплексирования, другой процесс на той же машине может подключиться к уже аутентифицированному каналу. Следующий процесс обходит границу безопасности, которая, как считали операторы, должна действовать для каждого запуска. С stateless-помощником второму процессу придется снова запросить действие и пройти предусмотренный путь авторизации.
Преимущество связано и с безопасностью, и с расследованием. Если проверяющий видит общий канал, который работал несколько часов, ему приходится восстанавливать, какие из нескольких процессов им пользовались. Когда каждый запрос управляет собственным жизненным циклом соединения, у записи есть естественные начало и конец.
Совместное использование соединения мешает связать действие с запуском
OpenSSH специально поддерживает совместное использование соединений. В руководстве ssh_config параметр ControlMaster описан как способ разрешить нескольким сеансам использовать одно сетевое соединение. ControlPersist может оставить master-соединение открытым в фоне после завершения клиентов. Это удобно администратору, который выполняет несколько команд из одного терминала. Но для запусков агентов, которым нужны отдельные полномочия и отдельные записи, это плохое значение по умолчанию.
Рассмотрим типичную конфигурацию:
Host build-box
HostName 10.0.0.24
User deploy
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 15m
Первый вызов SSH создает аутентифицированный master-сокет в ~/.ssh. Последующие вызовы, получившие к нему доступ, могут использовать его соединение в течение пятнадцатиминутного периода сохранения. Процесс агента, запущенный после завершения исходного процесса, может выглядеть в собственном журнале как независимое подключение, хотя на самом деле он использовал транспорт, созданный ранее.
Это создает четыре отдельные проблемы.
Во-первых, меняется смысл одобрения. Если человек разрешил конкретному процессу агента выполнить проверку, это одобрение не должно автоматически распространяться на новый процесс, который случайно нашел тот же сокет.
Во-вторых, сложнее своевременно проверить личность назначения. Проверка ключа хоста происходит при создании master-соединения. Последующие клиенты наследуют ее результат. Чтобы понять, какая проверка была выполнена, проверяющему придется искать более старое событие подключения.
В-третьих, журналы теряют простую связь между запросом и транспортом. Традиционный журнал SSH-сервера может записать один вход, а записи на уровне приложения покажут много команд. Оба журнала могут быть точными, но во время инцидента их сопоставление отнимает время.
В-четвертых, сам сокет становится ценным объектом. OpenSSH предупреждает в ssh_config, что любой, кто может получить доступ к control-сокету, получает доступ к соединению. Разрешения файлов помогают, но не делают общий аутентифицированный канал подходящим для враждебного или просто запутавшегося рабочего окружения.
Для вызовов, которым нужна четкая атрибуция, отключайте мультиплексирование. Это можно явно указать в прямом вызове:
ssh -o ControlMaster=no -o ControlPath=none deploy@build-box 'id; hostname'
Ожидаемый вывод имеет собственную форму команды, например:
uid=1004(deploy) gid=1004(deploy) groups=1004(deploy)
build-box
Команда доказывает только то, какая учетная запись и какой хост ответили. Она не доказывает, что учетная запись имела подходящие права или что запрошенная задача была безопасной. Но проверяющий получает одно действие, которое можно сопоставить с одной попыткой подключения.
Не считайте отсутствие строки ControlMaster полной исправленной проблемой. Ее может задать глобальная конфигурация SSH, добавить оболочка-обертка, а процесс может указать неожиданный ControlPath. Граница выполнения должна сама задавать параметры SSH, а не доверять dotfiles репозитория.
Для каждого запуска агента нужна собственная граница полномочий
Запуск агента это не человеческая терминальная сессия с более быстрым набором текста. Агент может получать новые инструкции из задач, pull request, сгенерированного вывода тестов, скопированного текста журналов и исходных файлов, которые он не создавал. Любое из этого содержимого может направить его к командам, не входившим в исходное намерение.
Это меняет смысл удобных функций. Человек обычно понимает, что сохраняет сеанс. Агент может начать следующую задачу, не зная, что старый канал все еще доступен. Его интерфейс инструментов часто показывает только «запустить команду», пока клиент незаметно находит сокет и получает прежние полномочия.
Считайте процесс агента единицей, которой выдается одобрение. Когда процесс завершается, вместе с ним должен закончиться и доступ. Если запускается новый процесс, даже из того же редактора или оркестратора, перед обращением к защищенному хосту нужно новое решение об авторизации.
Сначала такой подход кажется разработчикам слишком строгим. Повторные одобрения раздражают, и они пытаются создать широкий список разрешений. Это устраняет неудобство ценой полезной границы. Лучше объединять связанные действия в явный запуск, выдавать ему доступ после того, как проверяющий увидел его личность, и отзывать доступ после завершения запуска.
Здесь важна личность, подтвержденная подписью кода, но она не отвечает на все вопросы. Она может показать, какая подписанная программа запустила процесс. Но она не скажет, получает ли программа надежные инструкции и не содержит ли текущий репозиторий вредоносную подсказку. Одобрение должно связывать известный процесс с ограниченным периодом полномочий, а не благословлять любые инструкции, которые этот процесс когда-либо прочитает.
Четкая граница также помогает правильно обрабатывать повторы. Сбой сети может привести к новому запросу. Шлюз должен записать, что первое действие завершилось ошибкой до выполнения или во время транспорта, а затем записать повтор как отдельное действие. Если объединить повторы в одну расплывчатую запись об успехе, расследованию не хватит важной информации.
Пересылка агента разрушает границу учетных данных
Пересылка SSH-агента дает удаленному хосту возможность попросить ваш локальный агент аутентификации подписать запросы. Файл закрытого ключа на этот хост не копируется, но такое различие может создать ложное ощущение безопасности.
В руководстве OpenSSH для ssh предупреждается: пользователи, способные обойти разрешения файлов на удаленном хосте, могут получить доступ к пересланному агенту. Пользователь root на этом хосте может использовать пересланный сокет для аутентификации в другом месте, пока соединение открыто. Закрытый ключ он не получает, но может пользоваться полномочиями, которые за ним стоят. Для автономного агента обычно именно этого риска и хотелось избежать.
Избегайте вызовов вроде:
ssh -A deploy@build-box 'git fetch && ./deploy.sh'
Флаг -A пересылает локальный агент аутентификации. Если сценарий развертывания подключается ко второй машине, удаленный хост может запросить подписи через пересланный сокет. Скомпрометированная машина сборки или сценарий, измененный ненадежным участником, получает неожиданный путь в другую среду.
Вместо этого используйте учетные данные, предназначенные для задачи на сервере назначения. Это может быть учетная запись развертывания с ограниченным открытым ключом, короткоживущий SSH-сертификат для одной среды или удаленная сервисная учетная запись, которая может получить только нужный артефакт. Конкретный механизм зависит от вашей инфраструктуры. Принцип остается тем же: не превращайте одобренное действие одного агента в общий доступ к аутентификации с удаленной машины.
Некоторые команды сохраняют пересылку ради удобства вложенных SSH-подключений. Например, сначала используется bastion-хост, а затем выполняется переход к закрытым хостам. По возможности применяйте ProxyJump или тщательно контролируемый маршрут через шлюз. Так клиент сохраняет контроль над установлением соединения, а промежуточный хост не получает мощный локальный сокет агента.
К пробросу портов нужно относиться с таким же подозрением. Локальный, удаленный и динамический проброс может превратить одобренную команду в открытый туннель, который переживет полезную работу действия. Stateless-помощник должен отклонять функции проброса или одобрять их отдельно, если конкретной задаче они действительно нужны.
Точная удаленная учетная запись ограничивает ущерб от плохих инструкций
Свежего локального состояния недостаточно, если удаленный вход позволяет делать что угодно. У учетной записи, к которой обращается агент, должна быть четкая задача и соответствующие ей разрешения.
Для хоста развертывания это может быть учетная запись, которая умеет переключать симлинк релиза, перезапускать одну службу и читать один каталог развертывания. У нее не должно быть безусловного повышения привилегий без пароля, доступа к домашним каталогам всех пользователей и разрешения менять CI-runner. Такие сочетания появляются, когда кто-то хочет запустить автоматизацию до обеда. Они сохраняются, потому что никто не возвращается, чтобы сузить права.
OpenSSH дает операторам серверов несколько средств управления в формате authorized_keys. Руководство описывает, например, параметр command=, который принудительно запускает команду при аутентификации ключа, а также параметры отключения проброса портов, X11 и агента. Эти ограничения полезны, когда набор команд на стороне назначения небольшой и стабильный.
Например, команда эксплуатации может привязать отдельный открытый ключ к ограниченной удаленной обертке:
command="/usr/local/libexec/release-action",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAA... agent-release
Принудительная команда должна осторожно разбирать аргументы. Не передавайте необработанный пользовательский текст в оболочку. Безопасная обертка принимает небольшой набор действий, проверяет идентификаторы релизов по ожидаемому формату, записывает событие аудита и выполняет соответствующую фиксированную операцию. Если агенту нужен произвольный доступ к оболочке, признайте это прямо и ограничьте права учетной записи так, чтобы они соответствовали этому риску.
Принудительная команда не является универсальным языком политик. Это намеренно небольшой интерфейс на одном хосте. В этом и состоит его сила. Его можно протестировать, проверить и точно определить, какие действия он выполняет, не разбирая набор правил на естественном языке.
Будьте осторожны с sudo. Строка, разрешающая запуск сценария, все еще может дать широкие права, если сценарий принимает произвольные пути, загружает конфигурацию из доступного для записи каталога, запускает редактор или вызывает другую программу через контролируемую пользователем переменную окружения. Изучайте всю цепочку вызовов. Строка разрешения это начало проверки, а не конец.
Запись действия должна отвечать на вопросы «кто, что, где и с результатом»
Хороший журнал аудита должен показывать больше, чем сам факт использования SSH. Он должен позволять восстановить действие без догадок о том, какому запуску агента оно принадлежало.
Записывайте идентификатор запуска агента, полномочия его процесса, решение проверяющего, назначение, удаленную учетную запись, запрошенную команду или операцию, время, результат и ссылку на вывод. Если архитектура это позволяет, сохраняйте подтвержденную личность хоста. Записывайте и отказ. Отклоненное действие показывает, что граница сработала, а также может выявить плохую инструкцию до того, как возникнет ущерб.
С командами нужна особая осторожность. В необработанной команде могут быть пароли, токены доступа, параметры запросов и закрытые пути. Если скрыть все, запись станет бесполезной, а если сохранить все, журнал превратится в еще одно хранилище секретов. Практичный ответ состоит в том, чтобы проектировать интерфейс действий так, чтобы секретные значения изначально не попадали в аргументы. Если редактирование необходимо, записывайте сам факт редактирования и достаточно структурированного контекста для идентификации операции.
С выводом та же проблема. Ошибка команды развертывания может напечатать переменную окружения или URL с учетными данными. Храните вывод отдельно от основной записи события, ограничьте круг читателей и считайте его потенциально опасным вводом для следующего запуска агента. Не передавайте автоматически агенту полный production-транскрипт только потому, что он попросил диагностировать сбой.
Важно также различать два события: запись о том, что агент запросил действие, и запись, доказывающую, что действие дошло до удаленного хоста. Сбой транспорта, отказ проверки ключа хоста, ошибка аутентификации на сервере, код завершения команды и потеря соединения это разные результаты. Хорошая запись дает каждому собственный статус, а не объединяет их под одним словом «ошибка».
Защита от подделки меняет модель доверия к записи. Журнал с цепочкой хешей может показать, что записи изменили, удалили или переставили, если проверка завершится ошибкой. Но он не скажет, была ли исходная команда разумной, и не сделает скомпрометированный endpoint честным. Он защищает имеющуюся историю, но не заменяет укрепление системы.
Неудачное развертывание показывает, зачем нужна свежая локальная среда
Представим, что агент, работающий с кодом, получает запрос развернуть ветку на staging-хосте. Первый вызов открывает SSH-соединение, проверяет место на диске и запускает команду релиза. Команда завершается ошибкой, потому что существует блокировка миграции. Агент видит ошибку, читает заметки репозитория и получает скопированное сообщение: «снимите блокировку и повторите с аварийной учетной записью».
Это сообщение может быть безобидным, но небезопасным советом. А может быть prompt injection, спрятанным в файле, который агент попросили изучить. В любом случае агент теперь предлагает команду за пределами исходной процедуры развертывания.
При долго живущем общем соединении возможны разные проблемы. Исходный транспорт все еще может аутентифицировать широкую учетную запись развертывания. Новый процесс может использовать это соединение. Проверяющий видит только первое одобрение подключения и может не заметить последующее повышение прав. Если при этом пересылался SSH-агент, staging-хост мог получить путь к другой личности.
С stateless-помощником и авторизацией для каждого запуска следующий вызов агента начинается как новый запрос. Граница определяет новый процесс агента, записывает фактическую предложенную команду и цель, а затем запрашивает одобрение, если запуск еще не был одобрен. Проверяющий может отклонить запрос аварийной учетной записи и вместо этого разрешить ограниченную операцию, которая проверит владельца блокировки.
Свежесть соединения сама не решает, безопасно ли удалять блокировку. Операции все равно должны оценивать люди. Но она не позволяет старому одобренному каналу незаметно сделать это решение неважным.
Поэтому stateless SSH-дизайн нужен рядом с разумными ограничениями команд, а не вместо них. Изоляция соединения ограничивает унаследованные локальные полномочия. Ограниченный удаленный интерфейс определяет, что может сделать новый вызов. Записи аудита помогают позже понять оба решения.
Для stateless-выполнения нужна явная операционная схема
Помощнику, который открывает соединение для каждого запроса, нужны четкие правила проверки хоста, тайм-аутов, отмены и сообщений об ошибках. Если оставить эти детали окружению агента, скрытое состояние просто появится под другим названием.
Проверяйте ключи хостов. Параметр OpenSSH StrictHostKeyChecking=yes говорит клиенту отклонять хосты с неизвестным или изменившимся ключом, а не задавать интерактивный вопрос. Для защищенного автоматического действия это обычно правильная позиция. Передавайте доверенные ключи хостов через контролируемый процесс и завершайте работу с отказом, если назначение не совпадает.
Используйте ограниченные тайм-ауты. Зависшее SSH-соединение должно в итоге вернуть записанную ошибку, а не бесконечно оставаться наполовину завершенным сеансом. При отмене помощник также должен завершать дочерние процессы, если это позволяет операционная система, и сообщать, удалось ли подтвердить их завершение. Не сообщайте, что удаленная команда отменена, если транспорт оборвался уже после того, как сервер начал ее выполнять.
Делайте форму запроса небольшой и понятной для проверки. Граница действия может описывать SSH-запрос структурированными полями: назначение, учетная запись, команда, рабочий каталог, если он поддерживается, и тайм-аут. Не принимайте блок shell-конфигурации, который без проверки меняет файлы идентичности, поведение прокси, пути control-сокетов и параметры пересылки.
Шлюз может хранить SSH-учетные данные, пока агент запрашивает действие. Sallyport использует эту модель для SSH через встроенный stateless-помощник sp-ssh: агент не получает SSH-ключ, а приложение выполняет действие само.
Граница также должна различать одобрение сеанса и одобрение каждого вызова. Одобрение сеанса подходит для ограниченного запуска агента, когда проверяющий принимает последовательность ожидаемых действий. Одобрение каждого вызова подходит для учетных данных или назначений, где каждое использование заслуживает отдельного решения человека. Не делайте вид, что эти варианты дают одинаковый контроль. Они намеренно по-разному распределяют компромисс между количеством прерываний и точностью контроля.
Проверьте скрытое повторное использование до того, как доверять границе
Проверить, действительно ли ваша схема создает независимые соединения, можно в непроизводственной среде с учетной записью без доступа к важным данным.
Сначала запустите два отдельных процесса агента или помощника, каждый с безобидной командой вроде id. Проверьте журнал аутентификации SSH-сервера и журнал действий. Вы должны увидеть две попытки подключения, которые можно сопоставить с двумя отдельными записями процессов.
Затем проверьте клиентскую сторону на наличие сокетов мультиплексирования. В macOS и Linux-подобных системах можно выполнить такую общую проверку:
find ~/.ssh -type s -print
Если команда напечатает путь, соответствующий control-сокету, выясните, какая конфигурация его создала. Не удаляйте сокет вслепую на общей машине. Сначала найдите владельца и активных клиентов. Убирайте настройки совместного использования из пути выполнения изолированного помощника, а не рассчитывайте на очистку после завершения работы.
Наконец, выполните одно одобренное действие, завершите процесс агента и запустите второй процесс. Второй процесс не должен наследовать разрешение первого, его транспорт аутентификации или возможность запустить удаленную оболочку без нового решения. Проверяйте отказ так же внимательно, как успешный путь. Команды часто обнаруживают, что обычные запросы изолированы, а обработчик ошибки переходит на прямой SSH-вызов.
Эта последняя проверка выявляет знакомую проблему: аккуратный шлюз обрабатывает обычные запросы, но сценарий повтора или диагностический инструмент обходит его, когда ситуация становится напряженной. Такой обход обычно добавляет человек, пытающийся быстро восстановить сервис. Затем он становится маршрутом, который агент находит после первой ошибки.
Свежего состояния для каждого SSH-действия недостаточно назвать церемонией. Оно дает автономной работе границу, которую можно проверить, одобрить, отозвать и расследовать. Ограничивайте удаленную учетную запись, отказывайтесь от пересылки учетных данных, точно записывайте результаты и требуйте от каждого нового процесса агента заслужить собственный доступ.
Вопросы и ответы
Что такое stateless SSH-помощник?
Stateless-помощник создает новый локальный контекст выполнения для каждого запрошенного SSH-действия и удаляет его после завершения действия. Он не должен сохранять SSH-master-соединение, повторно используемую удаленную оболочку, пересланный сокет агента или закрытые учетные данные в собственном процессе агента. Удаленная машина все еще может хранить файлы и процессы, поэтому stateless-режим не стирает последствия на сервере.
Не сделает ли stateless SSH рабочие процессы агентов слишком медленными?
Обычно нет. Новое соединение добавляет время на аутентификацию и настройку, но большинство действий агента это ограниченные административные команды, развертывания или проверки, а не тысячи коротких интерактивных операций. Если настройка соединения занимает основное время, сократите число запланированных действий или используйте узкий интерфейс на стороне сервиса вместо незаметного совместного control-сокета.
Стоит ли AI-агентам использовать SSH ControlMaster?
Нет. ControlMaster позволяет последующим SSH-клиентам использовать существующее master-соединение, поэтому такие вызовы наследуют соединение, созданное другим процессом. Для короткой терминальной сессии человека это может быть удобно, но для агентских запусков такой подход ухудшает привязку действий к конкретному запуску и увеличивает последствия скомпрометированного или запутавшегося процесса агента.
Сбрасывает ли новое SSH-соединение состояние удаленного сервера?
Удаленная команда может оставить работающий процесс, измененную рабочую копию, временные файлы, новые разрешения или измененную службу. Считайте каждый вызов изолированным локально, а состояние на сервере описывайте явно: используйте аргументы команд, проверенные ревизии, именованные каталоги развертываний и правила очистки. Не обещайте пользователям, что новое SSH-соединение откатывает изменения на машине.
Почему повторное использование SSH-соединения опасно для автономных агентов?
Повторное использование SSH-соединения удобно: оно избавляет от повторной аутентификации и установки соединения. Но оно снижает прозрачность, когда несколько запусков агента работают через один аутентифицированный канал, особенно если общий сокет имеет слишком свободные разрешения или остается после завершения создавшего его запуска. Выбор зависит от приоритета: быстрая интерактивная работа или возможность точно связать действие с конкретным запуском.
Как ограничить удаленную учетную запись, которую использует AI-агент?
Используйте учетную запись только с теми разрешениями, которые нужны задаче, ограничьте список доступных хостов и по возможности запретите свободное интерактивное использование. Ограничить назначение учетной записи помогают параметры authorized_keys, принудительная команда или специальная удаленная оболочка. Stateless-клиент не спасет, если учетная запись может читать все секреты в production.
Безопасна ли пересылка SSH-агента для агентов, работающих с кодом?
Не включайте пересылку, если без нее задача действительно не выполняется. OpenSSH предупреждает, что пересланные учетные данные агента позволяют удаленному хосту запрашивать подписи через ваш локальный агент, а root на удаленном хосте часто может получить доступ к пересланному сокету. Лучше выдать агенту специальную учетную запись на стороне назначения.
Что должно входить в журнал SSH-аудита AI-агента?
Записывайте вызывающий процесс агента, одобренную личность или полномочия, назначение, учетную запись, команду или запрос, время, код завершения и решение об одобрении. Защищайте вывод команды, потому что в нем часто появляется следующая проблема, включая случайно раскрытые секреты. Журнал только для добавления надежнее изменяемого текстового лога, но он не заменяет проверку.
Защищает ли stateless SSH от скомпрометированного сервера?
Локальное stateless-выполнение помогает, потому что каждый вызов начинается без повторно используемого локального транспорта или скрытой настройки сеанса. Но оно не мешает удаленному серверу быть вредоносным, скомпрометированным или чрезмерно привилегированным. Проверяйте ключи хоста, ограничивайте учетные записи назначения и не пересылайте учетные данные на хосты, которым не доверяете.
Когда AI-агенту нужен SSH-шлюз вместо прямого SSH?
Используйте шлюз, когда агентам нужно работать с системами, где есть реальные учетные данные, а вам требуется одобрение человека или долговечная запись действий. Прямая SSH-команда может подойти для одноразовой песочницы с неважной учетной записью. Доступ к production заслуживает границы, которую агент не может переписать из собственного рабочего каталога.