Читать 7 мин

Файлы запуска SSH: почему удаленные команды вводят агентов в заблуждение

Файлы запуска SSH могут менять команды удаленного агента через профили, псевдонимы, функции, изменения PATH, поведение TTY и серверные правила.

Файлы запуска SSH: почему удаленные команды вводят агентов в заблуждение

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

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

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

Удаленная команда сначала проходит через оболочку учетной записи

OpenSSH обычно не выполняет слова после ssh host напрямую через execve целевого бинарного файла. В руководстве sshd сказано, что после аутентификации sshd запускает запрошенную команду через оболочку пользователя с параметром -c. Путь к оболочке входа берется из базы учетных записей, а не с локальной машины, которая открыла SSH-соединение.

Эта деталь объясняет многие странные сообщения об ошибках. Допустим, агент отправляет:

ssh deploy@buildbox git -C /srv/app rev-parse HEAD

Удаленная учетная запись может использовать Bash, zsh, fish, ограниченную оболочку или оболочку-обертку, принятую в конкретной организации. Выбранная оболочка получает команду, эквивалентную git -C /srv/app rev-parse HEAD. Сначала она может выполнить инициализацию. Кроме того, sshd может передать ей заменяющую команду, не дожидаясь этого этапа.

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

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

Поэтому первый практический вопрос звучит не так: «Удалось ли подключиться по SSH?» Спросите: «Какая программа интерпретировала удаленную команду, под какой учетной записью и с какими правилами инициализации?»

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

Поведение при запуске зависит от семейства оболочки и режима вызова. Обычных обозначений вроде «SSH-оболочка» или «оболочка Bash» недостаточно, чтобы предсказать результат.

Для Bash руководство GNU Bash Reference Manual различает оболочки входа, интерактивные и неинтерактивные оболочки. Оболочка Bash для входа читает /etc/profile, а затем первый доступный личный файл из ~/.bash_profile, ~/.bash_login и ~/.profile. Интерактивная оболочка, не являющаяся оболочкой входа, читает ~/.bashrc.

Обычная удаленная команда обычно выполняется в неинтерактивной оболочке. Это не значит, что она ничего не загружает. Если задана переменная окружения BASH_ENV, Bash читает указанный ею файл перед выполнением неинтерактивной команды. В руководстве Bash также описано особое поведение: если Bash понимает, что sshd запустил его со стандартным вводом, подключенным к сетевому соединению, он может прочитать ~/.bashrc. Не стройте автоматизацию на бытовом правиле, будто .bashrc влияет только на интерактивную работу.

У zsh другая схема. /etc/zshenv и ~/.zshenv применяются при каждом запуске zsh, поэтому шумный или меняющий состояние .zshenv особенно опасен. Для оболочек входа zsh читает .zprofile, а для интерактивных оболочек - .zshrc. Если пользователь помещает изменения PATH и вывод статуса в .zshenv, он меняет удаленные команды, скрипты и часто запуск графических приложений.

POSIX sh не дает универсального способа обойти это поведение. Реализации различаются. Спецификация POSIX описывает файл ENV для интерактивных оболочек, но системный /bin/sh может быть dash, Bash в режиме совместимости или другой реализацией со своими деталями. Проверяйте конкретный хост, а не предполагайте, что одно имя файла везде означает одно и то же.

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

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

Изменения PATH меняют идентичность программы, а не только удобство

PATH часто считают настройкой для удобства. В автоматическом выполнении он выбирает, какая именно программа будет запущена. Если профиль добавляет /opt/team/bin в начало PATH, то curl, git, ssh или python могут оказаться обертками, а не системными программами.

Такая обертка может быть намеренной. Команды используют обертки, чтобы выбирать облачные учетные данные, проверять репозитории, добавлять телеметрию или выбирать версии языков. Ошибка возникает, когда агент предполагает, что команда без полного пути означает программу поставщика. На самом деле это означает лишь «первый подходящий исполняемый файл в текущем PATH этой оболочки».

Псевдонимы и функции создают похожую неоднозначность, хотя их поведение зависит от режима оболочки. В Bash псевдонимы обычно не раскрываются в неинтерактивной оболочке, если не включен expand_aliases. Поэтому в обычных SSH-командах они встречаются реже, но не становятся невозможными. Подключенный файл может включить такое раскрытие. Функции вообще не зависят от раскрытия псевдонимов. Профиль может определить функцию с именем git, экспортировать состояние окружения и сделать так, чтобы все последующие команды этой оболочки вызывали функцию.

Сначала спросите оболочку, но воспринимайте ответ как свидетельство из проверяемого окружения, а не как универсальное доказательство:

