SSH-клиент читает ~/.ssh/config строчку за строчкой, но без переменных файл быстро превращается в копипасту. Разбираю, как Host *, Match exec и подстановки %h, %r, %l сокращают конфиг в разы и закрывают реальные сценарии — от динамической маршрутизации до проброса агента через bastion.
Шаблоны и wildcards в SSH config
SSH поддерживает glob-подобные шаблоны в директиве Host. Самая популярная — Host *, но работают и составные паттерны.
# Все хосты без явного описания получат общие настройкиHost *
ServerAliveInterval 60 ServerAliveCountMax 3 IdentitiesOnly yes
# Все хосты домена example.comHost *.example.com
User ubuntu
Port 22# Хосты по маске: web-01, web-02, web-03Host web-0?
IdentityFile ~/.ssh/id_ed25519_web
Примечание
SSH читает конфиг сверху вниз и применяет первую подходящую директиву Host. Более конкретные правила ставьте выше общих.
# Правильный порядок: частное перед общимHost bastion
HostName bastion.example.com
User admin
Host *.internal
ProxyJump bastion
Host *
ServerAliveInterval 60
Шаблоны можно комбинировать в одном Host через пробел — это работает как логическое ИЛИ.
Host dev-* staging-*
User deploy
IdentityFile ~/.ssh/id_ed25519_deploy
Переменные подстановки: %h, %r, %l
SSH подставляет переменные в значения директив на этапе обработки конфига. Три основных:
Переменная
Значение
Пример
%h
Имя хоста из командной строки
ssh web-01 → web-01
%r
Имя пользователя на удалённой машине
ubuntu
%l
Локальное имя пользователя
alex
Подстановка работает в большинстве директив — ProxyCommand, IdentityFile, LocalCommand, RemoteCommand.
Host jump-*
HostName %h.internal
ProxyJump bastion.example.com
User deploy
Host backup-*
HostName %h.backup.local
User backup
# Логин совпадает с локальным пользователем IdentityFile ~/.ssh/id_ed25519_%r
Переменная %h заменяется на то, что вы передали после ssh. Это позволяет написать одно правило на сотню хостов.
# Вместо пяти отдельных блоковHost server-{01..05} HostName %h.internal.corp
User ansible
ProxyJump bastion
Подсказка
%h подставляет то, что вы набрали, а неresolved HostName. ssh web-01 подставит web-01, а не его IP.
Match exec и динамическая маршрутизация
Директива Match позволяет задавать правила на основе условий. Без exec это проверка по user, host, localuser. С Match exec — произвольная логика через shell-команду.
# Подключаться через bastion только для внутренних хостовMatch host 10.* exec"echo true" ProxyJump bastion.example.com
Условие exec выполняется на локальной машине. Команда должна вернуть 0 (успех) для применения блока Match.
# Прокидывать агента только при подключении к prodMatch exec"[ '%h' = prod-* ]" AddKeysToAgent yes
IdentityAgent SSH_AUTH_SOCK
# Разная маршрутизация в зависимости от сетиMatch exec"hostname -I | grep -q 192.168.1" ProxyCommand none # локальная сеть, прямой доступMatch exec"[ '%h' != prod-* ]" ProxyJump office-jump
Предупреждение
Match exec выполняется shell-оболочкой. Обратные кавычки и переменные раскрываются. Для сложной логики выносите проверку в отдельный скрипт.
Match exec"/home/alex/.ssh/route-check.sh %h" ProxyJump bastion
# route-check.sh#!/bin/bashHOST="$1"if[["$HOST"== prod-* ]];thenexit1# для prod нужна другая логикаfiexit0
Escape символа: как написать %
Если нужно передать символ % literally — в RemoteCommand, ProxyCommand или LocalCommand — используйте %%.
# SSH в Docker-контейнер с evalHost docker-*
HostName %h
User docker
RemoteCommand docker exec -it %h /bin/sh -c "eval $(ssh-agent -s) && exec /bin/sh"
# Добавить timestamp в remote commandHost *
RemoteCommand echo"%%Y-%%m-%%d %%H:%%M:%%S connected to %h" >> /tmp/ssh-log
# Проброс порта через nc на удалённом хостеHost relay
HostName relay.example.com
ProxyCommand ssh -W %h:%p user@bastion
# или классический nc ProxyCommand nc -Z %h 22
Примеры для dev, staging, prod
Типичная схема: один файл, три окружения, общая база.
# ============================================# Базовые настройки для всех# ============================================Host *
ServerAliveInterval 60 ServerAliveCountMax 3 IdentitiesOnly yes
AddKeysToAgent yes
# ============================================# Окружения: динамический HostName# ============================================Host dev-*
HostName %h.dev.internal
User developer
IdentityFile ~/.ssh/id_ed25519
Host staging-*
HostName %h.staging.internal
User deploy
IdentityFile ~/.ssh/id_ed25519_deploy
# Дополнительныйjump через staging-bastion ProxyJump staging-bastion
Host prod-*
HostName %h.prod.internal
User deploy
IdentityFile ~/.ssh/id_ed25519_prod
ProxyJump prod-bastion
# Только через агент, без файлового ключа на диске IdentityAgent SSH_AUTH_SOCK
# ============================================# Match: динамическая логика# ============================================# Прокидывать агента только на prod и stagingMatch exec"[ '%h' = prod-* ] || [ '%h' = staging-* ]" ForwardAgent yes
# Отключать ForwardAgent для dev (параноидальная настройка)Match host dev-* exec"true" ForwardAgent no
# ============================================# Переменные подстановки в командах# ============================================Host db-*
HostName %h.internal
User dbadmin
RemoteCommand psql -U postgres -c "SELECT now(), '%r'"# ============================================# Escape %% в аргументах# ============================================Host log-*
HostName %h
RemoteCommand echo"Connected at %%Y-%%m-%%d" >> /var/log/ssh-connections.log
После этого конфига ssh prod-web-03 подключается к prod-web-03.prod.internal через prod-bastion с ключом id_ed25519_prod, а ssh dev-app-01 — напрямую.
# Проверить, что SSH видит ваш конфигssh -G dev-app-01 | grep -E "^hostname|^user|^proxyjump"# Тест соединения без выполнения командыssh -v prod-db-01 echo"connected"
Примечание
-G выводит все параметры, которые SSH получит после разбора конфига для указанного хоста. Используйте для отладки перед реальным подключением.
Комбинация Host-шаблонов, подстановки переменных и Match exec превращает SSH config из набора копипаст в систему с динамической маршрутизацией. Один файл покрывает dev/staging/prod без дублирования, а %h, %r, %l избавляют от ручной правки при добавлении новых хостов.