Привет, Хабр!

Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.

Думаю, многим администраторам знакома ситуация, когда сеть работает (в смысле нормально работает), пинг до сервера летает, ключи на месте, но при этом ssh user@host упорно висит десять, двадцать, а то и тридцать секунд, прежде чем показать приглашение.

Если при выполнении интерактивных задач это просто раздражает, то в продуктиве это становится настоящей проблемой. Так, Ansible‑плейбуки упираются в таймауты, CI‑пайплайны «залипают» на этапе деплоя, а дежурный, пытающийся зайти на упавший сервер, теряет драгоценные минуты.

Тезис сегодняшней статьи довольно простой и заключается в том, что в подавляющем большинстве случаев виноваты два параметра: GSSAPIAuthentication на стороне клиента и UseDNS на стороне сервера.

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

SSH‑handshake изнутри

Для начала давайте разберёмся с тем, как устроен SSH‑handshake и где он может зависать.

И для начала давайте вспомним матчасть, а именно из каких этапов состоит подключение по SSH:

  1. TCP‑connect — установка соединения на порт 22.

  2. Обмен данными и согласование параметров.

  3. Обмен ключами (Key Exchange).

  4. Аутентификация.

  5. Установление защищённого канала.

Здесь важно понимать, что задержка почти всегда локализована в одном конкретном этапе, и запуск подробного вывода ssh -vvv показывает, в каком именно.

  • Если пауза возникает до строки debug1: SSH2_MSG_KEXINIT, то это означает, что почти наверняка клиент пытается достучаться до Kerberos KDC.

  • Если же пауза после начала аутентификации, то сервер, скорее всего, делает обратный DNS‑запрос.

  • А зависание прямо на этапе Connection established говорит о проблеме в сети или фаерволе, который молча дропает пакеты вместо того, чтобы ответить RST.

И дальше наша задача сводится к тому, чтобы аккуратно замерить, где именно теряется время.

Подробный вывод и чтение лога

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

ssh -vvv user@host 2>&1 | ts '%.s'

Утилита ts из пакета moreutils проставляет секунды перед каждой строкой, и здесь мы можем сразу увидеть, где задержка.

Если ts нет, то можно писать лог в файл и смотреть на него через less:

ssh -vvv -E /tmp/ssh-debug.log user@host
less /tmp/ssh-debug.log

В «медленном» логе вы увидите примерно такую картину: последняя строка перед паузой — что‑то вроде debug1: Authenticating to host:22 as 'user', а следующая появляется через полминуты.

Именно эти две строки и есть границы нашей проблемы. Дальше остаётся только сопоставить их с этапами handshake из предыдущего раздела.

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

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

Причина первая: GSSAPI/Kerberos на клиенте

Реализация OpenSSH на многих дистрибутивах (особенно в корпоративных сборках и на RHEL‑подобных системах) по умолчанию включает параметр GSSAPIAuthentication yes.

Это означает, что клиент при подключении пытается получить билет Kerberos у KDC, причём даже если вы подключаетесь по обычному ключу к серверу, не имеющему к Kerberos никакого отношения.

Если KDC недоступен или отвечает медленно, клиент терпеливо ждёт таймаута, и именно эти секунды вы наблюдаете.

В таком случае диагностика начинается с проверки текущих билетов, которые можно увидеть с помощью команды klist и трассировки GSSAPI:

klist
KRB5_TRACE=/dev/stderr ssh user@host

Если klist показывает пустой список, а трассировка полна попыток достучаться до несуществующего kdc.example.com, диагноз подтверждён.

Дополнительно можно посмотреть, что именно настроено:

ssh -G user@host | grep -i gssapi

Здесь решение будет достаточно простое отключить GSSAPI там, где он не нужен. Для этого в клиентском файле конфига ~/.ssh/config просто отключим GSSAPI:

Host *
    GSSAPIAuthentication no

Однако, если GSSAPI нужен только для конкретных корпоративных хостов, лучше не отключать его глобально, а сделать наоборот — включить точечно для отдельного домена:

Host *.corp.example.com
    GSSAPIAuthentication yes

Host *
    GSSAPIAuthentication no

Но здесь важно понимать, что в корпоративной среде, где SSH‑доступ построен на SSO с использованием Kerberos, отключать GSSAPI по понятным причинам нельзя.

Вместо этого нужно чинить доступность KDC и корректность параметров в файле krb5.conf.

Отключение GSSAPI здесь является лечением симптома, а не болезни.

Причина вторая: UseDNS yes на сервере

Ещё одной классической причиной задержек является использование параметра UseDNS в sshd_config.

