casablanque

casablanque

Инженер из Северной Каролины
Пикабушник
166 рейтинг 0 подписчиков 1 подписка 2 поста 2 в горячем
53

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

Самый недооцененный механизм безопасности 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. com
ssh 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 почти не помогает ответить на самые обычные вопросы.

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

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

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

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

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

И начинается дурдом с grep, awk, sed.

А мой коллега американец достает припасенную пачкау shell-скриптов и с умным видом делает вид, что (его цитата) shoveling shit)

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

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

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

Создать доверие - пожалуйста. Поддерживать его - уже без меня.

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

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

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

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

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

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

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 --all

OK  github. com

OK  gitlab. com

CHANGED  old. example. com

UNREACHABLE  backup. example. com

4 hosts • 2 OK • 1 changed • 1 unreachable

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

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

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

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

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

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

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

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

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

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

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

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

Смотреть

Устанавливайте, чекайте, ищите изъяны, ставьте звезды, открывайте пулл-реквесты, наводите порядок на своих хостах. Увидимся!

Показать полностью
21

Как я хотел сэкономить 15 минут на хоумлабе, а в итоге дебажил Cloudflare

Схема стандартная: мне нужно было выкатить наружу очередной сервис из своего хоумлаба (дашборд Grafana, внутреннее API, музыкальный сервер - неважно). Я открывал админку Cloudflare, создавал туннель, прописывал DNS-запись, настраивал политики Zero Trust Access, копировал ID туннеля, генерировал конфиг, устанавливал службу на сервер и со спокойной душой закрывал вкладку. Первые несколько раз меня это вообще не парило.

Примерно на пятом сервисе до меня дошло: я больше не настраиваю инфраструктуру. Я исполняю ритуал.

И я задался вопросом нафига я вообще это делаю руками? У Cloudflare есть отличный API. Всё, по чему я кликаю мышкой в красивом (не очень) интерфейсе, в конечном итоге превращается в обычный HTTP-запрос. Так почему весь этот адский воркфлоу нельзя упаковать в одну единственную команду?

Так родился проект cfzt.

Изначально это не планировалось как какой-то коммерческий продукт или крутой опенсорс. Мне просто хотелось перестать страдать фигнёй по выходным.

Идеальная цель выглядела так:

zt up grafana 3000

И всё. За этими четырьмя словами утилита сама создаёт туннель Cloudflare, настраивает правила ингресса, регистрирует DNS, вешает авторизацию через Zero Trust Access, ставит демона в систему и запоминает состояние, чтобы потом всё это можно было чисто удалить одной командой.

Команда выглядит крошечной. Объём работы под капотом - огромный. И, как выяснилось позже, это была самая простая часть.

Процесс от init до поднятия туннеля

Дело было не в Cloudflare

Самое забавное, что я вообще не пытался заменить Cloudflare. Скорее наоборот.

Cloudflare Tunnel - это одна из тех штук, которая при первом знакомстве кажется чистой магией. Твой сервер может сидеть за NAT, за жёстким провайдерским CGNAT, вообще где угодно, но он всё равно будет доступен из интернета без единого открытого входящего порта на роутере.

Добавь сверху zero trust - и бах! Каждый твой внутренний сервис по дефолту защищён нормальной авторизацией, привязан к identity-провайдеру и еще сертификат насыпают на сдачу.

База просто монументальная. Проблема была во всём том, что её окружало. Каждый новый чих требовал повторения одного и того же алгоритма:

- Создать туннель.

- Прописать DNS-запись.

- Настроить Ingress rule.

- Создать приложение в Access.

- Привязать к нему политику доступа.

- Сгенерировать конфигурационный файл.

- Установить системную службу на сервере.

- Запустить её.

Ни один шаг сам по себе не сложен. Они просто… нудные. И хуже всего была даже не потеря времени, а ментальная нагрузка. А я точно создал DNS? А access включить не забыл? Какой туннель держит этот домен? А если я его удалю, ничего не сломается?

В хоумлабе и так хватает хаоса, чтобы ещё держать в голове весь этот чек-лист деплоя.

И я подумал: а что, если бы публикация сервиса в сеть была такой же простой, как запуск Docker-контейнера?

Docker внутри устроен невероятно сложно, но его интерфейс скрывает весь этот ужас от пользователя. Ты пишешь команду - Docker разбирается со всем остальным. Это и стало главной фишкой cfzt: не плодить миллион галочек и настроек, а сделать стандартный путь банальным и скучным.

За одной строчкой скрывается куча работы с API, но пользователю до этого не должно быть никакого дела. Сложность должна оставаться внутри инструмента. Мне кажется, создателям современного софта для инфраструктуры стоит почаще выбирать именно такой подход.

Фича, которую никто не заметит

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

Инструменты автоматизации обожают ломаться на середине пути. То API Cloudflare выдаст rate limit, то сессия протухнет, то один запрос пройдёт, а следующий за ним упадет с ошибкой. В итоге у вас остаётся полурабочее нечто:

- Туннель вроде создался, но до него не достучаться.

- DNS-запись смотрит в никуда.

- В панели access висит приложение для сервиса, которого уже нет.

Убирать это дерьмо руками несложно, но выбешивает знатно.

Я хотел, чтобы cfzt работал иначе. Любой деплой - это транзакция. Если все шаги прошли успешно - сервис онлайн. Если споткнулись хоть на одном этапе - утилита автоматически сносит всё, что успела насоздавать до этого момента. Никаких осиротевших ресурсов и никакого гадания в веб-морде Cloudflare, что там надо подчистить.

