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).
Что действительно подтверждают 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).
Почему централизованное 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).
Учебный стенд «Вектор»: 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-атаку здесь не воспроизводил. Практический результат стенда конкретный: одна централизованная правка не охватила весь набор запусков, а повторная матрица проверок показала оставшиеся исключения. Такой протокол я и хочу видеть после внедрения исправления.
Как я собираю аудит всего парка
Единица учёта у меня такая: машина или контейнер, локальная учётная запись, путь к бинарнику, фактические аргументы и назначение. Список назначений собираю из инвентаризации, заданий резервного копирования, 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).
Как закрыть уязвимость и не остановить автоматизацию
Мой первый приоритет — обновить реально используемые клиенты из поддерживаемого репозитория поставщика. Для конкретной 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 пересобираю образы и проверяю новый запуск задания: обновлённый хост с прежним контейнером оставляет прежний клиент внутри.
Что проверять перед закрытием задачи
Я веду два независимых признака: исправлен ли бинарник и включена ли DNS-проверка в конкретном запуске. enabled на исправленном пакете не означает эту CVE; disabled на старом пакете означает временное снижение риска, а не завершённое обновление. Неизвестную ревизию, недоступный ноутбук или ошибку запуска оставляю отдельной строкой с владельцем и сроком. Иначе процент охвата получится красивым за счёт тех машин, до которых аудит не добрался.
После изменений проверяю реальное подключение к доверенному серверу и отрицательный сценарий с другим ключом на изолированном тестовом адресе. При отключённом VerifyHostKeyDNS, заранее закреплённом ключе и StrictHostKeyChecking=yes клиент должен отказать до пользовательской аутентификации. Это проверка рабочей политики; она не доказывает отсутствие CVE, потому что обычная смена ключа не воспроизводит специфическую ошибку памяти. Исправление подтверждаю бюллетенем поставщика и ревизией реально запускаемого пакета.
Возвращать SSHFP в автоматическое доверие я готов после обновления, проверки DNSSEC и назначения ответственного за ротацию host keys. Для парка с уже работающим управлением конфигурациями часто выбираю централизованную доставку проверенных ключей; при подходящем масштабе — SSH CA. Здесь нет универсального победителя: поддержка DNSSEC тоже требует дисциплины. А вот искать идеальный DNS-дизайн до обновления административных ноутбуков и CI я бы не стал. Сначала закрываем подтверждённый обход, затем улучшаем архитектуру.
- В отчёте есть устройство, локальный пользователь, бинарник, полная версия пакета, аргументы и назначение.
- Каждое значение yes, true или ask сопоставлено со статусом исправления; unknown не исключены из охвата.
- Проверены пользовательские Include, самостоятельные CI-конфиги, контейнерные образы и jump hosts.
- После изменения политики задания выполняются, а неподтверждённый ключ не принимается автоматически.
Частые вопросы
Достаточно заменить VerifyHostKeyDNS yes на ask?
Нет. На неисправленном клиенте оба значения включают затронутый путь. До обновления используйте эффективное no и сохраняйте проверку серверного ключа.
Почему ssh -V показывает 9.6p1 после исправления?
Поставщик мог перенести патч в свою ветку. Проверяйте полную ревизию пакета по бюллетеню вашей ОС, а также путь к реально запускаемому бинарнику.
Можно ли подтвердить защищённость через ssh -G?
Команда показывает эффективную конфигурацию конкретного запуска. Наличие исправления в бинарнике и правильность цепочки DNSSEC проверяются отдельно.
Если нужно проверить SSH-клиенты вашей инфраструктуры, я помогу определить охват и найти настройки, которые пропускает обычная инвентаризация. Команда «АйТи-Фреш» проведёт аудит, обновит клиенты и проверит административные подключения и автоматизацию после изменений.
Бесплатная консультация →

