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

Две границы, два симптома

Сбой на краю выглядит как страница состояния HTTP, приходящая туда, где должен быть баннер SSH. Соединение устанавливается на уровне TCP, а первые байты — это текст, принадлежащий веб-серверу. Это несовпадение протокола, а не потерянный пакет. Он также говорит вам, что край намеренно обслуживает для этого имени что-то другое.

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

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

Заголовок PROXY — это сообщение, а не переключатель

В модуле потока proxy_protocol on велит NGINX записать заголовок протокола PROXY в сторону вышестоящей системы перед полезной нагрузкой. Это не включает проксирование TCP и не имеет никакого отношения к шифрованию SSH. Это записка, адресованная получателю. Если получатель не может её прочитать, она становится мусором в начале потока.

Принимающий сервис должен осознанно потреблять этот заголовок. Обычный sshd ожидает обмен идентификацией SSH как первые байты соединения, поэтому неожиданная преамбула ломает рукопожатие так, что это выглядит как повреждённый сервер.

stream {
    server {
        listen 2222;
        proxy_pass 10.77.0.20:22;
    }
}

Этот фрагмент — весь работающий релей для бэкенда, доступного через VPN. Директива PROXY не появляется, потому что получатель не говорит на этом протоколе, и это отсутствие — решение, а не упущение.

Разделяйте и оба направления. Установка proxy_protocol в строке listen означает, что NGINX ожидает, что его собственный клиент отправит заголовок первым; включение этого для обычного клиента SSH даёт ровно обратное той проблеме, которую люди обычно пытаются решить.

Чего на самом деле достигает публичное имя хоста

Страница HTTP 400 в сеансе SSH — это доказательство о крае. Что-то на пути говорит по HTTP, и никакая работа с ключом или учётной записью на бэкенде этого не изменит. Перестаньте менять бэкенд, пока край не будет улажен.

Обычная проксированная запись Cloudflare передаёт поддерживаемый трафик HTTP и HTTPS на заданных портах. Включение прокси для имени хоста не создаёт общий релей TCP, а перенос SSH на порт 443 не делает его HTTPS.

Есть два работоспособных направления. Запись только DNS может указывать на ваш собственный релей TCP, что оставляет решение о раскрытии в ваших руках. Либо используйте продукт, который явно документирует передачу SSH, с собственной конфигурацией источника, клиента и доступа.

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

Сохраняйте идентичность SSH стабильной с помощью псевдонима

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

Host git.example.com
    HostName ssh-entry.example.net
    Port 2222
    User git
    HostKeyAlias git.example.com

Без HostKeyAlias OpenSSH ищет ключ хоста под тем именем, к которому подключается, поэтому запись смещается, и каждый пользователь при первом использовании видит предупреждение об изменённом ключе. Установка псевдонима сохраняет записи known-hosts стабильными, пока соединение по-прежнему идёт к релею.

Этот псевдоним применим только к удалённым репозиториям на основе SSH. URL Git по HTTPS его не учитывает, и типичный удалённый URL должен соответствовать транспорту, который вы действительно подготовили.

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

Проверяйте каждый шаг отдельно

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

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

Наконец, задокументируйте предполагаемое раскрытие. Релей TCP, который передаёт SSH к частному бэкенду, — это намеренно открытая дверь в публичном периметре, и он заслуживает такого же рассмотрения, как любой другой входящий сервис.

Похожие статьи