Звучит очевидно? Да. На практике же пришлось детально логировать каждый чих к API, записывать ID каждого созданного ресурса и писать под них логику удаления. Зато теперь я могу запускать утилиту и не бояться, что она оставит после себя кучу мусора. Лучший откат - это тот, о котором тебе не пришлось думать.

Но откат шага в своей проге это не то же, что откат на стороне cloudflare...

А потом я почитал логи...

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

Как-то вечером после очередного деплоя я заглянул в логи и заметил странное: cloudflared (официальный демон Cloudflare) внезапно переключился с протокола QUIC на старый добрый HTTP/2.

Вообще, это штатное поведение. QUIC работает поверх UDP, и если с UDP что-то идёт не так (роутер ребутнулся, Wi-Fi мигнул, провайдер решил подрезать пакеты) - туннель автоматически падает в фоллбэк на HTTP/2 поверх TCP, чтобы связь вообще не оборвалась.

Странно было другое. Он никогда не переключался обратно.

Подождите... Это что, навсегда?

Сначала я подумал, что чего-то не понимаю в работе туннелей. Ну, наверное, он проверяет сеть раз в пару минут? Или есть скрытая настройка?

Я сидел и караулил логи. Прошёл час, два. Туннель работал идеально, трафик шёл, но намертво сидел на TCP.

Я перерыл официальную документацию - тишина. Пошёл штурмовать гитхаб. И бинго! Нашёл старый тикет, где чувак жаловался ровно на то же самое. Логика у демона Cloudflare простая: если туннель один раз упал в HTTP/2, он больше никогда не попытается вернуться на быстрый QUIC. Только если полностью перезапустить сам процесс ручками. И баг этот висел в issues в официальном репозитории уже очень давно.

Выбора было два: либо смириться и ждать, пока пацаны из Cloudflare это починят (спойлер: можно не дождаться), либо костылить обходной путь. Я выбрал второй вариант.

Пишем вочдог вместо патча

Лезть в исходники самого cloudflared, форкать его и пересобирать сетевой стек ради одного бага - так себе идея. Поэтому я посмотрел на задачу проще: какие данные у меня реально есть на руках?

Ответ оказался банальным: при переходе с QUIC на HTTP/2 демон пишет в stderr конкретную строчку. Всё, это и есть наш API! Зачем анализировать пакеты или пинговать порты, если можно просто читать логи процесса?

План получился такой: cfzt запускает туннель и начинает грепать его логи на лету. Как только ловим строчку о фоллбэке, запускается таймер. Мы не рубим процесс сразу - вдруг сеть ещё штормит. Выжидаем паузу, и если туннель всё ещё на HTTP/2 - тихонько дергаем службу.

При перезапуске cloudflared снова пытается инициировать модный QUIC. Если UDP всё ещё лежит - ок, он опять упадёт в HTTP/2. Наш вочдог это увидит и увеличит таймер ожидания: 10 минут, 20, 40... В итоге шаг баг-оффа упирается в 1 час. Мы не боремся с сетью, мы просто даём туннелю шанс очухаться, когда шторм пройдёт.

Выглядит этот «инженерный фикс» до смешного просто. Вот кусок кода на Go, который закрыл эту проблему раз и навсегда:

// Строка, которую выплёвывает Cloudflare, когда сдается и уходит на TCP

const FallbackSignal = "Failed to connect to the edge over QUIC, falling back to HTTP2"

func watchTunnelLogs(scanner *bufio.Scanner, restartTrigger chan<- bool) {

backoff := 5 * time.Minute

for scanner.Scan() {

line := scanner.Text()

if strings.Contains(line, FallbackSignal) {

log.Warn("Обнаружен откат с QUIC на HTTP/2. Запускаем вочдог.")

// Ждём, пока сеть стабилизируется

time.Sleep(backoff)

// Триггерим мягкий перезапуск процесса

restartTrigger <- true

// Экспоненциальный шаг ожидания (максимум до 1 часа)

backoff = min(backoff * 2, 1 * time.Hour)

}

}

}

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

Чему меня научил этот проект

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

И в этом вся прелесть создания своих инструментов для автоматизации (переизобретенных велосипедов, если хотите). Ты берёшь мелкую бытовую проблему, думаешь: да ладно, сейчас набросаю тонкую CLI обертку за пару часов в субботу. Но как только ты пытаешься сделать интерфейс по-настоящему простым для пользователя, тебе приходится впитать в свой код всю ту сложность, которую разработчики платформы оставили за бортом.

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

Интерфейс остается крошечным. Инструмент делает всю грязную работу. Ритуал наконец-то разрушен.

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

$ zt up grafana 3000

✔ Creating Cloudflare Tunnel... Done.

✔ Provisioning DNS records... Done.

✔ Setting up Zero Trust Access policies... Done.

✔ Launching background service with QUIC... Done.

Service is live at https: //grafana. yourdomain. com

И спокойно иду пить кофе. Потому что инфраструктура, черт возьми, должна быть скучной.

Ссылка на проект

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

GitHub: репозиторий

Буду рад фидбеку. Ломайте, тестируйте, закидывайте ишью или предлагайте фичи. Если утилита сэкономит вам пару минут на выходных - значит, все это было не зря. Только не просите меня добавлять в CLI новые кнопки. Я написал его как раз для того, чтобы их больше никогда не видеть.

Всем удачи!

Показать полностью 1
Отличная работа, все прочитано!

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества