# Проверка хоста SSH для агентов, работающих с рабочей средой

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

SSH ставит два разных вопроса безопасности: «Подключился ли я к нужному серверу?» и «Что может делать эта учётная запись на сервере?» Команды часто смешивают эти вопросы, потому что в обоих случаях используются открытые ключи. Такая ошибка превращает соединение не с тем хостом сначала в утечку учётных данных, а затем чрезмерно привилегированный аккаунт в инцидент на рабочей среде.

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

## Идентичность хоста нужно проверять до аутентификации пользователя

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

Ключ хоста принадлежит серверу, а не администратору и не агенту. Отпечаток - это короткое представление открытого ключа хоста, обычно в формате SHA256. Если `deploy.example.internal` обычно предъявляет один отпечаток, а затем внезапно предъявляет другой, у клиента есть свидетельство изменения. Причиной может быть законная пересборка. Но это также может быть ошибка DNS, повторно использованный адрес, ошибка в bastion-хосте или активная попытка перехвата.

Представьте, что агенту программирования поручили выполнить миграцию на `db-prod.internal`. Конфигурация SSH разрешает это имя в адрес. Злоумышленник, способный повлиять на DNS, маршрут через прокси или устаревшую запись инвентаризации, может направить соединение на подконтрольный сервер. Если клиент примет незнакомый ключ хоста, этот сервер сможет запросить аутентификацию пользователя. Закрытый ключ SSH на стороне клиента может не покинуть клиент, но доступ агента часто включает пароли, операции подписи сертификатов, перенаправление или команды, раскрывающие полезную информацию после входа. И главное, агент теперь может выполнить запланированную команду не на той машине.

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

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

## Доверие при первом использовании плохо подходит для работы без присмотра

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

OpenSSH описывает этот выбор в руководстве `ssh_config`, в разделе `StrictHostKeyChecking`. При значении `yes` клиент никогда не добавляет неизвестные ключи хостов автоматически и отклоняет изменившийся ключ. При `accept-new` он автоматически записывает неизвестный ключ, но всё равно отклоняет изменившийся. При `no` или `off` он принимает больше ситуаций, требующих внимания оператора.

`accept-new` часто называют разумным компромиссом. Он действительно уменьшает количество остановок, когда хосты создаются часто. Но он также даёт сетевому маршруту право создать исходную запись идентичности. Для агента, способного менять инфраструктуру, это неправильная сторона, принимающая решение.

Для рабочих и тестовых конечных точек, которыми управляет агент, используйте `StrictHostKeyChecking=yes`. Если соединение не удалось из-за неизвестного хоста, передайте запрос агента человеку, который сравнит показанный отпечаток с источником вне этого SSH-соединения. Подойдут консоль облачного провайдера, подписанная запись инвентаризации, физическая консоль или существующий канал управления.

Не устраняйте паузу, добавляя `StrictHostKeyChecking=no` в глобальный файл конфигурации. Такая строка обычно переживает временный инцидент, ради которого её добавили, а затем незаметно применяется к хостам, для которых никто не хотел ослаблять проверку.

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

## Отпечаток полезен только при независимом источнике

Отпечаток, скопированный с конечной точки, которую вы пытаетесь проверить, почти ничего не доказывает. `ssh-keyscan` удобен для сбора открытых ключей хостов, и именно это удобство создаёт знакомую ловушку: оператор запускает команду для имени хоста, вставляет результат в `known_hosts` и считает хост проверенным. Если DNS или маршрутизация уже указывают на злоумышленника, оператор закрепил ключ злоумышленника.

Используйте `ssh-keyscan` для сбора ключа после получения независимого отпечатка, а не как источник доверия. Например, администратор может получить отпечаток ключа хоста через консоль провайдера или из подписанной записи сборки, а затем сравнить его с собранным ключом.

```sh
ssh-keyscan -t ed25519 app-prod.internal > /tmp/app-prod.hostkey
ssh-keygen -lf /tmp/app-prod.hostkey -E sha256
```

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

```text
256 SHA256:exampleFingerprintMaterial app-prod.internal (ED25519)
```

Сравните значение `SHA256:` посимвольно с независимо полученным значением. Также проверьте алгоритм. Если в записи указан ED25519, а собранный результат относится к RSA, остановитесь и разберитесь, а не считайте результаты взаимозаменяемыми.

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

```text
app-prod.internal ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExamplePublicHostKeyMaterial
[192.0.2.44]:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExamplePublicHostKeyMaterial
```

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

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

## Закрепите маршрут и запись хоста в конфигурации SSH

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

```sshconfig
Host app-production
    HostName app-prod.internal
    User agent_release
    Port 22
    UserKnownHostsFile ~/.ssh/agent_known_hosts
    GlobalKnownHostsFile /dev/null
    StrictHostKeyChecking yes
    UpdateHostKeys no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    ForwardAgent no
    PermitLocalCommand no
```

Этот фрагмент предотвращает несколько обычных ошибок. `UserKnownHostsFile` не даёт неожиданно зависеть от записей хостов, накопленных человеком. `GlobalKnownHostsFile /dev/null` не позволяет неуправляемому общесистемному файлу незаметно расширить доверие. `UpdateHostKeys no` запрещает автоматическим обновлениям ключей менять набор закреплённых ключей во время работы агента. `ForwardAgent no` не даёт удалённой машине использовать перенаправленный агент аутентификации для доступа куда-либо ещё.

Руководство OpenSSH `ssh_config` объясняет, что `UpdateHostKeys` поддерживает ротацию ключей хоста, когда хост доказывает владение доверенным ключом. Это может быть полезно для интерактивных парков машин с дисциплинированным управлением хостами. Для автономного агента автоматическое изменение конфигурации усложняет разбор инцидента. Человек должен одобрить изменение идентичности рабочей системы и намеренно обновить управляемый файл хостов.

Используйте в запросах агента стабильный псевдоним вроде `app-production`, а исходные адреса оставьте для аварийных процедур. Псевдоним делает утверждённую цель видимой в журналах и не позволяет командам расходиться между именами, временными адресами и скопированными фрагментами оболочки.

Проверьте действующую конфигурацию до того, как агент получит доступ к любым учётным данным:

```sh
ssh -G app-production | grep -E '^(hostname|user|stricthostkeychecking|userknownhostsfile|forwardagent|updatehostkeys) '
```

Ожидайте значения примерно такого вида:

```text
hostname app-prod.internal
user agent_release
stricthostkeychecking yes
userknownhostsfile /Users/operator/.ssh/agent_known_hosts
forwardagent no
updatehostkeys no
```

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

## Ограничения аккаунта сдерживают ущерб после правильного подключения

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

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

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

Если задача узкая, ограничьте авторизованный открытый ключ SSH принудительной командой. В `authorized_keys` запись на сервере может привязать эти учётные данные к оболочке:

```text
command="/usr/local/sbin/agent-release-wrapper",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleAgentPublicKey agent-release
```

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

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

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

## При проверке команды нужно видеть её эффект, а не расплывчатое намерение

Подтверждение команды полезно только тогда, когда человек видит достаточно контекста для оценки последствий. «Развернуть релиз» - это намерение. `sudo systemctl restart payments-api` на `app-production` от имени `agent_release` - действие, которое можно оценить.

Проверяйте точный псевдоним назначения, удалённый аккаунт, буквальную команду, аргументы и рабочий каталог. Также выясняйте, запускает ли команда оболочку, раскрывает ли переменные, читает ли удалённый скрипт, скачивает ли содержимое или использует `sudo`. Именно эти детали определяют, может ли внешне безобидный запрос выйти далеко за пределы своего описания.

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

```text
Target: app-production (app-prod.internal)
Account: agent_release
Command: /usr/local/sbin/agent-release-wrapper activate 2025.04.17-rc2
Reason: activate the approved release after smoke tests
```

Сравните её с таким запросом:

```text
ssh app-production "curl $URL | sudo sh"
```

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

У проверки команд есть собственный тип сбоя: усталость от подтверждений. Если агент просит подтверждение для каждого безобидного `cat`, `git status` и запроса состояния сервиса, люди начинают подтверждать, не читая. Передайте исследование только для чтения ограниченному аккаунту или оболочке с чётко заданными командами, а интерактивные подтверждения оставьте для действий, которые записывают данные, перезапускают сервисы, меняют ключи, права или пересекают границу доверия.

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

## Опасный сценарий сбоя обычно складывается из обычных коротких путей

Большинство инцидентов с SSH в автоматизации начинаются не с экзотического взлома криптографии. Обычно всё начинается с упрощения, которое во время настройки казалось безобидным.

Представьте агента выпуска, настроенного с `StrictHostKeyChecking=accept-new`, общей учётной записью развёртывания и запросом подтверждения, в котором написано только «выполнить deploy». Запись DNS ненадолго указывает на заменённую машину, пока инвентаризация ещё не обновлена. Агент видит незнакомый хост, записывает его ключ, входит на заменённую машину через общий аккаунт и запускает оболочку развёртывания. У оболочки широкие права на запись, потому что общий аккаунт также используется для аварийного ремонта.

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

Каждый контроль прерывает свой участок цепочки:

1. Предварительно закреплённая запись хоста отклоняет заменённую машину, пока оператор не проверит её.
2. Отдельный аккаунт ограничивает ущерб, если утверждённая цель всё же неверна по причине, которую идентичность хоста не позволяет обнаружить.
3. Запись подтверждения с полной командой даёт проверяющему возможность заметить неожиданную цель или действие с повышенными правами.
4. Журнал сессии и вызова позволяет после сбоя восстановить запрос, подтверждение, цель и результат.

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

## Для смены ключа хоста нужна процедура, а не кнопка исключения

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

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

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

Никогда не поручайте агенту автоматически запускать `ssh-keygen -R hostname` и подключаться снова. Удаление старой записи стирает сигнал о несоответствии до того, как кто-либо установит причину. Оператор может использовать эту команду после проверки как часть согласованного обновления, но она не должна быть логикой восстановления внутри рабочего процесса агента.

## Записи аудита должны позволять восстановить решение

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

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

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

Sallyport разделяет журналы сессий и активности и записывает их в защищённый от записи зашифрованный журнал аудита с цепочкой хешей; `sp audit verify` может проверить цепочку офлайн без ключа хранилища. Используйте такие данные для расследования исключения, но настройте закрепление хоста, ограничения аккаунта и текст подтверждения до того, как инцидент даст вам повод открыть журнал.

Первое операционное изменение простое: найдите все конфигурации SSH агентов, которые принимают неизвестный хост, замените такое поведение отдельным файлом с закреплёнными хостами и проверьте действующие параметры через `ssh -G`. Вы обнаружите устаревшие псевдонимы, случайные универсальные правила и аккаунты с полномочиями, намного превышающими требования их задачи. Именно эти соединения стоит исправить до того, как агент станет быстрее.
