# Блоки Match в конфигурации SSH и одобрение агента

Одобрение SSH имеет смысл только тогда, когда одобренное соединение совпадает с тем, которое SSH действительно установит. На первый взгляд это очевидно, пока агент не запускает `ssh prod`, алиас хоста не выбирает другой `HostName`, блок `Match` не меняет удаленного пользователя, а `ProxyJump` не отправляет сеанс через bastion, о котором никто не упомянул в запросе.

Большинство ошибок в конфигурации SSH переживаемы, если человек наблюдает за терминалом. Он видит незнакомый запрос о ключе хоста, замечает `root@...` или помнит, что `prod` означает другое в офисной сети. У автономного агента нет такой настороженности. Он использует переданный алиас и точно следует конфигурации.

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

## Алиас хоста - это входные данные, а не идентификатор

`ssh app-prod` не сообщает, куда именно подключится SSH, какую учетную запись он использует, какой ключ предложит и пройдет ли трафик сначала через другую машину. Команда сообщает OpenSSH, с какого параметра конфигурации начать.

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

OpenSSH сначала читает параметры командной строки, затем пользовательский `~/.ssh/config`, а потом системную конфигурацию. Для большинства параметров с одним значением используется первое полученное значение. В руководстве OpenSSH `ssh_config(5)` прямо сказано о практическом следствии: размещайте конкретные объявления раньше, а общие значения по умолчанию позже. Широкий блок `Host *` в начале файла может незаметно сделать более позднее условное правило бесполезным.

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

| Алиас | Разрешенное назначение | Удаленный пользователь | Маршрут | Назначение ключа |
|---|---|---|---|---|
| `staging-api` | `api-01.staging.example.net` | `deploy` | напрямую | ключ для staging-деплоя |
| `prod-api` | `api-01.prod.example.net` | `deploy` | `prod-bastion` | ключ для production-деплоя |
| `prod-breakfix` | `api-01.prod.example.net` | `ops` | `prod-bastion` | ключ только для инцидентов |

В столбце назначения не пишите `production`. Указывайте реальный адрес, который использует SSH. Не пишите `пользователь по умолчанию`. Укажите `deploy`, `ubuntu`, `ec2-user` или любую другую учетную запись, которую получает сервер. Если в маршруте есть jump-хост, назовите его. Если алиас ведет себя по-разному в разных сетях, для него нужна отдельная строка: это разные итоговые соединения.

Полезно различать следующее: **алиас указывает на запись конфигурации, а назначение указывает на удаленную конечную точку**. Если смешать эти понятия, одобрения становятся ненадежными. Проверяющий может согласиться на сеанс агента с `staging-api` и все равно ошибиться насчет конечной точки, потому что в алиасе есть устаревшее или условное поведение.

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

## Блоки Match - это исполняемая логика соединения

Блок `Match` не является меткой для группы хостов. Это условный раздел `ssh_config`, который меняет применяемые директивы. Условия могут включать запрошенный хост, исходный хост, удаленного пользователя, локального пользователя, состояние каноникализации, локальную сеть, запрошенную команду и команду `exec`, которую SSH запускает через локальную оболочку.

Такая мощность полезна. Но из-за нее конфигурация может содержать поведение, незаметное при проверке только соседнего алиаса `Host`.

Рассмотрим эту конфигурацию:

```sshconfig
Host prod-api
    HostName api-01.prod.example.net
    User deploy
    ProxyJump prod-bastion

Match originalhost prod-api user root
    IdentityFile ~/.ssh/breakfix_ed25519
    IdentitiesOnly yes
```

Проверяющий, который читает только блок `Host prod-api`, видит соединение для деплоя от имени `deploy`. Если вызывающая сторона запускает `ssh -l root prod-api`, условие `Match originalhost prod-api user root` может сработать. Применится ли настройка идентификатора, также зависит от того, где были получены более ранние настройки идентификаторов и поддерживает ли параметр несколько значений. Важнее другое: соединение изменилось из-за аргумента пользователя в командной строке, а не из-за изменения алиаса.

Для работы агента избегайте правил `Match user`, которые дают более привилегированный ключ или маршрут. Такое правило кажется аккуратным, потому что группирует поведение по имени учетной записи. Но вызывающий инструмент легко меняет его через `-l`, `user@host` или сгенерированную команду. Вместо этого задавайте нужного пользователя непосредственно в алиасе с конкретным назначением.

Более безопасный вариант явно разделяет намерения:

```sshconfig
Host prod-deploy
    HostName api-01.prod.example.net
    User deploy
    ProxyJump prod-bastion
    IdentityFile ~/.ssh/prod_deploy_ed25519
    IdentitiesOnly yes

Host prod-breakfix
    HostName api-01.prod.example.net
    User ops
    ProxyJump prod-bastion
    IdentityFile ~/.ssh/prod_breakfix_ed25519
    IdentitiesOnly yes
```