Когда он включён (а в некоторых дистрибутивах это значение по умолчанию), демон sshd при каждом подключении делает обратный DNS‑запрос по IP‑адресу клиента.

Ему нужно получить PTR‑запись для логирования осмысленного имени хоста и, при необходимости, применения правила вида AllowUsers user@*.example.com.

Если резолвер медленный, перегруженный или вообще недоступен из сетевого сегмента сервера, каждый такой запрос упирается в таймаут — те самые 10–30 секунд.

Проверить текущее значение достаточно просто:

sudo sshd -T | grep -i usedns

Если вы видите usedns yes, то это первый подозреваемый.

Также дополнительно стоит посмотреть, что происходит в момент подключения, в логах демона:

sudo journalctl -u ssh -f

И проверить, резолвится ли адрес клиента с самого сервера:

dig -x <client_ip>

Наличие статуса NXDOMAIN и пустая секция ANSWER означают отсутствие записи.

Сам по себе такой ответ быстрый (3 мс), и sshd с параметром UseDNS yes на нём не зависнет надолго, так как он просто залогирует reverse mapping checking getaddrinfo for … failed и пойдёт дальше.

Но если резолвер, к которому обращается sshd, настроен на forward-only (без рекурсии в обратные зоны) или сам тормозит, то ответ придёт не за 3 мс, а за 10–30 секунд — и вот это уже причина задержки.

Для исправления отредактируйте файл /etc/ssh/sshd_config:

UseDNS no

После правки обязательно проверьте конфиг на синтаксис и перезагрузите демон без разрыва существующих сессий:

sudo sshd -t
sudo systemctl reload sshd

Но здесь есть важный нюанс, о котором часто забывают.

После применения UseDNS no перестают работать правила AllowUsers, DenyUsers, AllowGroups и DenyGroups, заданные по имени хоста, и остаются только IP‑адреса и шаблоны.

Если у вас в конфиге есть что‑то вроде AllowUsers admin@*.corp.example.com, то после отключения DNS это правило перестанет срабатывать, и вы можете случайно отрезать себе доступ.

Поэтому перед правкой всегда проверяйте наличие соответствующих правил:

sudo sshd -T | grep -E 'allowusers|denyusers|allowgroups|denygroups'

И если такие правила есть, то переписывайте их на IP‑подсети заранее, до reload.

Некоторые менее очевидные причины

Мы рассмотрели две наиболее распространённые причины, но не всё сводится к описанным параметрам. Если после правок выше подключение всё ещё медленное, то стоит проверить ещё несколько параметров, представленных далее.

CanonicalizeHostname и Match exec

Начнём с CanonicalizeHostname и Match exec в клиентском конфиге.

Если в ~/.ssh/config включены канонические имена или используются блоки Match exec с внешними командами, каждая такая команда выполняется при подключении.

Медленный скрипт внутри Match exec даёт ровно такой же эффект, как GSSAPI.

Всё это легко проверяется через ssh -G host, там видно итоговый конфиг после всех подстановок.

ProxyCommand/ProxyJump

Ещё один подозреваемый — это параметр ProxyCommand/ProxyJump с медленным первым хопом.

Если подключение идёт через бастион, задержка может быть на стороне jump‑хоста, особенно если он сам страдает от UseDNS или GSSAPI.

Проверяйте тайминги по цепочке: сначала напрямую на бастион, потом уже через него.

PAM‑модули с сетевыми вызовами

Отдельного внимания требуют PAM‑модули с сетевыми вызовами.

На этапе аутентификации sshd может ходить в pam_sss или pam_ldap, которые, в свою очередь, обращаются к внешнему каталогу.

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

Для выявления данной проблемы нужно смотреть логи в journalctl -u ssh и в /var/log/auth.log: там будут сообщения от PAM с указанием модуля.

Лимит MaxStartups

Если сервер принимает много одновременных подключений (например, из Ansible‑инвентаря, содержащего сотни хостов), sshd может начинать дропать часть попыток по достижении лимита MaxStartups.

Со стороны клиента это выглядит как случайные задержки и повторы.

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

sshd -T | grep maxstartups

И мониторингом количества сессий.

Здесь «проблема» не в синтаксисе, а в значении 10 в третьей позиции. Это означает, что sshd полностью прекращает принимать новые подключения, как только число неаутентифицированных сессий достигает 10.

MTU, фрагментация и «тихие» фаерволы

Ну и не стоит забывать о сетевой инфраструктуре, а именно об MTU, фрагментации и «тихих» фаерволах.

