Клиентский конфиг SSH - это файл ~/.ssh/config (в Windows - C:\Users\<you>\.ssh\config). Каждый блок Host даёт подключению короткое имя и хранит всё, что иначе пришлось бы набирать вручную: HostName - настоящий адрес, User, Port, IdentityFile - ключ, ProxyJump - промежуточная машина. После этого ssh web заменяет ssh -i ~/.ssh/id_work -p 2222 deploy@203.0.113.10, и то же самое работает для scp, sftp, rsync, Git и любого редактора, который умеет SSH, потому что все они читают один и тот же файл.
Одно правило объясняет почти все неожиданности: для каждой настройки побеждает первое найденное значение. Конкретные блоки идут вверху, универсальный Host * - внизу, а параметр, указанный в командной строке, сильнее всего, что есть в файле. Запомните это, и остальная часть руководства станет справочником, из которого можно копировать.
Эта статья - про клиентскую сторону. Генерация ключей, агент и защита sshd_config на сервере описаны в материале SSH-ключи и защита sshd_config; если ключа у вас ещё нет, начните с него.
Где лежит конфиг#
Файлов два, и читаются они по порядку: сначала ваш, затем общесистемный.
| Система | Файл пользователя | Системный файл |
|---|---|---|
| Linux, BSD | ~/.ssh/config | /etc/ssh/ssh_config |
| macOS | ~/.ssh/config | /etc/ssh/ssh_config |
| Windows OpenSSH | %USERPROFILE%\.ssh\config | %ProgramData%\ssh\ssh_config |
Пока вы его не создадите, файла нет. В Linux и macOS запись в него не должна быть разрешена никому, кроме вас, иначе ssh откажется его использовать и выдаст Bad owner or permissions on /home/you/.ssh/config:
$ mkdir -p ~/.ssh && chmod 700 ~/.ssh$ touch ~/.ssh/config && chmod 600 ~/.ssh/configОбратите внимание на название: ssh_config - это клиент, а sshd_config (с буквой d, от daemon) - сервер. Правка не того файла - удивительно частый способ потерять полдня, а серверный файл лежит в том же каталоге /etc/ssh.
Два флага делают файл прозрачным. ssh -G name выводит полностью разобранную конфигурацию для имени без подключения - каждый параметр после применения всех блоков и значений по умолчанию. ssh -F /dev/null name подключается в обход вашего конфига и одним шагом показывает, в нём ли проблема. Флаг -G появился в OpenSSH 6.8, так что он есть на всём, что вам вероятно встретится.
$ ssh -G web | grep -E '^(hostname|user|port|identityfile|proxyjump) 'hostname 203.0.113.10user deployport 2222identityfile ~/.ssh/id_workproxyjump noneБлоки Host и как ssh выбирает значение#
Строка Host начинает блок, который продолжается до следующей строки Host или Match. Отступы нужны вам, а не ssh. Ключевые слова нечувствительны к регистру, значения в основном чувствительны, а писать можно как Keyword value, так и Keyword=value.
Host web HostName 203.0.113.10 User deploy Port 2222 IdentityFile ~/.ssh/id_work IdentitiesOnly yesHost *.lab User admin StrictHostKeyChecking accept-newHost * ServerAliveInterval 30 AddKeysToAgent yesКогда вы набираете ssh web, ssh проходит файл сверху вниз. Каждый блок, чей шаблон подходит, добавляет те настройки, которых ещё не было. Поэтому web получает порт из собственного блока, а ServerAliveInterval - из Host *. Если бы Host * стоял в начале и содержал User root, все более поздние строки User игнорировались бы - это классический вариант ситуации "мой конфиг не работает".
Исключение - небольшой набор параметров, которые накапливаются, а не берут первое значение: IdentityFile, CertificateFile, LocalForward, RemoteForward, DynamicForward и SendEnv. Несколько строк IdentityFile из нескольких подошедших блоков образуют список, который перебирается по порядку.
Шаблоны сопоставляются с тем именем, которое вы набрали, а не с HostName. Они поддерживают * и ?, несколько шаблонов в одной строке и отрицание через !:
Host web db cache User deployHost 10.0.0.* !10.0.0.1 ProxyJump bastionHost * !github.com ForwardAgent noОтрицаемый шаблон сам по себе ничему не соответствует; он лишь исключает из других шаблонов в той же строке. Host !github.com в одиночку - это блок, который не применяется ни к чему.
Match - для условий, которые Host выразить не может
Блоки Match проверяют условия, а не имена. Полезные из них - host (имя после применения HostName), originalhost (то, что вы набрали), user, localuser, exec и all.
# Use the office jump host only when not on the office networkMatch host 10.20.* !exec "ip route | grep -q 10.20.0.0/16" ProxyJump gw.example.comexec запускает команду оболочки и считается совпавшим, если она завершилась со статусом ноль. Она выполняется при каждом подключении, поэтому должна быть быстрой, и помните, что это команда оболочки на вашей машине - никогда не вставляйте такую, которую не понимаете.
Настройки, которые стоит знать#
Это параметры, которые заслуживают места в реальном конфиге. Значения по умолчанию - значения OpenSSH.
| Параметр | По умолчанию | Что делает |
|---|---|---|
HostName | набранное имя | Настоящий адрес или DNS-имя |
User | ваше локальное имя | Удалённая учётная запись |
Port | 22 | Удалённый порт SSH |
IdentityFile | стандартные файлы id_* | Приватный ключ, который предлагается серверу |
IdentitiesOnly | no | Предлагать только указанные ключи |
ProxyJump | нет | Сначала пройти через эти хосты |
ServerAliveInterval | 0 (выкл.) | Секунды между keepalive-запросами |
ServerAliveCountMax | 3 | Сколько пропущенных keepalive до отключения |
ConnectTimeout | системный таймаут TCP | Сколько секунд ждать подключения |
StrictHostKeyChecking | ask | Как относиться к неизвестным ключам хоста |
HostName принимает токены, поэтому один блок может обслуживать целое семейство машин. %h - это имя, которое вы набрали:
Host *.prod HostName %h.example.comЭто работает не совсем так, как ожидают: ssh api.prod подключится к api.prod.example.com. Если в ваших реальных именах нет .prod, пишите по одному блоку на хост. Хитрые шаблоны экономят строки, но стоят ясности, а конфиг - это файл, который вы будете читать в два часа ночи.
User стоит задавать в каждом блоке, потому что по умолчанию берётся ваше локальное имя пользователя. На ноутбуке с Windows оно часто начинается с заглавной буквы или похоже на адрес почты и не существует ни на одном сервере Linux, а ошибка в таком случае - обычный Permission denied, и ничто не указывает на имя пользователя.
Port важен в основном для машин, где SSH перенесли с порта 22, чтобы в логах было тише, и для всего, что держит на другом порту собственный сервис, говорящий по SSH. ssh -p 2222 в командной строке сильнее файла, как и любой параметр командной строки.
IdentityFile и IdentitiesOnly
IdentityFile называет приватный ключ, и путь может начинаться с ~. Без него ssh пробует стандартные имена в ~/.ssh - id_rsa, id_ecdsa, id_ed25519 и их варианты для аппаратных ключей - плюс всё, что загружено в агент.
С одним ключом такое поведение нормально. С несколькими оно приводит к сбою, который рано или поздно встречает каждый, у кого есть рабочий и личный ключ: ssh предлагает все ключи из агента один за другим, каждое предложение считается попыткой аутентификации, и сервер с MaxAuthTries 3 разрывает соединение с сообщением Too many authentication failures, не дойдя до нужного ключа. IdentitiesOnly yes велит ssh предлагать только те ключи, что названы для этого хоста, даже если в агенте их больше. Поместите его в Host * ближе к концу файла и указывайте ключ в каждом блоке, которому он нужен.
IdentityFile принимает и файл .pub, если приватная половина хранится в другом месте, например в менеджере паролей, который работает как агент. ssh использует публичный ключ, чтобы выбрать, какой ключ агента предложить, - это самый аккуратный способ совместить IdentitiesOnly с агентом, в котором лежит дюжина ключей.
Проверка ключа хоста без привычки жать yes-yes-yes
StrictHostKeyChecking accept-new (OpenSSH 7.6 и новее) без вопросов добавляет ключи хостов, которых вы ещё не видели, и по-прежнему громко отказывает, если ключ известного хоста изменился. Это разумная настройка для лаборатории из одноразовых машин и плохая - для боевого сервера, к которому вы подключаетесь впервые: там запрос - единственный момент, когда можно сверить отпечаток. Ограничивайте её шаблоном, а не помещайте в Host *. UserKnownHostsFile может направить группу хостов в отдельный файл, что удобно для тестовой сети, которую пересоздают каждую неделю:
Host *.lab StrictHostKeyChecking accept-new UserKnownHostsFile ~/.ssh/known_hosts_labProxyJump и jump-серверы#
Jump-сервер (его же называют bastion) - это одна машина, до которой можно добраться из интернета и которая стоит перед другими, недоступными напрямую: база данных в частной сети, игровой сервер за файрволом, пропускающим SSH только с bastion, или все внутренние машины небольшой компании. ProxyJump делает этот переход невидимым.
Host bastion HostName 203.0.113.10 User jump IdentityFile ~/.ssh/id_ed25519Host db HostName 10.0.0.20 User deploy IdentityFile ~/.ssh/id_ed25519 ProxyJump bastionТеперь ssh db подключается к bastion, просит его открыть TCP-соединение с 10.0.0.20:22 и запускает второй, полноценный сеанс SSH через этот туннель. Важно, где происходит аутентификация: на вашем ноутбуке, от начала до конца, для обоих переходов. Bastion пересылает зашифрованные байты и никогда не видит ваш приватный ключ, ваш агент и ваш сеанс. Поэтому ProxyJump и заменил пробрасывание агента как правильный способ добраться до второй машины.
Что стоит знать о нём:
- Он появился в OpenSSH 7.3. В более старых версиях эквивалент -
ProxyCommand ssh -W %h:%p bastion, который делает ту же работу и встречается в большинстве руководств до 2017 года. Не задавайте оба параметра для одного хоста: побеждает тот, что ssh прочитает первым. - Каждый переход ищется в конфиге.
ProxyJump bastionиспользует адрес, пользователя, порт и ключ из блокаbastion. Переход можно также записать прямо в строке какuser@host:port. - Цепочки задаются через запятую.
ProxyJump bastion,innerпроходит через оба хоста по порядку, и каждый переход снова разбирается по конфигу. - В командной строке это `-J`.
ssh -J jump@203.0.113.10 deploy@10.0.0.20- разовая форма, иscpсsftpтоже принимают-J. - `ProxyJump none` в более конкретном блоке отключает переход, унаследованный от широкого шаблона ниже по файлу.
Целевая машина видит подключение как приходящее с частного адреса bastion, поэтому правила файрвола на внутренней машине должны разрешать SSH с этого адреса и ни с какого другого. На самом bastion sshd должен разрешать TCP-проброс: AllowTcpForwarding no или DisableForwarding yes в его sshd_config ломает любой переход сообщением channel 0: open failed: administratively prohibited.
Учётная запись на bastion, которая умеет только перебрасывать
Bastion полезнее, когда учётная запись на нём не может ничего, кроме проброса. В authorized_keys пользователя jump на bastion параметры ключа из руководства по защите делают именно это:
restrict,port-forwarding,permitopen="10.0.0.20:22",permitopen="10.0.0.21:22" ssh-ed25519 AAAAC3Nza... laptoprestrict отключает всё необязательное, port-forwarding снова включает проброс, а permitopen ограничивает его перечисленными адресами назначения. С таким ключом одиночный ssh bastion не даёт оболочки, зато ssh db работает. Украденный ключ открывает два порта SSH на двух частных машинах, которым всё равно нужны собственные ключи.
Keepalive и зависающие соединения#
Сеанс, который через несколько минут простоя не закрывается, а зависает, почти всегда - результат того, что запись в таблице NAT домашнего роутера, мобильной сети или корпоративного файрвола тихо удалили. Исправляется это на стороне клиента:
Host * ServerAliveInterval 30 ServerAliveCountMax 3Каждые 30 секунд тишины ssh отправляет запрос внутри зашифрованного канала и ждёт ответа. Этот трафик не даёт промежуточным устройствам забыть соединение, а если подряд пропадают три ответа, ssh закрывает сеанс примерно через 90 секунд, а не оставляет замёрзший терминал. Не путайте это с TCPKeepAlive, который включён по умолчанию и работает на уровне TCP; его проще подделать, а тайминг определяется операционной системой и часто составляет два часа, что совершенно бесполезно против роутера, который забывает соединение через пять минут.
ConnectTimeout 10 - парная настройка для скриптов. Без неё подключение к выключенной машине ждёт системного таймаута TCP, который может быть заметно больше минуты, и скрипт резервного копирования, обходящий хосты в цикле, застревает на первом же недоступном.
Переиспользование соединения с ControlMaster#
Мультиплексирование позволяет последующим сеансам к тому же хосту ехать по TCP-соединению первого. Второй ssh web, каждый scp и каждый шаг запуска Ansible или git pull стартуют мгновенно и не проходят аутентификацию заново. На медленном канале или через jump-сервер разница - секунды на каждую команду.
Host * ControlMaster auto ControlPath ~/.ssh/cm-%C ControlPersist 10mControlMaster auto делает первое соединение главным и позволяет последующим им пользоваться. ControlPath - локальный файл сокета; %C - хеш параметров соединения (локальный хост, удалённый хост, порт, пользователь), поэтому он уникален для каждого назначения и всегда короткий. Это важно: у сокетов Unix есть ограничение на длину пути порядка сотни символов, а старый стиль %r@%h:%p ломается на длинных именах хостов. ControlPersist 10m держит главное соединение открытым в фоне десять минут после закрытия последнего сеанса.
Главным соединением управляют через -O:
$ ssh -O check web # is a master running for this host?Master running (pid=48213)$ ssh -O exit web # close it and every session on itОграничения реальны, и именно из-за них некоторые оставляют это выключенным:
- Всё делит одно соединение. Если сеть пропадёт, все сеансы на нём умрут разом.
- Настройки определяет главное соединение. Новый сеанс с другими параметрами, например с пробросом порта, присоединяется к существующему соединению, а параметры уровня соединения, включая пользователя и путь через jump-серверы, принадлежат главному. Если изменение конфига, похоже, ни на что не влияет, сначала выполните
ssh -O exit. - Сокет - это учётные данные. Любой, кто может открыть этот файл, способен воспользоваться вашим уже аутентифицированным соединением, поэтому он должен лежать в каталоге, который читаете только вы.
- Windows OpenSSH это не поддерживает. Во встроенном клиенте нет мультиплексирования через сокеты Unix, и строка
ControlMasterприводит к ошибкам, а не к скорости. Используйте защиту черезMatch exec, держите её в отдельном файле для машин с Linux и macOS или работайте через WSL.
Windows OpenSSH: пути и подводные камни#
Windows 10 (1809 и новее) и Windows 11 поставляются с клиентом OpenSSH как C:\Windows\System32\OpenSSH\ssh.exe, и он читает конфиг в том же формате. Файл пользователя - C:\Users\<you>\.ssh\config, а ~ в IdentityFile работает. Пути с пробелами нужно брать в двойные кавычки; прямые слэши вовсе снимают вопросы об экранировании:
Host web HostName 203.0.113.10 User deploy IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yesHost db HostName 10.0.0.20 User deploy IdentityFile "C:/Users/you/Key Store/id_db" ProxyJump webВсе подводные камни лежат вне синтаксиса.
Кодировка. В Windows PowerShell 5.1 команда echo "Host web" > config пишет UTF-16, и ssh отвергает файл с невнятным Bad configuration option на первой строке. Создайте файл командой New-Item -ItemType File $HOME\.ssh\config и правьте его в редакторе, который сохраняет обычный UTF-8. PowerShell 7 по умолчанию пишет UTF-8 и этой проблемы не имеет.
Права на ключ. Windows ssh проверяет ACL приватных ключей и отказывается от ключа, который могут читать другие учётные записи, с сообщением UNPROTECTED PRIVATE KEY FILE. Обычно это случается с ключом, скопированным откуда-то ещё. Уберите унаследованные права и дайте доступ только себе:
icacls "$HOME\.ssh\id_ed25519" /inheritance:ricacls "$HOME\.ssh\id_ed25519" /grant:r "$($env:USERNAME):(R)"Агент - это служба. AddKeysToAgent yes ничего не делает, пока не запущен OpenSSH Authentication Agent. Из PowerShell с правами администратора:
Get-Service ssh-agent | Set-Service -StartupType AutomaticStart-Service ssh-agentДве программы ssh. Git for Windows поставляет собственный ssh в C:\Program Files\Git\usr\bin. Он читает тот же ~/.ssh/config, но использует свой агент, поэтому ключи, добавленные Windows-версией ssh-add, для Git невидимы. Исправить это можно, указав Git системный клиент: git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe". Расширение Remote-SSH для VS Code читает тот же файл конфига, так что каждое имя Host, которое вы задали, появляется в его списке.
Рецепты, которые нужны снова и снова#
Два аккаунта GitHub на одной машине
GitHub определяет вас по ключу, а каждый аккаунт подключается как git@github.com, поэтому второму аккаунту нужны второй ключ и псевдоним:
Host github-work HostName github.com User git IdentityFile ~/.ssh/id_work IdentitiesOnly yesКлонируйте, подставив псевдоним вместо имени хоста, - git clone git@github-work:company/app.git - и проверяйте командой ssh -T github-work. Без IdentitiesOnly агент может предложить личный ключ первым, GitHub его примет, и вы отправите изменения не от того человека.
Доступ к базе данных через SSH
База данных, которая на сервере слушает только 127.0.0.1, доступна с вашего ноутбука через проброс порта, который можно хранить в конфиге:
Host web-pg HostName 203.0.113.10 User deploy LocalForward 15432 127.0.0.1:5432 SessionType nonessh web-pg затем держит туннель открытым без оболочки (SessionType none требует OpenSSH 8.7; на более старых клиентах используйте ssh -N web-pg), а клиент на вашей машине подключается к localhost:15432. Это часто лучший ответ, чем открывать порт базы данных в интернет; подробно об этом говорится в статье удалённые подключения к PostgreSQL.
Разделение файла через Include
Include (OpenSSH 7.3 и новее) читает другие файлы, в том числе по маске, а относительные пути отсчитываются от ~/.ssh. Ставьте его в самое начало, вне любого блока, иначе он будет действовать только внутри блока, в котором стоит:
Include config.d/*Host * IdentitiesOnly yes ServerAliveInterval 30Один файл на клиента или проект в ~/.ssh/config.d/ оставляет длинный конфиг читаемым и позволяет удалить все хосты проекта за один шаг.
Управляемые серверы по SFTP
У игрового, прикладного или веб-сервера на хостинг-панели обычно нет оболочки, но блок пригодится и для его доступа по SFTP. На RE:NODE SFTP доступен на каждом сервере, а учётные данные выдаются на сервер, а не на аккаунт, поэтому блок на каждый сервер с хостом, портом и именем пользователя из панели позволяет заменить длинную строку командой sftp mc-world, а редакторы, читающие конфиг, покажут его по имени. Пароль всё равно запрашивается каждый раз; не пытайтесь навязать ключ через IdentitiesOnly в таком блоке. О клиентах и о стороне панели рассказано в статье SFTP и файловый менеджер.
Полный конфиг для старта#
Собранный вместе, конфиг для одного VDS, закрытой машины за ним и GitHub выглядит так:
Include config.d/*Host vds HostName 203.0.113.10 User deploy IdentityFile ~/.ssh/id_ed25519Host db HostName 10.0.0.20 User deploy IdentityFile ~/.ssh/id_ed25519 ProxyJump vdsHost github.com User git IdentityFile ~/.ssh/id_ed25519Host * IdentitiesOnly yes AddKeysToAgent yes ServerAliveInterval 30 ServerAliveCountMax 3 ConnectTimeout 10 ControlMaster auto ControlPath ~/.ssh/cm-%C ControlPersist 10mВ Windows удалите три строки Control. Если сервер свежий, учётную запись и файрвол на другой стороне разбирает материал первый час на новом VDS, а руководство по ufw показывает, как разрешить SSH только с bastion, когда jump-сервер уже настроен.
Диагностика конфига#
| Симптом | Обычная причина |
|---|---|
| Настройки словно игнорируются | Их раньше задал предыдущий блок |
Too many authentication failures | Агент предлагает все ключи; добавьте IdentitiesOnly yes |
Bad owner or permissions | Конфиг или ключ доступен на запись другим |
Bad configuration option на строке 1 | Файл в UTF-16 из PowerShell 5.1 |
administratively prohibited на переходе | На bastion отключён проброс |
| Изменение конфига не действует | Соединение ControlMaster ещё открыто |
Could not resolve hostname | Для имени нет блока Host или в нём опечатка |
Для всего остального метод один и тот же - три шага. ssh -G name показывает, какими ssh считает настройки. ssh -v name показывает, какие строки конфига применены, какие ключи предложены и где остановилось подключение; при ProxyJump вывод охватывает оба перехода, так что смотрите, на каком из них сбой. ssh -F /dev/null user@address полностью включает файл в круг подозреваемых или исключает его.
FAQ#
Используют ли scp, rsync и Git мой конфиг SSH?
Да. Все они запускают системный клиент ssh, который читает ~/.ssh/config, поэтому псевдоним Host работает везде, где вы написали бы имя хоста: scp file web:/tmp/, rsync -a dir/ web:/srv/, git@github-work:org/repo.git. Программы со своей реализацией SSH, например некоторые графические клиенты SFTP, могут игнорировать файл или читать только его часть.
В чём разница между ProxyJump и пробросом агента?
При ProxyJump оба входа происходят на вашей машине, а промежуточный хост лишь передаёт байты. При пробросе агента (ssh -A) вы входите на промежуточный хост и затем пользуетесь своим агентом оттуда, так что любой с root на той машине может позаимствовать ваши ключи, пока вы подключены. Используйте ProxyJump всякий раз, когда цель - добраться до другой машины.
Можно ли одновременно использовать другой порт и jump-сервер?
Да. Port в блоке относится к хосту этого блока, а каждый переход разбирается отдельно, так что для bastion на порту 2222 и цели на 22 нужны только их собственные строки Port. В строке то же самое делает ProxyJump jump@203.0.113.10:2222.
Безопасно ли включать ControlMaster для всех хостов сразу?
В Linux и macOS для интерактивной работы это безопасно, если ControlPath указывает внутрь вашего собственного каталога ~/.ssh. Некоторые исключают хосты, которые используются для долгих передач файлов, поскольку один застрявший главный сеанс останавливает все сеансы на нём. В Windows не включайте его вовсе.
Почему ssh всё равно спрашивает про ключ или берёт не тот?
Обычно в агенте лежит несколько ключей, а IdentitiesOnly не задан, либо в пути IdentityFile опечатка, и ssh молча откатывается к ключам по умолчанию. ssh -v перечисляет по порядку все предложенные ключи, а ssh -G показывает, какие строки identityfile он действительно разобрал.