Это не делает привилегированный доступ безобидным. Зато запрошенное соединение становится понятным. Для вызова `prod-breakfix` агенту все равно нужно отдельное разрешение, но он не сможет случайно получить такое поведение, просто изменив имя пользователя.

К `Match exec` в конфигурации, доступной агенту, нужно относиться еще осторожнее. Он запускает локальную команду оболочки, пока SSH оценивает конфигурацию. Команды такого типа используют для определения сети, поиска сведений об инвентаре или выбора учетных данных. В результате попытка SSH-соединения превращается в локальный путь выполнения с зависимостями от окружения. Если такая гибкость нужна для работы человека, держите эти алиасы за пределами набора, который может вызывать агент. Проверка соединения не должна требовать обратного анализа произвольной команды оболочки.

## Первое совпавшее значение может лишить смысла ваше исключение

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

Предположим, разработчик пытается обязать production-трафик проходить через bastion и пишет так:

```sshconfig
Host *
    User deploy
    ProxyJump dev-bastion

Host prod-*
    ProxyJump prod-bastion
```

Ожидание понятно: `prod-*` выглядит более конкретным, значит, он должен победить. Но OpenSSH не сортирует блоки по степени конкретности. Он обрабатывает их по порядку в файле, и для многих директив используется первое полученное значение. `prod-api` сохранит `dev-bastion`, потому что более ранний `Host *` уже задал `ProxyJump`.

Размещайте конкретные блоки раньше:

```sshconfig
Host prod-*
    ProxyJump prod-bastion

Host *
    User deploy
    ServerAliveInterval 30
```

Это не вопрос стиля. Маршрут через неправильный bastion может отправить сеанс не в ту сетевую зону. Общее значение `User deploy` может привести к тому, что production-алиас будет входить под учетной записью, которой не место на этом хосте. Общее значение `IdentityFile` может предложить неожиданные учетные данные раньше нужных.

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

Для автоматизированных соединений выбор идентификатора должен быть простым и явным:

```sshconfig
Host prod-deploy
    HostName api-01.prod.example.net
    User deploy
    IdentityFile ~/.ssh/prod_deploy_ed25519
    IdentitiesOnly yes
    ProxyJump prod-bastion
```

`IdentitiesOnly yes` сообщает OpenSSH использовать только идентификаторы, настроенные в конфигурации SSH или переданные в командной строке, вместо того чтобы свободно перебирать все идентификаторы, доступные агенту. Это не исправляет неаккуратную конфигурацию. Но оно не дает случайному ключу из локального агента стать неожиданным кандидатом.

Популярная рекомендация - поместить все значения по умолчанию в `Host *`, а переопределять их только при необходимости. Для безобидных параметров, например интервалов keepalive, это разумно. Для выбора пользователя, маршрутов, файлов идентификаторов, портов, `ProxyCommand` и переписывания имен хостов это плохая практика. Настройки по умолчанию, влияющие на полномочия, должны быть редкими. Небольшое повторение дешевле, чем объяснение, почему агент пришел на правильную машину неправильным путем.

## ProxyJump создает еще одно соединение, которое нужно проверять

`ProxyJump` не просто дополняет соединение с назначением. Сначала SSH подключается к jump-хосту, затем создает через него TCP-канал к назначению. Можно указать несколько прокси и пройти их последовательно. В руководстве OpenSSH также предупреждается, что настройки хоста назначения обычно не применяются к jump-хостам.

Именно здесь проверки часто дают сбой. Конфигурация может быть точной для `prod-api` и совершенно неопределенной для `prod-bastion`.

```sshconfig
Host prod-api
    HostName 10.40.8.17
    User deploy
    ProxyJump prod-bastion

Host prod-bastion
    HostName bastion.prod.example.net
    User jump
    IdentityFile ~/.ssh/bastion_ed25519
    IdentitiesOnly yes
```

Здесь есть два решения об аутентификации и два идентификатора хоста:

1. SSH аутентифицирует локальный клиент на `bastion.prod.example.net` как `jump`.
2. Bastion передает TCP-поток на `10.40.8.17`.
3. SSH аутентифицируется через этот поток на конечном хосте как `deploy`.

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

Проверяйте каждый переход через `ssh -G`, а не только конечный алиас:

```sh
ssh -G prod-api | egrep '^(hostname|user|port|proxyjump|identityfile|identitiesonly) '
ssh -G prod-bastion | egrep '^(hostname|user|port|identityfile|identitiesonly) '
```

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

```text
hostname api-01.prod.example.net
user deploy
port 22
proxyjump prod-bastion
identityfile ~/.ssh/prod_deploy_ed25519
identitiesonly yes
```