Если межсетевой экран настроен на DROP, а не REJECT, клиент будет ждать полного TCP‑таймаута вместо быстрого отказа.

Классическим признаком здесь является зависание ровно на строке Connection established ещё до обмена баннерами.

Проверяется через tcpdump на обеих сторонах: если SYN уходит, а SYN‑ACK не приходит, то вопрос к сетевой инфраструктуре.

Массовое лечение

Когда серверов десятки или сотни, руками править sshd_config — идея, мягко говоря, сомнительная.

Гораздо лучше использовать идемпотентный playbook с валидацией конфига перед перезагрузкой.

Минимальный playbook здесь будет иметь следующий вид:

- hosts: all
  become: true

  tasks:
    - name: Ensure UseDNS is disabled
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?UseDNS'
        line: 'UseDNS no'
        validate: '/usr/sbin/sshd -t -f %s'
      notify: reload sshd

    - name: Drop GSSAPI for non-corp hosts on clients
      ansible.builtin.blockinfile:
        path: /etc/ssh/ssh_config.d/99-fast.conf
        create: true
        block: |
          Host *
              GSSAPIAuthentication no

  handlers:
    - name: reload sshd
      ansible.builtin.service:
        name: sshd
        state: reloaded

Здесь ключевым параметром является параметр validate. Он прогоняет новый конфиг через sshd -t до применения, и если синтаксис некорректен, файл не будет записан.

Это спасает от классической ошибки, когда после правки sshd не стартует, и вы остаётесь без доступа к серверу.

Для применения изменений надо использовать reload, а не restart, так как иначе существующие сессии будут разорваны, что важно при работе в продуктивной среде.

После раскатки полезно замерить результат до и после, например с помощью представленного ниже способа:

time ssh -o BatchMode=yes -o ConnectTimeout=35 user@host true

Здесь разница в разы будет лучшим результатом, говорящим о том, что всё корректно настроено.

Чек‑лист и профилактика

В заключение давайте рассмотрим короткий чек-лист для дежурного, столкнувшегося с медленным SSH:

  • Запустить ssh -vvv с таймингами и найти шаг, на котором происходит задержка.

  • Проверить klist и ssh -G host | grep -i gssapi. Если GSSAPI включён и KDC недоступен, то отключить.

  • Проверить sshd -T | grep -i usedns на сервере и, если yes, то отключить, предварительно убедившись, что нет правил по hostname.

  • Посмотреть journalctl -u ssh на предмет задержек PAM.

  • Проверить, нет ли в клиентском конфиге Match exec или CanonicalizeHostname.

  • Если ничего не помогло, то tcpdump на обеих сторонах и проверка фаервола на DROP vs REJECT.

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

И регулярно проверять, не вернулись ли отключённые параметры после обновления пакетов, особенно если дистрибутив поставляет свои drop-in конфиги.

И главное — это соблюдать баланс, так как, отключая UseDNS и GSSAPI, вы выигрываете в скорости, но теряете часть возможностей.

Например, вы лишитесь возможности использования правил по hostname и корпоративного SSO.

Поэтому эти функции нужно включать там, где они действительно нужны, и там правильнее чинить инфраструктуру (доступность KDC, скорость резолвера), а не отключать их.

А насколько уверенно вы разбираетесь в других задачах администрирования Linux? Проверить себя и найти пробелы можно во вступительном тесте.

Делаем выводы

Медленный SSH — это не про какую-то «магию» или глючную сеть. Это конкретная стадия handshake, на которой клиент или сервер ждёт ответа от внешней системы.

Алгоритм действий в такой ситуации достаточно прост. Нам необходимо сначала получить тайминги в ssh -vvv, потом составить гипотезу о причине, после чего выполнить точечную правку с валидацией конфига.

Не «перезагрузить и посмотреть, что будет», а именно так, методично и воспроизводимо.

Для тренировки полезно поднять стенд из двух контейнеров с OpenSSH, включить на одном UseDNS yes с недоступным резолвером, а на другом GSSAPI с «мёртвым» KDC и пройти весь путь диагностики руками.

Один раз увидев паузу в логе своими глазами, вы больше никогда не перепутаете её с сетевой проблемой.

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

А познакомиться с отдельными темами — на бесплатных открытых уроках:

  • 17 ноября в 20:00. «/proc, /sys, /dev: как Linux превращает железо и процессы в файлы». Записаться

  • 1 декабря в 19:00. «Ansible: инфраструктура как код на практике». Записаться

  • 17 декабря в 20:00. «Процессы в Linux: глубокое погружение». Записаться

Ещё больше открытых уроков — в ноябрьском дайджесте OTUS