Самый недооцененный механизм безопасности SSH или почему known_hosts — это не кэш

от автора

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

The authenticity of host 'example.com' can't be established.ED25519 key fingerprint is SHA256:...Are you sure you want to continue connecting (yes/no)?

И почти все бездумно вводят одинаковый ответ

yes

Эта модель называется TOFU или Trust On First Use — доверие при первом использовании.

Через несколько месяцев или даже лет по тому же адресу может прилететь уже совсем другое сообщение.

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

И тут сценарий в большинстве случаев тоже простой и бестолковый.

ssh-keygen -R example.comssh example.com# yes

Вот и все, можно снова не беспокоиться, проблема решена.

Или нет?


До недавнего времени я относился к файлу known_hosts примерно так же. Ну какой-то там служебный кэш SSH. Если что-то пошло не так — удалил запись, подключился заново, живем дальше.

Но однажды я поймал себя на простой как два рубля мысли.

known_hosts — это вообще не кэш.

Это база доверенных идентичностей серверов.

Когда SSH спрашивает:

Вы уверены, что хотите доверять этому серверу?

он сохраняет ваш ответ именно в known_hosts. И потом годами использует этот файл как единственный источник истины, чтобы понимать, разговариваете вы с тем же сервером или с кем-то совершенно другим.

Получается интересная ситуация.

Один из важнейших механизмов безопасности SSH хранится в обычном текстовом файле.

При этом вокруг него почти нет инструментов.

Где заканчивается OpenSSH

Давайте посмотрим, что OpenSSH умеет делать со своей собственной базой доверия.

  • автоматически добавить новую запись при первом подключении

  • удалить запись (ssh-keygen -R)

  • найти запись (ssh-keygen -F)

  • захэшировать файл (ssh-keygen -H)

И, по большому счёту, на этом его полномочия всё.

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

И неожиданно оказывается, что OpenSSH почти не помогает ответить на самые обычные вопросы.

  • Какие серверы уже давно не существуют?

  • Какие ключи изменились?

  • Есть ли дубликаты?

  • Какие записи используют устаревшие алгоритмы?

  • Кому я доверяю прямо сейчас?

Обычно ответ выглядит так.

grepawksed

Или пачка shell-скриптов.

Меня это искренне удивило.

SSH существует уже больше тридцати лет. Это один из самых распространённых сетевых протоколов в мире.

Но его модель доверия практически заканчивается сразу после первого yes.

Создать доверие — пожалуйста.

Поддерживать его — уже без меня.

А как вообще SSH получает host key?

Стало интересно, как именно клиент получает публичный ключ сервера.

Мне всегда казалось, что для этого нужно установить полноценную SSH-сессию, договориться о шифровании, пройти аутентификацию и только потом получить нужную информацию.

Оказалось, все гораздо прозаичнее.

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

Client                                 Server  |                                        |  | -------- TCP connect ----------------> |  |                                        |  | <------- Version banner -------------  |  | -------- Version banner -------------> |  |                                        |  | <------- SSH_MSG_KEXINIT ------------  |  | -------- SSH_MSG_KEXINIT ----------->  |  |                                        |  | -------- SSH_MSG_KEX_ECDH_INIT ----->  |  |                                        |  | <------ SSH_MSG_KEX_ECDH_REPLY ------  |  |                                        |

Сначала стороны просто обмениваются строками версии вроде:

SSH-2.0-OpenSSH_10.0

Затем договариваются о поддерживаемых алгоритмах.

И наконец клиент отправляет SSH_MSG_KEX_ECDH_INIT.

Самое интересное происходит в ответном сообщении.

SSH_MSG_KEX_ECDH_REPLY содержит:

  • host key сервера

  • временный публичный ключ сервера

  • криптографическую подпись

Именно здесь клиент вычисляет фингерпринт, сравнивает его с known_hosts и принимает решение — продолжать соединение или нет.

Что меня удивило больше всего — все это происходит ещё до аутентификации пользователя.

Никаких паролей. Никаких приватных ключей. Никакого shell. Никакой открытой SSH-сессии.

И это абсолютно логично.

Host key существует именно для того, чтобы клиент убедился, с кем он разговаривает, прежде чем отправлять хоть какие-либо секреты.

Получается, что если цель — просто проверить идентичность сервера, то после получения SSH_MSG_KEX_ECDH_REPLY соединение можно сразу закрывать.

Всё необходимое уже получено.

От исследования к инструменту

Когда я это понял, стало очевидно еще кое-что.

Чтобы проверить host key сервера, как я уже упомянул выше, не нужен полноценный SSH-клиент. Ни шелл, ни аут. Не нужна большая часть SSH вообще.

Нужен лишь небольшой кусок протокола.

Именно из этого наблюдения постепенно вырос небольшой CLI проект, который я назвал khm (known hosts manager).

Изначально он вообще писался для себя.

Мне хотелось иметь возможность быстро ответить на вопросы, на которые OpenSSH почему-то не отвечает.

Например:

  • изменились ли ключи у уже известных серверов

  • есть ли мусор и дубликаты в known_hosts

  • чем отличаются два файла после миграции инфраструктуры или ротации ключей

  • что вообще я имею на сегодняшний день

Постепенно стало понятно, что проблема гораздо шире, чем казалось сначала.

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

Хотя на самом деле именно он определяет, каким серверам ваш компьютер доверяет прямо сейчас.

Например, именно такого вывода мне всегда не хватало в OpenSSH.

$ khm verify --allOK           github.comOK           gitlab.comCHANGED      old.example.comUNREACHABLE  backup.example.com4 hosts • 2 OK • 1 changed • 1 unreachable

Не список ключей. Не тупой, подверженный человеческому фактору, вывод grep, а простой ответ на простой вопрос:

Всё ли ещё соответствует тому, чему я доверял раньше?

Вместо заключения

Пока я работал над этим проектом, самым неожиданным открытием оказался вовсе не SSH-протокол.

Меня удивило отношение к known_hosts. Мое в том числе.

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

Но если посмотреть внимательнее, это единственная локальная база доверенных идентичностей серверов, которой пользуется OpenSSH.

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

Именно поэтому появился khm.

Не как ещё один SSH-клиент. И не как замена OpenSSH.

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


Исходный код проекта, бин сборки и документация доступны в репо:

https://github.com/casablanque-code/khm

ссылка на оригинал статьи https://habr.com/ru/articles/1056282/