Затем отдельно проверьте bastion. Если `prod-api` использует цепочку вроде `edge-bastion,prod-bastion`, выполните команду для обоих алиасов. Цепочка - не один непрозрачный маршрут. Это несколько отдельных конфигураций SSH-клиента.

Не задавайте общий маршрут через jump-хост в `Host *` или в широком шаблоне вроде `Host *.internal`. Он может захватить временные хосты, staging-окружения и алиасы, добавленные несколько месяцев спустя. Определяйте маршрут там, где он нужен, в алиасах, которые его требуют. Если он нужен многим production-алиасам, используйте узкий шаблон, зарезервированный для production, и не применяйте его случайно.

Проверьте также `ProxyCommand`. OpenSSH рассматривает `ProxyJump` и `ProxyCommand` как конкурирующие параметры: тот, который указан первым, не дает последующим экземплярам другого параметра вступить в силу. Конфигурация, которая выглядит как подключение через bastion, может на самом деле выполнять более раннюю proxy-команду. Проверяющий должен отметить любую из этих настроек, поскольку обе меняют место, откуда начинается сетевое соединение, и путь к назначению.

## Выбор пользователя меняет полномочия, которые вы одобряете

Удаленная учетная запись входит в состав запрошенного действия. `deploy@api-01.prod.example.net` и `ops@api-01.prod.example.net` могут попасть на один сервер, но у них разные полномочия, профиль оболочки, принудительные команды, права `sudo` и аудит.

SSH может получить удаленного пользователя из нескольких источников: `user@host` в команде, `ssh -l user host`, директива `User` или локальное имя пользователя, если больше ничего не задано. Проверка конфигурации, которая спрашивает только «На какой хост?», неполна.

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

```sshconfig
Host inventory-read
    HostName inventory.prod.example.net
    User inventory_ro
    IdentityFile ~/.ssh/inventory_ro_ed25519
    IdentitiesOnly yes

Host inventory-deploy
    HostName inventory.prod.example.net
    User deploy
    IdentityFile ~/.ssh/inventory_deploy_ed25519
    IdentitiesOnly yes
```

Не выдавайте агенту общий адрес хоста, рассчитывая, что запрос или оболочка не дадут сменить учетную запись. Генератор команд с такой же легкостью создаст `ops@inventory.prod.example.net`, как и `deploy@inventory.prod.example.net`. Конфигурация должна делать разрешенный путь самым простым, а привилегированные пути - явно отличающимися.

Проверьте варианты, которые может сгенерировать инструмент:

```sh
ssh -G inventory-read | grep '^user '
ssh -G -l ops inventory-read | grep '^user '
ssh -G ops@inventory-read | grep '^user '
```

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

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

## IdentityFile определяет больше, чем путь к ключу

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

Типичная проблема выглядит так:

```sshconfig
Host *
    IdentityFile ~/.ssh/id_ed25519

Host prod-*
    IdentityFile ~/.ssh/prod_ed25519
```

Оператор считает, что production использует `prod_ed25519`. Но SSH может добавить оба файла в список кандидатов, поскольку `IdentityFile` допускает несколько значений. Если в SSH-агенте загружены дополнительные ключи, а `IdentitiesOnly` не задан, SSH может предложить и их. Некоторые серверы рано отклоняют повторные предложения, другие принимают неожиданный ключ, который случайно дает доступ. Ни один вариант не выражает намерение ясно.

Алиас для агента должен указывать одну цель использования учетных данных и ограничивать предложения:

```sshconfig
Host reports-export
    HostName reports.prod.example.net
    User exporter
    IdentityFile ~/.ssh/reports_export_ed25519
    IdentitiesOnly yes
```

Затем изучите итоговую конфигурацию, а не доверяйте одному блоку:

```sh
ssh -G reports-export | grep '^identityfile '
ssh -G reports-export | grep '^identitiesonly '
```

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

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

То же правило относится к именам ключей. Путь вроде `~/.ssh/id_ed25519` ничего не говорит о цели использования. `prod_deploy_ed25519` лучше, но конфигурация должна полностью объяснять, какая группа хостов, какой пользователь и какой маршрут используют этот ключ. Имена файлов помогают проверке, но не заменяют ее.

## Каноникализация может заставить один алиас совпасть дважды

Каноникализация имен хостов - один из наименее заметных способов изменить поведение SSH. Когда включен `CanonicalizeHostname yes`, OpenSSH может взять неполное имя, добавить настроенные суффиксы домена, разрешить его и повторно обработать конфигурацию уже с новым именем назначения. `Match canonical` применяется на этом втором проходе. `Match final` запрашивает окончательный разбор и совпадает во время него; если каноникализация включена, условия `canonical` и `final` совпадают вместе.

В больших внутренних сетях это может быть полезно. Но короткий алиас также может попасть в ловушку условной конфигурации.

