Безопасность SSH ProxyCommand слабее, чем кажется?
Безопасность SSH ProxyCommand требует проверить локальное выполнение оболочки, подстановки, Include и маршрутизацию через промежуточные хосты до того, как агент получит доступ к удалённому узлу.

SSH ProxyCommand выполняет локальный код ещё до удалённой команды, которую вы собирались запустить. После такого объяснения это кажется очевидным, однако при проверках его регулярно считают деталью транспорта: агент запрашивает ssh host uptime, проверяющий смотрит на хост и удалённую команду, а клиент незаметно запускает локальную команду оболочки, которую никто не учитывал при принятии решения.
Этот разрыв особенно важен, когда автономный агент для программирования может вызывать SSH. Удалённое назначение может быть жёстко ограничено, а локальная конфигурация SSH за годы обрастает псевдонимами, подключаемыми файлами, условными правилами, прокси-помощником и цепочкой промежуточных хостов. Безопасную проверку стоит начинать с действий SSH на стороне клиента, а затем переходить к сети.
ProxyCommand запускается до появления удалённой сессии
ProxyCommand указывает SSH-клиенту запустить локальную команду и использовать её стандартный ввод и вывод как транспорт к SSH-серверу. Команда выполняется не на целевом узле. Она запускается на машине, с которой вызвали ssh, от имени локальной учётной записи и до того, как SSH сможет аутентифицировать нужный сервер.
Обычная запись выглядит безобидно:
Host build-private
HostName build.internal.example
ProxyCommand /usr/local/bin/connect-private %h %p
Когда кто-то выполняет ssh build-private, SSH подставляет %h и %p, а затем запускает /usr/local/bin/connect-private build.internal.example 22. Помощник может открыть сокет, вызвать другой клиент, прочитать файл токена, загрузить библиотеку, обратиться к переменным окружения или запустить скрипт оболочки. После этого SSH-клиент видит лишь поток байтов. В запросе на подтверждение может быть указано SSH-подключение, хотя первым существенным действием уже стал запуск локальной программы.
Руководство OpenSSH по ssh_config чётко описывает эту границу: ProxyCommand задаёт команду для подключения к серверу и предупреждает, что команда выполняется через оболочку пользователя. Последняя оговорка меняет подход к проверке. Командная строка не равна массиву аргументов. В действие входят разбор оболочкой, подстановки, перенаправления, подстановка команд и среда запуска оболочки.
Не отмахивайтесь от этого только потому, что помощник хранится в репозитории dotfiles. Проверенная шесть месяцев назад конфигурация может вызывать бинарный файл, найденный через PATH, подключать файл из каталога с правом записи или зависеть от псевдонима, который теперь разрешается иначе. Проверять нужно итоговый путь выполнения на машине, где он будет запущен.
Псевдоним хоста может выбирать гораздо больше, чем хост
Конфигурация SSH - это система сопоставления, а не простой словарь. Короткий псевдоним может активировать настройки из системной и пользовательской конфигурации, подключённых фрагментов, шаблонов хостов, канонизации и условных блоков. Итоговая команда может находиться в файле, который никто не связывает с этим псевдонимом.
Начните с перечисления конфигураций, которые читает SSH. В распространённом клиенте OpenSSH это /etc/ssh/ssh_config, файлы, на которые указывает Include, и ~/.ssh/config. Пакетирование дистрибутива и параметры командной строки могут изменить этот список, поэтому проверьте и сам вызов. Вызывающий код с -F /some/file меняет всю поверхность проверки.
Используйте ssh -G, чтобы вывести итоговую конфигурацию для конкретного назначения. Для контролируемого тестового псевдонима это полезный артефакт:
ssh -G build-private | grep -E '^(hostname|user|port|proxycommand|proxyjump|identityfile) '
Вывод выглядит так:
hostname build.internal.example
user deploy
port 22
proxycommand /usr/local/bin/connect-private %h %p
identityfile ~/.ssh/id_deploy
ssh -G показывает, что выбрал SSH, и помогает заметить удивительно частые ошибки: широкую запись Host *, старый подключённый файл или псевдоним, который указывает на другую учётную запись. Он не доказывает безопасность помощника. Кроме того, он может не показать все условия, которые привели к результату, в понятном для проверяющего виде, поэтому проследите каждую важную директиву до её исходного файла.
Считайте ввод имени хоста данными только тогда, когда конфигурация сохраняет его данными. Если агент может передавать произвольные псевдонимы, он может выбрать любой подходящий блок Host, доступный локальной учётной записи. Если он может передавать произвольные параметры -o или путь к конфигурации, он часто сможет полностью обойти проверку псевдонима. Обёртка, принимающая произвольную SSH-команду, не создаёт значимой границы.
Лучший интерфейс принимает идентификатор назначения из небольшого списка разрешённых и сопоставляет его с фиксированными учётной записью, именем хоста, портом и способом подключения. Идентификатор может быть production-readonly, но реальные аргументы SSH не должны появляться из строки, созданной агентом.
Подстановки оболочки превращают конфигурацию в границу ввода
Поскольку SSH передаёт ProxyCommand оболочке, кавычки в конфигурации не обеспечивают ту же защиту, что структурированные аргументы процесса. Синтаксис оболочки остаётся до тех пор, пока она его не обработает. Это касается пробелов, символов шаблонов, ссылок на переменные, перенаправлений и подстановок команд, когда они встречаются в тексте команды.
Опасный шаблон - помощник, который строит собственную команду оболочки из полученных значений. Рассмотрим такую конфигурацию:
Host *
ProxyCommand sh -c 'relay --target %h --port %p --token "$RELAY_TOKEN"'
Здесь есть несколько отдельных вопросов. Соответствует ли каждое возможное итоговое имя хоста ожидаемому синтаксису имени? Разбирает ли relay --target как одно значение? Может ли окружение, управляемое пользователем, задать RELAY_TOKEN или изменить путь поиска? Получает ли внешняя оболочка строку команды с символами, которые меняют смысл до запуска relay? Тот факт, что имя хоста пришло от SSH, не отвечает на эти вопросы.
Процентные токены не образуют безопасную систему шаблонов. OpenSSH подставляет токены вроде %h, %p, %r и %n во многих параметрах. %n - исходный аргумент хоста, а %h - имя хоста, которое SSH будет использовать после обработки конфигурации. Эту разницу легко упустить. Прокси-команда с %n может получить понятный человеку псевдоним или произвольный запрошенный текст там, где автор ожидал каноническое DNS-имя. Команда с %h тоже может получить значение, изменённое HostName или канонизацией.
Обычно исправление проще исходной настройки. Используйте небольшой отдельный помощник с фиксированным путём к исполняемому файлу, дайте ему ограниченный синтаксис и заставьте отклонять всё, что ему не соответствует. Пусть помощник вызывает API подключения или запускает процесс через массив аргументов, вместо того чтобы собирать ещё одну строку оболочки. Если скрипт оболочки неизбежен, проверяйте ввод до подстановки, заключайте каждую подстановку в кавычки для этой оболочки и держите допустимый набор настолько узким, чтобы аудитор мог его проверить.
Не заменяйте разбор на eval. Команды используют его, потому что он делает обёртку на основе конфигурации гибкой на вид. Но одна пропущенная кавычка превращает это в дефект локального выполнения. Гибкость должна жить в проверенном формате конфигурации, а не в оболочке, которая повторно разбирает текст.
ProxyJump убирает одну оболочку, но не проблему доверия
ProxyJump, часто записываемый как -J или ProxyJump, просит SSH добраться до назначения через один или несколько промежуточных SSH-хостов. Для обычной маршрутизации через бастион он предпочтительнее самописного ProxyCommand, который лишь запускает другую команду ssh -W. Он описывает ожидаемую топологию и не помещает её в пользовательскую команду оболочки.
Например:
Host build-private
HostName build.internal.example
User deploy
ProxyJump bastion-admin
Host bastion-admin
HostName bastion.example
User relay
Такую конфигурацию легче проверить, чем строку с вложенными кавычками. Но она всё равно требует тщательной проверки. Клиент аутентифицируется на промежуточном хосте, тот участвует в маршруте, а его имя, учётная запись, ключ хоста, требования MFA, правила перенаправления и сетевая доступность влияют на действие. Скомпрометированный или неверно настроенный промежуточный хост может раскрыть особенности трафика и перенаправить попытку подключения. Проверка ключа хоста обязательна на каждом SSH-переходе.
OpenSSH допускает обе настройки, но при одновременном применении ProxyCommand имеет приоритет над ProxyJump. Это неприятный сюрприз конфигурации. Проверяющий может увидеть аккуратный ProxyJump в блоке для конкретного хоста, а широкое правило, подходящее раньше или позже, добавит прокси-команду. Проверяйте итог через ssh -G, а не делайте вывод по одному блоку.
Нескольким промежуточным хостам тоже нужна явная причина. Не считайте цепочку дополнительной защитой по умолчанию. Каждый добавленный хост означает ещё одно использование учётных данных, решение по ключу хоста и место для аудита. Используйте цепочку только когда этого требует сетевой маршрут, и записывайте ожидаемую учётную запись на каждом переходе.
Include и Match exec могут выполнять локальную логику при выборе настроек
Include делает конфигурацию SSH составной, что удобно, пока репозиторий, инструмент управления конфигурацией или установщик не добавит фрагмент в каталог с широким шаблоном. OpenSSH разворачивает шаблоны в Include, поэтому неожиданный файл может изменить настройки хоста, не затрагивая основную конфигурацию. Проверяйте владельцев и права записи в подключённых каталогах, а не только их текущее содержимое.
Match добавляет условия. Условия Match host, user, localnetwork и связанные с ними меняют применяемые параметры. Match exec заслуживает более заметного предупреждения: SSH запускает указанную локальную команду и использует её код завершения, чтобы решить, подходит ли блок. Поэтому чтение конфигурации может вызвать локальный исполняемый файл.
Этот пример показывает границу:
Match exec "/usr/local/bin/on-corporate-network"
ProxyJump corp-bastion
Условие может быть разумным, если помощник - фиксированная программа с простым результатом, принадлежащая root. Оно становится хрупким, если вызывает оболочку, читает файл проекта с правом записи, обращается к сетевому сервису или зависит от PATH. Не проверяйте неизвестные правила Match exec на рабочей станции с активными учётными данными только потому, что собирались выполнить ssh -G: сначала изучите команду и при необходимости используйте изолированную учётную запись или машину.
В руководстве OpenSSH также описана подстановка токенов в параметрах сопоставления и связанных с командами параметрах. Значит, условие Match exec может зависеть от запрошенного текста хоста или пользователя. Не допускайте значений, выбранных агентом, в эти условия. Если условию нужны контекстные данные, получайте их из доверенного локального файла состояния со строгими правами владения, а не из строки, прошедшей через границу агента.
Инцидент обычно начинается с удобного помощника
Для реалистичного сбоя не нужна конфигурация, которая выглядит вредоносной. Представьте, что разработчик много лет назад добавил такую запись для доступа к закрытой тестовой сети:
Host test-*
ProxyCommand connect-testnet %h %p
connect-testnet когда-то установил сценарий начальной настройки в личный каталог бинарных файлов. Скрипт вызывает корпоративный VPN-клиент, а затем запускает релейную команду, найденную через PATH. Позже агент сборки запускается от имени разработчика. Он может запросить ssh test-cache. Ожидаемое действие - команда только для чтения на тестовой машине.
Сначала SSH сопоставляет test-*. Затем локальная оболочка запускает connect-testnet. Потом скрипт запускает VPN-клиент и исполняемый файл, который в этот момент первым находится через PATH. Лишь после этих локальных шагов SSH начинает свой протокол с указанным хостом. Аудит, который фиксирует только удалённую команду, теряет поведение, изменившее состояние локальной машины и выбравшее сетевой маршрут.
Проблема не в том, что скрипты оболочки запрещены. Проблема в том, что цепочку локальных удобных средств считают частью удалённого сервиса. Возможность агента выбрать test-cache выбрала локальный код. Злоумышленный агент мог бы искать псевдонимы, подходящие под более широкие шаблоны, но обычный агент может вызвать ту же проблему, просто угадав существующее имя.
Исправьте реальный путь. По возможности замените шаблон с подстановочным знаком именованными назначениями. Поместите релейный помощник по абсолютному пути. Уберите внутри него поиск через PATH. Пусть помощник принимает только ожидаемые значения хоста и порта. Если VPN-подключение не должно запускаться для каждого SSH-действия, вынесите управление VPN из прокси-команды, вызываемой для каждого запроса. Затем проверьте итоговую конфигурацию от имени учётной записи и в окружении, которые будет использовать агент.
Подтверждайте способ подключения, а не только назначение
В потоке подтверждения должно быть достаточно сведений, чтобы человек понял действие. ssh deploy@build-private - неполный контекст, когда псевдоним запускает прокси-помощник или строит маршрут через бастион. Проверяющему нужны итоговые назначение, учётная запись, порт, способ проксирования и путь через промежуточные хосты. Иначе он подтверждает метку и надеется, что локальная конфигурация по-прежнему означает то же, что на прошлой неделе.
Разделите три решения, которые команды часто смешивают. Первое: может ли этот процесс агента выполнять любые SSH-действия? Второе: может ли он использовать конкретный SSH-идентификатор? Третье: может ли это назначение использовать конкретный локальный способ подключения? Согласие на первые два вопроса не отвечает автоматически на третий. Прокси-помощник может вести в другую зону доверия или вызывать локальные побочные эффекты, которых не было бы при прямом подключении.
Sallyport не передаёт SSH-учётные данные агенту и использует встроенный помощник sp-ssh для SSH-действий, но это не делает локальную конфигурацию SSH безопасной поверхностью политик. Ограничьте интерфейс назначений для агента и проверяйте любую конфигурацию клиента или процесс-помощник, который участвует до начала действия с учётными данными.
Для чувствительных маршрутов требуйте подтверждение каждого использования SSH-идентификатора, а в описании подтверждения указывайте назначение и маршрут. Так можно заметить изменившийся псевдоним, неожиданный бастион или попытку использования вне обычного пути обслуживания. Но это не сделает расплывчатую карточку подтверждения полезной. В ней должна быть нужная информация.
Проверяйте итоговый путь с одноразовыми учётными данными
Проверка конфигурации требует теста выполнения, но проводите его с учётными данными, которые не могут повредить рабочей среде. Используйте тестовый хост, временную учётную запись и контролируемое окружение. Если операционная система позволяет, зафиксируйте дерево процессов на стороне клиента и сетевые назначения. Цель - подтвердить, какой исполняемый файл запускается, какие аргументы он получает и создаёт ли он соединения помимо ожидаемого маршрута.
Для большинства псевдонимов хостов достаточно короткой последовательности проверки:
- Выполните
ssh -G aliasи сохраните итоговыеhostname,user,port,proxycommand,proxyjumpи настройки идентификатора. - Проследите каждый
Includeи каждый подходящий блокHostилиMatch, который задал эти значения. - Прочитайте каждый локальный исполняемый файл в пути прокси или сопоставления, включая скрипты и их файлы конфигурации.
- Запустите соединение с одноразовыми учётными данными и проверьте реальные промежуточные хосты, ключи хостов и локальные процессы.
- Отзовите тестовые учётные данные после проверки и запишите ожидаемый маршрут рядом с псевдонимом.
Не полагайтесь только на успешное соединение. Успех доказывает, что байты достигли SSH-сервера. Он не доказывает, что их передала нужная программа, что их получил задуманный хост или что до этого не произошло локального побочного действия.
Небольшое трение здесь уместно. Если никто не может объяснить исполняемый файл за ProxyCommand, никто не должен разрешать агенту вызывать этот псевдоним. Замените его, удалите или оставьте маршрут недоступным для агента, пока за него не будет отвечать конкретный человек.
Небольшая SSH-поверхность лучше сложной клиентской конфигурации
Самая безопасная SSH-настройка для агента содержит мало именованных назначений, фиксированные идентификаторы, явные промежуточные хосты и не допускает произвольных параметров конфигурации. Для этого ей не нужен универсальный язык политик. Нужны ограниченный интерфейс и конфигурация, которая остаётся простой при проверке.
Проверяйте всё снова, когда меняется что-либо из следующего: SSH-клиент, новый скрипт начальной настройки, развёртывание системы управления конфигурацией, добавленный каталог подключений, новая среда выполнения агента или новый промежуточный хост. Такие изменения часто попадают в разные репозитории, поэтому путь подключения и деградирует незаметно.
Пусть итоговый стандарт будет простым: до запуска удалённой команды вы должны уметь назвать каждый локальный исполняемый файл, который запускает SSH, каждый хост, с которым он связывается, каждый используемый идентификатор и человека, подтвердившего этот маршрут. Если вы не можете сделать этого для псевдонима, он не готов к автономному использованию.
Вопросы и ответы
ProxyCommand запускается на локальной машине?
Да. SSH запускает ProxyCommand на клиенте до того, как установит транспортное соединение с целевым узлом. Считайте эту команду и все программы, которые она запускает, локальным выполнением от имени пользователя или агента, запустившего SSH.
Как увидеть ProxyCommand, который будет использовать SSH?
Используйте ssh -G alias, чтобы посмотреть итоговую настройку для именованного хоста, затем самостоятельно изучите все подходящие файлы конфигурации. Проверяйте из контролируемой тестовой учётной записи, если конфигурация использует Match exec: это условие может запустить локальную команду, пока SSH читает настройки.
ProxyJump безопаснее ProxyCommand?
Обычно нет. ProxyJump описывает SSH-переход без передачи командной строки вашей локальной оболочке, поэтому рисков, связанных с кавычками и подстановками, меньше. Но он всё равно создаёт границу доверия на промежуточном хосте и требует такой же проверки ключей хостов и учётных записей.
Может ли псевдоним SSH-хоста привести к внедрению команды?
Да, если оболочка получает текст, контролируемый атакующим, как часть командной строки ProxyCommand. Псевдонимы хостов, канонические имена хостов, имена пользователей, номера портов, переменные окружения и подключённая конфигурация могут влиять на итоговую команду, если намеренно не ограничить их.
Какой риск для безопасности SSH добавляет промежуточный хост?
Промежуточный хост может видеть и передавать поток байтов, а также становится местом, где важны его собственные SSH-учётные данные, конфигурация и правила перенаправления. Он не должен незаметно превращаться в универсальный хост администрирования только потому, что так удобнее.
Почему Match exec опасен в ssh_config?
Match exec позволяет SSH запустить локальную команду, чтобы решить, применяются ли последующие настройки. Это удобно для узких доверенных условий, но превращает чтение конфигурации в путь локального выполнения кода и требует такой же проверки, как вспомогательный скрипт.
Стоит ли использовать абсолютные пути в ProxyCommand?
Абсолютные пути не дадут изменённому PATH выбрать другого помощника, но сами по себе не делают его безопасным. Всё равно нужно проверить его права, аргументы, файлы конфигурации и идентификаторы, с которыми он связывается.
Можно ли безопасно разрешить ИИ-агенту использовать SSH?
Не позволяйте агенту передавать произвольные аргументы SSH, псевдонимы хостов или пути к конфигурации. Дайте ему небольшой набор проверенных назначений и не включайте локальную механику соединения в его входные данные.
Делает ли хранилище SSH-учётных данных ProxyCommand безопасным?
Авторизация агента и изоляция секретов определяют, кто может запросить действие и кто видит учётные данные. Они не проверяют локальную конфигурацию SSH на подстановки оболочки, Include или исполняемых помощников, поэтому конфигурацию всё равно нужно проверять.
Что нужно проверить в конфигурации SSH-клиента в первую очередь?
Сначала перечислите псевдонимы хостов, к которым агент может обращаться, и выполните ssh -G для каждого в контролируемой среде. Удалите пользовательские записи ProxyCommand, которые не можете объяснить, затем замените оставшиеся проверенными помощниками с абсолютными путями или ProxyJump, где это уместно.