ssh -T deploy@buildbox '\nprintf "shell argv0: %s\\n" "$0"\nprintf "shell flags: %s\\n" "$-"\nprintf "PATH: %s\\n" "$PATH"\ncommand -V git\ncommand -V python\ncommand -V curl\n'

Типичный результат может выглядеть так:

shell argv0: -bash
shell flags: hBc
PATH: /opt/team/bin:/usr/local/bin:/usr/bin:/bin
git is /opt/team/bin/git
python is /usr/local/bin/python
curl is /usr/bin/curl

Встроенная команда command -V может сообщить о псевдониме, функции, встроенной команде или пути. В оболочках, которые ее поддерживают, type -a git показывает несколько совпадающих путей. В Bash declare -f git выводит тело функции, если git является функцией. Эти проверки выявляют распространенную проблему: профиль определяет функцию git(), которая запускает настоящий Git, а затем незаметно отправляет событие статуса. Вывод по-прежнему выглядит как вывод Git. Побочный эффект происходит в другом месте.

Есть и еще одна тонкость, хеширование команд. Некоторые оболочки запоминают, где нашли команду. Если профиль меняет PATH после разрешения имени, оболочка может продолжать использовать сохраненный путь, пока hash -r или аналогичная команда не очистит кэш. Обычно это заметно в долгих интерактивных сессиях, но с этим могут столкнуться и агенты, сохраняющие процесс оболочки.

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

Псевдотерминал меняет не только форматирование

ssh host command обычно не создает псевдотерминал. ssh -t host command принудительно создает его. Один этот параметр может переключить ветку запуска оболочки, изменить буферизацию, заставить программы выводить цветовые коды и запросить ввод, которого обычный запуск без TTY не требует.

Во многих файлах конфигурации оболочки есть такая проверка:

case $- in
  *i*) ;;
  *) return ;;
esac

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

Программы тоже напрямую реагируют на терминал. Команда может показать индикатор выполнения, постранично вывести результат, выбрать другой формат диагностики или запросить подтверждение. Парсер, ожидающий один JSON-объект, сломается, если профиль напечатает приветствие до запуска программы или программа обнаружит TTY и выведет управляющие символы.

При расследовании расхождения проверьте оба режима:

ssh -T deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'
ssh -tt deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'

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

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

Серверные правила могут заменить запрошенную команду

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

Файлы запуска - только один уровень. Сервер может заменить или ограничить команду до того, как ее увидит оболочка учетной записи. Если проверить .bashrc и остановиться, можно пропустить правило, которое на самом деле изменило результат.

В руководстве sshd_config описан ForceCommand. Администратор может применить его глобально или внутри блока Match для пользователя, группы, адреса или другого условия. Например, с ForceCommand internal-sftp сервер игнорирует обычные команды оболочки и запускает внутренний сервис SFTP. Если задана собственная обертка, она получает контекст запрошенной команды и решает, что делать.

Запись открытого ключа SSH также может содержать ограничение command="..." в файле authorized_keys учетной записи. Это распространено для резервного копирования, доступа к репозиториям, передачи файлов и автоматизации с узким набором действий. Такая настройка может быть хорошей практикой безопасности. Проблема возникает, когда агенту выдают ограниченные учетные данные, а затем ожидают выполнения произвольных команд.

Настройки окружения нужно проверять в том же контексте. AcceptEnv сообщает sshd, какие переменные, переданные клиентом, разрешено принять. SetEnv задает переменные на стороне сервера. При включенном PermitUserEnvironment файл окружения учетной записи SSH или параметры в authorized key могут задавать значения. Они способны влиять на PATH, локаль, поведение среды выполнения языков и такие хуки, как BASH_ENV.

Используйте ssh -G на клиенте, но помните о его границах:

ssh -G buildbox | grep -E '^(hostname|user|port|requesttty|remotecommand|sendenv|setenv) '

OpenSSH выводит конфигурацию клиента после применения локальных блоков Host и настроек по умолчанию. Вывод может показать неожиданный RemoteCommand, принудительную настройку терминала, правило передачи окружения или другой целевой хост. Но он не показывает серверный ForceCommand, оболочку удаленной учетной записи или ограничения из authorized_keys.

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

Проверяйте путь выполнения, не доверяя одному тесту

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

Сначала проверьте локальную конфигурацию клиента с помощью ssh -G. Затем определите настроенную оболочку удаленной учетной записи через базу учетных записей хоста. На многих Linux-хостах getent passwd deploy возвращает запись, разделенную двоеточиями, где последнее поле содержит путь к оболочке. В macOS администратор может проверить учетную запись средствами Directory Service. По возможности делайте это через надежный административный канал, а не через потенциально измененную оболочку самой учетной записи.

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

  • присваивания PATH=, инициализацию менеджеров версий и команды hash;
  • alias, определения функций и параметры оболочки вроде expand_aliases;
  • команды вывода, такие как echo, printf и помощники управления терминалом;
  • BASH_ENV, ENV, переменные локали и хуки окружения для конкретных языков;
  • условные ветви, проверяющие -t, $-, $SSH_TTY или $TERM.