```sshconfig
CanonicalizeHostname yes
CanonicalDomains corp.example.net

Host build
    User ci

Match canonical host *.prod.example.net
    ProxyJump prod-bastion
```

Вызывающая сторона вводит `ssh build`. На первом проходе виден `build`. Если каноникализация разрешает это имя как `build.prod.example.net`, SSH повторно читает конфигурацию, и блок `Match canonical host *.prod.example.net` может задать production-маршрут. Соединение изменилось не потому, что вызывающая сторона попросила другой алиас. Изменилась цель, которую увидели последующие правила, из-за DNS и второго прохода разбора.

В руководстве OpenSSH различаются два условия, которые часто считают взаимозаменяемыми:

- `Match originalhost` проверяет токен хоста в том виде, как его передала вызывающая сторона.
- `Match host` проверяет назначение после подстановки `HostName` или каноникализации.

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

У каноникализации есть еще одна важная особенность, связанная с bastion-хостами. `CanonicalizeHostname yes` обычно не применяется к соединениям через `ProxyCommand` или `ProxyJump`; значение `CanonicalizeHostname always` распространяет его на проксируемые соединения. Поэтому два похожих алиаса могут следовать разным правилам переписывания только потому, что у одного есть jump-хост.

Для разрешений агента обычно лучше всего работает простая политика: отключить каноникализацию для алиасов, передаваемых агенту, и использовать явные полные значения `HostName`. Если каноникализация необходима, проверяйте каждый разрешенный алиас в точном сетевом окружении, где работает агент. Не предполагайте, что короткое имя разрешается одинаково в домашней, корпоративной сети, через VPN и в офисной Wi-Fi-сети.

`Match localnetwork` вызывает ту же проблему. OpenSSH предупреждает, что адрес локальной сети ненадежен для чувствительной к безопасности конфигурации, особенно в сетях с DHCP. Для удобных настроек он подходит. Не используйте его для решения, получает ли агент более привилегированный ключ, пропускает ли bastion или получает ли доступ к production.

## Сначала сформируйте соединение, потом одобряйте его

`ssh -G` - самый быстрый способ превратить конфигурацию SSH из текста в проверяемый результат. Команда выводит настройки, которые SSH будет использовать после обработки правил `Host` и `Match`, а затем завершает работу, не открывая соединение.

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

```sh
ssh -G prod-deploy | egrep '^(hostname|user|port|proxyjump|proxycommand|identityfile|identitiesonly|canonicalizehostname) '
```

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

```sh
ssh -F ./agent-ssh-config -G prod-deploy > ./testdata/prod-deploy.effective
```

Проверяйте этот файл при каждом изменении конфигурации. Полезное сравнение обнаружит изменившиеся `hostname`, `user`, `proxyjump` или список идентификаторов до того, как они попадут в процесс одобрения. Даже шумное сравнение всей конфигурации лучше, чем доверие блоку, который кто-то вставил в pull request.

Используйте `ssh -vvv` только после того, как `ssh -G` покажет ожидаемые значения. Подробные журналы соединения помогают подтвердить, какие ключи хоста и методы аутентификации SSH действительно пробует, но смешивают решения конфигурации с сетевым шумом. Сначала `-G` отвечает на вопрос «Что говорит эта конфигурация?». Именно это нужно выяснить до поиска проблем с доступностью.

Проверяйте варианты намеренно:

```sh
ssh -F ./agent-ssh-config -G prod-deploy
ssh -F ./agent-ssh-config -G -l ops prod-deploy
ssh -F ./agent-ssh-config -G ops@prod-deploy
ssh -F ./agent-ssh-config -G prod-deploy.prod.example.net
```

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

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

## Конфигурация SSH для агента должна быть небольшой и целевой

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

Создайте отдельный файл конфигурации, содержащий только одобренные алиасы и нужные им jump-хосты. Передайте агенту или его оболочке выполнения этот файл через `-F`. Пусть у каждого алиаса будет одна задача, явные `HostName` и `User`, маршрут и назначение идентификатора. Не добавляйте условную логику, если не можете объяснить, почему статического алиаса недостаточно.

Компактный пример:

```sshconfig
Host prod-bastion
    HostName bastion.prod.example.net
    User jump
    IdentityFile ~/.ssh/prod_bastion_ed25519
    IdentitiesOnly yes

Host prod-deploy
    HostName api-01.prod.example.net
    User deploy
    ProxyJump prod-bastion
    IdentityFile ~/.ssh/prod_deploy_ed25519
    IdentitiesOnly yes

Host staging-deploy
    HostName api-01.staging.example.net
    User deploy
    IdentityFile ~/.ssh/staging_deploy_ed25519
    IdentitiesOnly yes
```

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

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

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

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