АйТи Фреш
Главная / Статьи / Безопасность
Безопасность

VerifyHostKeyDNS включён ради SSHFP: почему клиент принимает MITM и как проверить весь парк

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~9 мин чтения
VerifyHostKeyDNS включён ради SSHFP: почему клиент принимает MITM и как проверить весь парк

SSHFP совпадает, DNSSEC работает, а доверять соединению всё равно нельзя: на неисправленном SSH-клиенте проверка может быть обойдена. Я — Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш». Покажу, где ломается защита, почему централизованная настройка переживает попытки её отключить и как собрать проверяемый результат по рабочим станциям, бастионам и автоматизации.

Ошибка находится в клиенте, до проверки SSHFP

Сначала определимся с границами проблемы. CVE-2025-26465 затрагивает клиент ssh в upstream OpenSSH 6.8p1–9.9p1 включительно, если включён VerifyHostKeyDNS. Исправление вышло 18 февраля 2025 года в OpenSSH 9.9p2. По умолчанию опция выключена. Поэтому начинать расследование со сканирования серверного порта 22 я считаю ошибкой: важен исполняемый файл на машине, которая устанавливает соединение. Сервер тоже попадает в проверку, когда с него запускают исходящий ssh. [Бюллетень OpenSSH](https://www.openssh.org/security.html).

Причина неприятно приземлённая. Функция проверки ключа могла вернуть ошибку выделения памяти SSH_ERR_ALLOC_FAIL, то есть −2. Вызывающий код признавал отказом только −1 и продолжал работу как при успехе. Проблемная операция стояла раньше проверки DNS и обычной проверки известных ключей. Отсюда вывод: при успешном срабатывании этой ошибки не спасают ни корректная SSHFP, ни DNSSEC, ни заранее записанный ключ в known_hosts, ни StrictHostKeyChecking=yes. Это следует из порядка вызовов и самого исправления. [Патч OpenSSH](https://github.com/openssh/openssh-portable/commit/0832aac79517611dd4de93ad0a83577994d9c907).

Риск не надо превращать в страшилку про любой чужой Wi-Fi. Нужен активный атакующий на пути соединения и специально вызванный отказ выделения памяти в клиенте. Простая предъявленная сервером другая пара ключей обхода не гарантирует. Qualys продемонстрировали атаку на OpenSSH 9.6p1 из Ubuntu 24.04 с ограничением RLIMIT_DATA до 256 МБ. Это условие их эксперимента, а не порог оперативной памяти для вашего ноутбука. Для расходования памяти исследователи использовали отдельную CVE-2025-26466; её диапазон версий нельзя переносить на CVE-2025-26465. [Технический отчёт Qualys](https://www.qualys.com/2025/02/18/openssh-mitm-dos.txt).

Особенно коварно значение ask: оно тоже включает уязвимый путь. Подтверждать чужой ключ пользователю при успешном обходе не требуется, наличие SSHFP вообще не обязательно. Последствия зависят от дальнейшей работы: подставной сервер может получить отправленный пароль, команды и файлы. При обычной аутентификации открытым ключом приватный ключ не передаётся, а подпись связана с конкретным сеансом. Обещание «MITM автоматически украдёт ваш id_ed25519» технически неверно. [Выводы Qualys](https://blog.qualys.com/vulnerabilities-threat-research/2025/02/18/qualys-tru-discovers-two-vulnerabilities-in-openssh-cve-2025-26465-cve-2025-26466), [RFC 4252, раздел 7](https://www.rfc-editor.org/rfc/rfc4252.html#section-7).

yes и ask опасны в сочетании с неисправленным клиентом. Само наличие этих значений на обновлённом OpenSSH ещё не означает уязвимость.
Цифры и версии: Ошибка находится в клиенте, до проверки SSHFP — схема
Цифры и версии: Ошибка находится в клиенте, до проверки SSHFP

Что действительно подтверждают SSHFP и DNSSEC

SSHFP связывает имя узла с отпечатком его серверного ключа. При штатной работе VerifyHostKeyDNS=yes позволяет автоматически доверять совпавшему защищённому DNS-отпечатку. Незащищённый ответ обрабатывается как ask. Сам ask сообщает о совпадении, а принятие нового ключа зависит от StrictHostKeyChecking: это не обещание задавать дополнительный вопрос при каждом подключении. Я разделяю две проверки: верна ли опубликованная запись и действительно ли клиент получил аутентифицированный результат. [Документация VerifyHostKeyDNS](https://man.openbsd.org/ssh_config#VerifyHostKeyDNS).

Для сверки беру публичный host key через доверенный доступ к серверу: консоль гипервизора, систему управления или уже проверенный административный канал. Генерирую запись и сопоставляю её с DNS для фактического имени назначения. Если сервер предлагает несколько алгоритмов ключей, проверяю соответствующие записи: совпадение отпечатка одного ключа ничего не говорит о другом. Команда ниже читает только публичный файл; секретный host key переносить на рабочую станцию не нужно. [ssh-keygen(1)](https://man.openbsd.org/ssh-keygen#r). ```sh # На сервере через доверенный канал ssh-keygen -r db01.vector.example \ -f /etc/ssh/ssh_host_ed25519_key.pub # На клиенте dig +dnssec db01.vector.example SSHFP ```

У dig +dnssec есть ловушка: команда запрашивает DNSSEC-данные, но сама по себе не доказывает их успешную проверку. Флаг ad имеет смысл при доверии к валидирующему резолверу и каналу до него. Я проверяю эту цепочку отдельно, особенно после смены VPN и корпоративных DNS. Красивый вывод dig с правильным отпечатком подтверждает только часть схемы. Пока неизвестен статус исправления клиента, тратить первый час аварийных работ на переподписание зоны бессмысленно: клиентский дефект этим не устраняется. [Документация dig](https://bind9.readthedocs.io/en/latest/manpages.html#dig-dns-lookup-utility), [RFC 4255, раздел 4](https://www.rfc-editor.org/rfc/inline-errata/rfc4255.html).

SSHFP полезна, когда работает вся цепочка доверия. Удалять корректные записи ради устранения CVE-2025-26465 не требуется.
VerifyHostKeyDNS включён ради SSHFP: почему клиент принимает MITM и как проверить весь парк — схема

Почему централизованное no может не сработать

Для VerifyHostKeyDNS действует первое полученное значение. Обычно источники читаются так: аргументы командной строки, пользовательская конфигурация, системная конфигурация. Значит, добавленный в конец 99-security.conf параметр no может вообще ничего не поменять. Более того, системная настройка не перебивает уже выбранную пользовательскую. Я всегда прошу показать результат разбора конфигурации, а не только успешный отчёт Ansible о доставленном файле. [Разбор конфигурации в исходниках OpenSSH](https://github.com/openssh/openssh-portable/blob/master/readconf.c).

Вот минимальный пример. Кажется, что для db-prod задано исключение. Фактически первый Host * уже установил yes, поэтому более поздний no не победит. Для такого исключения специфический блок должен стоять раньше общего. При аварийном отключении я предпочитаю удалить ненужные включения из их источников: иначе следующая правка порядка Include снова поменяет результат. ```sshconfig Host * VerifyHostKeyDNS yes Host db-prod VerifyHostKeyDNS no ```

Отдельно проверяю Include и Match. Файлы по маске Include читаются в лексическом порядке, а сама директива может находиться внутри условного блока. Каталог ssh_config.d не обладает самостоятельной магией: его файлы должны реально подключаться. Пользовательский Include способен вести в дополнительный управляемый каталог; CI может запускать ssh с собственным -F. Поэтому поиск одной строки в /etc/ssh/ssh_config — полезный первый шаг, но слабое основание для закрытия задачи. [Описание Include](https://man.openbsd.org/ssh_config#Include).

Файл с правильной настройкой и эффективная настройка конкретного запуска — разные результаты проверки.
Обратите внимание: Почему централизованное no может не сработать — схема
Обратите внимание: Почему централизованное no может не сработать

Учебный стенд «Вектор»: 24 проверки вместо одного grep

Для статьи я проверил конфигурационный стенд, моделирующий ООО «Вектор» на 120 рабочих мест. Название, масштаб организации и роли условные; приведённые ниже результаты разбора конфигураций измерены. Использован один Linux-клиент OpenSSH_9.6p1 Ubuntu-3ubuntu13.19, пакет openssh-client 1:9.6p1-3ubuntu13.19. Он уже содержит исправление. Шесть файлов моделировали два администраторских профиля, исходящие подключения с бастиона, два задания CI и профиль администратора WSL. WSL и шесть отдельных машин для этого теста не разворачивались.

Каждый профиль проверялся для четырёх алиасов: db-prod, backup-prod, web-prod и bastion-prod. Итого 24 сочетания. Общий файл содержал Host * с yes. admin-a подключал пользовательский Include с yes; admin-b задавал ask только для db-prod и backup-prod; jump-ops наследовал общий файл. ci-release использовал самостоятельный конфиг с ask, ci-backup — с no, wsl-admin — пользовательский no. В лаборатории я задавал файлы через -F, а общий слой подключал явно: так порядок чтения воспроизводится без изменения настоящего /etc/ssh.

Первый прогон дал 10 значений true, 6 ask и 8 false. Это важная мелочь: на проверенном клиенте ssh -G печатает true для настроенного yes, а для no — false. Затем я дописал no в конец общего файла. Результат не изменился. Перенос no в начало общего слоя помог только части профилей: остались 4 true и 6 ask. Пользовательские настройки и самостоятельный конфиг CI продолжали действовать. ```sh ssh -G -F ./admin-b.conf db-prod ssh -G -F ./admin-b.conf web-prod # До исправления: verifyhostkeydns ask и true соответственно ```

Закончился тест исправлением исходных пользовательских, общих и CI-файлов: все 24 сочетания стали false. Отдельный прогон с первым командным -o VerifyHostKeyDNS=no тоже дал 24 false. Я проверял именно конфигурацию, SSH-сессии и MITM-атаку здесь не воспроизводил. Практический результат стенда конкретный: одна централизованная правка не охватила весь набор запусков, а повторная матрица проверок показала оставшиеся исключения. Такой протокол я и хочу видеть после внедрения исправления.

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

Как я собираю аудит всего парка

Единица учёта у меня такая: машина или контейнер, локальная учётная запись, путь к бинарнику, фактические аргументы и назначение. Список назначений собираю из инвентаризации, заданий резервного копирования, Git, Ansible, cron, systemd и CI. Проверяю рабочие алиасы, FQDN и IP, если ими действительно пользуются. Один ssh -G example.org ничего не доказывает о Host db-prod. Запуск от root также не заменяет проверку под пользователем deploy.

На каждом таком профиле выполняю ssh -G с настоящими -l, -p, -F и -o. Для поиска источника значения использую ssh -vvv -G: стандартный вывод сохраняю отдельно от журнала чтения файлов. В обычный аудит нельзя самовольно добавлять -F /dev/null: он исключит штатные конфигурации, включая системную, и нарисует благополучный результат вместо проверки действующего запуска. [Параметры ssh(1)](https://man.openbsd.org/ssh). ```sh ssh -vvv -G -l deploy -p 2222 db-prod \ > effective.conf 2> config-trace.log rg '^(hostname|user|port|verifyhostkeydns|proxyjump|proxycommand|stricthostkeychecking) ' effective.conf ```

Ниже шаблон Bash для одного локального пользователя, бинарника и набора аргументов. В aliases.txt — доверенный список назначений, по одному на строку. ssh_args нужно заменить аргументами реального задания. Шаблон сохраняет ошибки и отличает enabled от unknown: ошибка чтения, неподдерживаемый -G или отсутствие строки не должны превращаться в «безопасно». При централизованном запуске я также задаю тайм-аут задания и сохраняю сырой вывод для разбора спорных результатов. ```bash ssh_bin=/usr/bin/ssh ssh_args=(-l deploy) while IFS= read -r target || [[ -n "$target" ]]; do [[ -z "$target" || "$target" == \#* ]] && continue if cfg=$("$ssh_bin" -G "${ssh_args[@]}" -- "$target" 2>>audit-errors.log); then value=$(awk '$1=="verifyhostkeydns" {print $2}' <<< "$cfg") case "$value" in yes|true|ask) state=enabled ;; no|false) state=disabled ;; *) state=unknown ;; esac else value=- state=unknown fi printf '%s\t%s\t%s\n' "$target" "$value" "$state" done < aliases.txt ```

Охват важнее длины скрипта. Системный OpenSSH Windows, клиент Git for Windows, WSL и контейнерный ssh учитываю отдельно; вывод одного нельзя переносить на остальные. Для SSH-библиотек приложений проверяю собственные бюллетени и настройки. При ProxyJump отдельно разбираю назначение и каждый бастион: опции конечного узла обычно не распространяются на jump host. Если человек сначала входит на бастион и уже там запускает ssh, в инвентаризации появляется ещё один исходящий клиент. [Документация ProxyJump](https://man.openbsd.org/ssh_config#ProxyJump).

ssh -G не устанавливает SSH-сессию, но может выполнять локальный Match exec и обращаться к DNS при каноникализации. Чужую пользовательскую конфигурацию проверяйте с правами её владельца, не от root.

Как закрыть уязвимость и не остановить автоматизацию

Мой первый приоритет — обновить реально используемые клиенты из поддерживаемого репозитория поставщика. Для конкретной CVE upstream-граница исправления — 9.9p2, но в сентябре 2026 года я не предлагаю закреплять именно этот старый релиз. Нужен актуальный поддерживаемый пакет. У дистрибутивов исправления переносят в прежние ветки: например, Ubuntu 24.04 получила исправление уже в 1:9.6p1-3ubuntu13.8. Поэтому строка OpenSSH_9.6p1 сама по себе не доказывает уязвимость. [USN-7270-1](https://ubuntu.com/security/notices/USN-7270-1). ```sh command -v ssh ssh -V dpkg-query -W -f='${Package} ${Version}\n' openssh-client # Для RPM-систем rpm -q openssh-clients ```

Пока обновление готовится, отключаю VerifyHostKeyDNS в реально применяемых источниках и повторяю аудит. Отключение закрывает условие именно этой CVE; качество обычной проверки ключей всё равно нужно сохранить. Для разового прямого подключения можно поставить параметры в начало аргументов, предварительно обеспечив доверенную запись сервера в known_hosts. Для цепочки ProxyJump отдельно обеспечивается защита каждого перехода. [Рекомендация Red Hat](https://access.redhat.com/security/cve/cve-2025-26465). ```sh ssh -o VerifyHostKeyDNS=no \ -o StrictHostKeyChecking=yes deploy@db01.vector.example ```

Самая неприятная эксплуатационная развилка возникает там, где раньше доверяли только SSHFP: после отключения DNS-проверки StrictHostKeyChecking=yes закономерно остановит соединение с неизвестным ключом. Я сначала доставляю проверенные ключи или настраиваю доверие к SSH CA, затем переключаю задания. Выгрузка через ssh-keyscan сама по себе не подтверждает подлинность сервера. Если без независимой сверки положить её в known_hosts, можно закрепить ключ того самого посредника. [Предупреждение ssh-keyscan(1)](https://man.openbsd.org/ssh-keyscan).

Обновление клиента не требует перезапуска удалённого sshd ради этой ошибки. Зато долгоживущие клиентские процессы и ControlMaster заслуживают внимания: замена файла на диске не заменяет уже работающий процесс. Для контрольного подключения я использую новый процесс без переиспользования master-соединения, например с -S none. В CI пересобираю образы и проверяю новый запуск задания: обновлённый хост с прежним контейнером оставляет прежний клиент внутри.

Не меняйте yes на ask в качестве исправления. Временная мера — эффективное no; окончательная — подтверждённое исправление используемого клиента.
Цифры и версии: Как закрыть уязвимость и не остановить автоматизацию — схема
Цифры и версии: Как закрыть уязвимость и не остановить автоматизацию

Что проверять перед закрытием задачи

Я веду два независимых признака: исправлен ли бинарник и включена ли DNS-проверка в конкретном запуске. enabled на исправленном пакете не означает эту CVE; disabled на старом пакете означает временное снижение риска, а не завершённое обновление. Неизвестную ревизию, недоступный ноутбук или ошибку запуска оставляю отдельной строкой с владельцем и сроком. Иначе процент охвата получится красивым за счёт тех машин, до которых аудит не добрался.

После изменений проверяю реальное подключение к доверенному серверу и отрицательный сценарий с другим ключом на изолированном тестовом адресе. При отключённом VerifyHostKeyDNS, заранее закреплённом ключе и StrictHostKeyChecking=yes клиент должен отказать до пользовательской аутентификации. Это проверка рабочей политики; она не доказывает отсутствие CVE, потому что обычная смена ключа не воспроизводит специфическую ошибку памяти. Исправление подтверждаю бюллетенем поставщика и ревизией реально запускаемого пакета.

Возвращать SSHFP в автоматическое доверие я готов после обновления, проверки DNSSEC и назначения ответственного за ротацию host keys. Для парка с уже работающим управлением конфигурациями часто выбираю централизованную доставку проверенных ключей; при подходящем масштабе — SSH CA. Здесь нет универсального победителя: поддержка DNSSEC тоже требует дисциплины. А вот искать идеальный DNS-дизайн до обновления административных ноутбуков и CI я бы не стал. Сначала закрываем подтверждённый обход, затем улучшаем архитектуру.

Задачу закрывает сочетание подтверждённого исправления, проверенной конфигурации и полного охвата согласованного перечня запусков. Одного зелёного сканирования серверов недостаточно.

Частые вопросы

Достаточно заменить VerifyHostKeyDNS yes на ask?

Нет. На неисправленном клиенте оба значения включают затронутый путь. До обновления используйте эффективное no и сохраняйте проверку серверного ключа.

Почему ssh -V показывает 9.6p1 после исправления?

Поставщик мог перенести патч в свою ветку. Проверяйте полную ревизию пакета по бюллетеню вашей ОС, а также путь к реально запускаемому бинарнику.

Можно ли подтвердить защищённость через ssh -G?

Команда показывает эффективную конфигурацию конкретного запуска. Наличие исправления в бинарнике и правильность цепочки DNSSEC проверяются отдельно.

Проверим SSH-клиенты вашего парка
Если нужно проверить SSH-клиенты вашей инфраструктуры, я помогу определить охват и найти настройки, которые пропускает обычная инвентаризация. Команда «АйТи-Фреш» проведёт аудит, обновит клиенты и проверит административные подключения и автоматизацию после изменений.
Бесплатная консультация →
© ООО «АйТи-Фреш» · Москва · Все статьи