Не ищите только слово alias. Строка . ~/.local/share/tool/init.sh может подключать файл, в котором определена нужная функция. Проследите цепочку подключений до конца. Файлы профиля часто превращаются в набор условных включений, и именно там поведение автоматизации становится непроверяемым.

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

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

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

Сделайте оболочку автоматизации намеренно скучной

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

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

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

ssh -T deploy@buildbox '/usr/bin/env -i PATH=/usr/bin:/bin /bin/sh -s' \u003c\u003c'REMOTE'
set -eu
PATH=/usr/bin:/bin
export PATH

/usr/bin/git -C /srv/app rev-parse HEAD
REMOTE

У такого подхода есть полезные свойства. -T не создает терминал. /usr/bin/env -i очищает унаследованные переменные. /bin/sh -s читает буквальный скрипт из стандартного ввода, а заключенный в кавычки heredoc не позволяет локальной оболочке раскрыть $PATH или другой текст до передачи. set -eu останавливает выполнение при обращении к неустановленному параметру или при ошибке простой команды с учетом обычной семантики оболочки.

Есть и ограничения. /usr/bin/env, /bin/sh и /usr/bin/git - примеры, а не пути, которые можно без проверки копировать на любой хост. Подтвердите пути для каждого класса целевых машин. Очистка окружения может удалить переменные, которые программе действительно нужны: домашний каталог, локаль, настройки прокси или расположение среды выполнения. Возвращайте только явно названные необходимые переменные и документируйте причину каждой.

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

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

Разделите интерактивные учетные записи и учетные записи агентов

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

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

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

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

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

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

Доверяйте результатам только после фиксации контракта

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

Здесь есть неудобная граница. По собственному выводу удаленной команды нельзя доказать, что удаленная оболочка не изменила команду до печати этого вывода. Если оболочка учетной записи, ограничение authorized key или принудительная команда находятся вне вашего контроля, нужны административные подтверждения или другая учетная запись. Десять одинаковых проверок лишь десять раз повторят то же предположение.

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

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

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

Загружает ли ssh host command файлы запуска оболочки?

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

Запускается ли .bashrc, когда SSH выполняет удаленную команду?

Обычно нет, если Bash выполняет обычную неинтерактивную команду, но именно это «обычно» часто создает проблемы для агентов. Bash может прочитать BASH_ENV, а также имеет особое поведение, когда sshd запускает его со стандартным вводом, подключенным к сети. У других оболочек свои правила.

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

Используйте ssh -T, чтобы не создавать псевдотерминал, запускайте известную оболочку по абсолютному пути, очищайте окружение с помощью env -i и указывайте абсолютные пути к важным программам. Это уменьшает случайные различия, но не отменяет ForceCommand или оболочку, которой управляет кто-то другой.

Почему ssh -t меняет вывод моей команды?

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

Как понять, что удаленная команда является псевдонимом или функцией?

command -V tool показывает, как текущая оболочка разрешает имя, включая псевдонимы, функции, встроенные команды и пути. type -a tool полезна в оболочках, которые ее поддерживают: она может показать несколько исполняемых файлов, найденных в PATH.

Стоит ли ИИ-агентам использовать общую SSH-учетную запись?

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

В чем разница между оболочками входа, интерактивными и неинтерактивными оболочками?

Оболочка входа читает файлы входа, например /etc/profile и один из личных профилей Bash для входа. Неинтерактивная оболочка выполняет строку команды и может прочитать другой файл, например BASH_ENV. Интерактивная оболочка включает пользовательские удобства, такие как приглашение и часто псевдонимы.

Может ли ssh -G показать, что происходит на удаленном сервере?

ssh -G host выводит конфигурацию клиента после применения локальных блоков Host и настроек по умолчанию OpenSSH. Он не показывает файлы запуска на сервере, оболочку удаленной учетной записи, ForceCommand или ограничение команды в записи authorized_keys.

Что проверить перед тем, как доверять результату SSH-команды агента?

Проверьте оболочку учетной записи, файлы запуска, PATH, разрешение команд, правила демона SSH и ограничения authorized_keys для этой учетной записи. Протестируйте точную команду с TTY и без него, а затем зафиксируйте получившийся контракт выполнения, не полагаясь на разовый результат в терминале.

Может ли SSH-шлюз не дать файлам профиля изменить команды?

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

Sallyport

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

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