У базы данных нет публичного SSH-порта. Это хорошо: сканеры интернета стучатся в пустоту, перебор паролей по такому адресу невозможен. Но завтра утром администратору всё равно нужно посмотреть логи на этой базе, и делать это он будет с обычного рабочего ноутбука, который стоит не в вашей частной сети, а на кухне, в коворкинге или в поезде. Между ноутбуком и закрытым сервером нужна дверь. Вопрос в том, какой она будет ширины: щель для одного человека к одному серверу или открытый проход ко всей внутренней сети.
Эту дверь принято называть bastion host. Звучит внушительно, хотя по сути это обычный небольшой VPS с публичным адресом, на котором работает SSH и почти ничего больше. Его задача скучная и благородная: быть единственным местом, куда администратор приходит снаружи, и единственным источником SSH-соединений, которые принимают закрытые серверы. Всё интересное в этой схеме происходит не в самом bastion, а в границах вокруг него. Ниже разберём, как эти границы провести.
Два перехода и два набора адресов
Начнём с картинки. В схеме три участника: ноутбук администратора, bastion и закрытый сервер. Условно назовём адреса так: у bastion публичный адрес 203.0.113.10 и частный 10.0.0.2 во внутренней сети, а у закрытого сервера только частный 10.0.0.12. Это выдуманные адреса для примера, подставьте свои.
Ноутбук видит только 203.0.113.10. Адреса 10.0.0.12 для него не существует: пакеты на него либо уйдут в никуда, либо в вашу домашнюю сеть, где такого адреса нет вовсе. Bastion, наоборот, видит оба мира: снаружи к нему приходит администратор, изнутри он дотягивается до частной сети. Поэтому адрес назначения нужно записывать с точки зрения bastion, а не ноутбука. Это самая частая путаница в первых настройках: человек вписывает в конфигурацию частный адрес и ждёт, что ноутбук как-нибудь сам его найдёт.
Два SSH-перехода получаются такими: первый ведёт с ноутбука на bastion, второй с bastion на закрытый сервер. В OpenSSH для этого есть параметр ProxyJump. По документации ssh_config, он подключает SSH-клиент через промежуточный хост и использует TCP forwarding до назначения. Для администратора это выглядит как одна команда:
ssh -J jump-anna@203.0.113.10 anna@10.0.0.12Чтобы не набирать её каждый раз, всё записывают в клиентский ~/.ssh/config:
Host bastion
HostName 203.0.113.10
User jump-anna
IdentityFile ~/.ssh/id_ed25519_bastion
Host db-internal
HostName 10.0.0.12
User anna
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519_internalПосле этого ssh db-internal делает ровно то, что вы нарисовали: сначала доходит до bastion, потом просит его соединить с закрытым сервером. Обратите внимание на две строки IdentityFile и два разных имени пользователя. Это не украшение, об этом следующий раздел.
Есть тонкость, которая важна для понимания всей схемы. При таком переходе сессия на закрытый сервер устанавливается между вашим ноутбуком и самим закрытым сервером, а bastion в этот момент работает как ретранслятор TCP-соединения. Это следует из того, что ProxyJump опирается на TCP forwarding: bastion получает просьбу «открой соединение до такого-то адреса и порта» и передаёт байты туда и обратно. Из этого рассуждения, а не из прямой цитаты документации, вытекают два практических следствия. Во-первых, аутентификация на закрытом сервере происходит с ноутбука, и ей не нужны ни ваши ключи на bastion, ни пересылка agent. Во-вторых, bastion по определению не видит содержимого внутренней SSH-сессии: он перекладывает зашифрованный поток. К этому мы вернёмся, когда будем говорить о журналах.
Кто такой пользователь на bastion и кем он не является
Самая коварная ошибка при построении bastion совсем не техническая, а смысловая: считать, что раз человек имеет вход на bastion, то он «админ инфраструктуры». Нет. Учётная запись на bastion отвечает на вопрос «может ли этот человек постучаться в нашу дверь», а учётная запись на закрытом сервере отвечает на вопрос «может ли он работать внутри». Это два отдельных решения, и хорошая схема хранит их отдельно.
Практически это означает несколько вещей.
- Отдельные учётные записи и отдельные ключи. В примере выше для bastion используется
jump-annaи ключid_ed25519_bastion, а для закрытого сервераannaи другой ключ. Если ключ от двери утечёт, он не откроет комнаты. Если ключ от комнаты утечёт, им нельзя воспользоваться без прохода через дверь. - На bastion нет прав суперпользователя для обычных администраторов. Если им не нужно ничего настраивать на самом bastion, sudo им там не нужен. Роль у них одна: транзит. Правами владельца bastion должен обладать маленький круг людей, которые отвечают за сам узел.
- На bastion нет рабочих инструментов. Нет клиентов баз данных, нет git, нет редакторов с плагинами, нет копий конфигураций и секретов. Чем меньше там установлено, тем меньше нужно обновлять и тем менее интересным становится захват этого узла.
- Список тех, кто может войти на bastion, и список тех, кто может войти на закрытые серверы, не обязаны совпадать. Подрядчик, которому нужен один сервер, не должен получать доступ к остальным только потому, что у него появилась учётная запись на общем входе.
Теперь о том, что здесь нельзя делать, хотя это очень соблазнительно. Удобный способ «пройти дальше» без второго ключа: включить пересылку SSH-agent, чтобы закрытый сервер мог использовать ваши ключи, которые загружены в agent на ноутбуке. Документация ssh_config прямо предупреждает, что к agent forwarding нужно относиться осторожно: тот, кто получит доступ к пересланному agent, может использовать загруженные в него identities для аутентификации, хотя сам материал ключа напрямую не извлекается.
Переведём это на язык bastion. Если вы включили пересылку agent при подключении к узлу, то на этом узле в течение сессии появляется канал, через который можно попросить ваш agent подписывать запросы аутентификации. Любой, кто на bastion обладает достаточными правами (например, администратор узла или злоумышленник, который его взломал), получает возможность воспользоваться этим каналом, пока сессия жива. Ключ не копируется, но подписи выдаются, и для входа на другие серверы этого достаточно. То есть вы взяли узел, который задумывался как минимально доверенный, и сделали его местом, где временно живёт ваша личность. Это ровно обратное тому, ради чего строили bastion.
Хорошая новость в том, что для ProxyJump пересылка agent не нужна. Раз внутренняя сессия устанавливается с ноутбука, аутентификация на закрытом сервере идёт оттуда же, с локального agent или с файла ключа. Поэтому разумное правило звучит так: agent forwarding по умолчанию не включаем ни глобально, ни для Host *, а если когда-нибудь он понадобится для конкретной задачи, то включаем его для конкретного хоста и осознанно, понимая, кто ещё имеет права на промежуточном узле.
Если вы хотите, чтобы сам вход на bastion был устойчивее обычного пароля или файла ключа, подойдёт аппаратный ключ. Как его настроить вместе с запасным способом входа, мы разбирали отдельно в статье про вход на Linux-сервер с ключом FIDO2. Для bastion это особенно уместно, потому что его публичный SSH-порт виден всему интернету, и именно на него будут направлены попытки подбора.
Сужаем проход: что может попросить пользователь у bastion
Теперь самое интересное. По умолчанию SSH умеет много: открыть терминал, выполнить команду, пробросить порт, создать туннель. Каждая из этих возможностей полезна администратору и одинаково полезна тому, кто украл его ключ. Если не ограничить пользователя bastion, получится универсальный прокси в вашу частную сеть: стоит войти, и можно просить соединиться с чем угодно внутри. А мы хотели узкую дверь.
Ограничения удобно строить в три слоя, и каждый закрывает ошибки другого.
Слой первый: закрытые серверы принимают SSH только от bastion
Это сетевое правило, и оно важнее всех настроек SSH. На каждом закрытом сервере (или на сетевом фильтре перед ним) порт SSH открывается только для внутреннего адреса bastion, в нашем примере 10.0.0.2. Всем остальным источникам он закрыт. Тогда даже если кто-то окажется в частной сети через другую дыру, например через взломанное приложение, он не сможет просто так попробовать подобрать вход к серверу по SSH: сервер не откликнется на него.
Заметьте, как это меняет смысл bastion. Раньше защита закрытого сервера состояла из надежды, что никто не знает его адрес. Теперь она состоит из конкретного правила: «принимаем только оттуда». Правило проверяемое, его можно прочитать, протестировать и показать коллеге.
Слой второй: bastion разрешает пересылку только туда, куда нужно
Сетевое правило закрытых серверов отвечает на вопрос «кто может стучать». Но на самом bastion тоже можно задать, какие назначения разрешены пользователю, который просит о пересылке соединения. В sshd есть настройки, управляющие TCP forwarding вообще и списком допустимых адресов и портов назначения для него. Точные названия и поведение параметров лучше читать в man sshd_config именно той версии OpenSSH, которая стоит на вашем bastion: возможности и значения по умолчанию различаются между релизами, и я не буду утверждать, что у вас именно такие. Логика, которую нужно получить на выходе, одна:
- пользователь транзитной группы может просить пересылку только к адресам из короткого списка закрытых серверов и только на порт SSH;
- интерактивная оболочка на bastion ему не нужна: он приходит не работать, а проходить сквозь узел;
- пересылка X11, пересылка agent и произвольные туннели для транзитных пользователей отключены, если нет конкретной причины их включать.
Здесь полезно подумать, что именно вы запрещаете, а не что разрешаете. Вот несколько вопросов, которые стоит задать своей конфигурации.
| Возможность | Нужна ли для обычного входа через ProxyJump | Чем рискуем, если оставить открытой |
|---|---|---|
| Пересылка TCP к закрытым серверам на порт SSH | Да, на ней строится сам переход | Только тем, что список назначений окажется шире, чем нужно |
| Пересылка к произвольным адресам и портам внутренней сети | Нет | Bastion превращается в прокси ко всей сети, включая админки и базы |
| Интерактивная оболочка на bastion | Нет | Появляется место, где можно запускать инструменты и искать соседей |
| Пересылка agent | Нет | Временный доступ к вашим identities для всех с правами на узле |
Первая строка таблицы показывает важную особенность: у ProxyJump нельзя отнять TCP forwarding, потому что он на нём построен. Значит, нельзя просто выключить пересылку на bastion целиком и считать дело закрытым, иначе перестанет работать и сам переход. Вот почему нужны именно списки назначений, а не рубильник.
Слой третий: проверка отрицательными тестами
Конфигурацию, которая ограничивает, нельзя проверить одними положительными тестами. Если вы убедились, что ssh db-internal работает, вы проверили лишь, что дверь открывается. Не менее важно убедиться, что она не открывается куда не надо. Поэтому в проверку входят негативные попытки, например:
- попросить переход к внутреннему серверу, которого нет в списке разрешённых, и убедиться, что запрос отклонён;
- попросить переход к разрешённому серверу, но на порт, не предназначенный для SSH, и убедиться, что он отклонён;
- попробовать открыть интерактивную оболочку на самом bastion под транзитной учётной записью и убедиться, что её нет;
- со второго сервера внутренней сети, не являющегося bastion, попытаться подключиться по SSH к закрытому серверу и убедиться, что сетевой фильтр отбрасывает попытку.
Если хоть один из этих тестов прошёл успешно, то вы ещё не построили то, что собирались. Лучше обнаружить это самому в спокойный день, чем потом читать в журнале чужие следы.
Сколько серверов в списке
Стоит заранее решить, насколько детально разделять назначения между людьми. Два крайних варианта: один общий список закрытых серверов для всех, кто вообще допущен к bastion, или отдельные списки по группам людей. Первый проще и годится для маленькой команды, где все равно доверяют друг другу. Второй нужен, когда у разных людей разные зоны ответственности: подрядчику доступен один сервер приложения, а командe эксплуатации ещё и базы. Не нужно сразу строить сложную матрицу. Начните с честного ответа на вопрос «кому какие серверы нужны сегодня» и записывайте списки так, чтобы их можно было прочитать глазами за минуту.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Что видно в журнале и чего он не доказывает
Одна из причин, по которой bastion любят, звучит так: «у нас появляется единая точка, где всё видно». В этой фразе правда смешана с надеждой, и их стоит разделить.
Начнём с того, что видно. На bastion работает sshd, и он ведёт журнал аутентификации. Обычно в таком журнале можно найти время попытки входа, имя пользователя, адрес, с которого пришло соединение, и результат: принят ключ или отвергнут. Этого достаточно, чтобы ответить на вопросы вида «кто и когда проходил через дверь», «с какого адреса» и «были ли неудачные попытки перед удачной». Уровень подробности зависит от настроек логирования sshd и от вашей системы журналов, поэтому полезно один раз посмотреть, что реально пишется, а не полагаться на память.
Теперь о том, чего журнал не доказывает. Здесь легко принять желаемое за действительное.
- Bastion не является системой записи всех команд. При схеме ProxyJump внутренняя SSH-сессия идёт зашифрованной от ноутбука до закрытого сервера, а bastion переносит TCP-поток. Он знает, что соединение было, возможно, к какому назначению оно запрашивалось и когда закончилось, но не видит, какие команды вводились внутри. Если вам нужна запись действий, это отдельная задача с отдельными инструментами: аудит на самих целевых серверах или специальное решение для записи сессий. Просто «поставить bastion» её не решает, и называть его так в документах для проверяющих не стоит.
- Запись о входе не означает, что вошёл именно этот человек. Журнал показывает, что был использован ключ, привязанный к учётной записи. Если ключ был скопирован, украден или лежал на общем ноутбуке, запись останется точной, а вывод «это был Анна» окажется неверным. Общие учётные записи делают ситуацию ещё хуже: за одним именем может скрываться кто угодно.
- Журнал на самом bastion доверять нужно ровно настолько, насколько защищён сам bastion. Если узел захвачен, записи могут быть изменены или удалены. Поэтому журналы разумно отправлять на отдельное хранилище, недоступное с bastion для изменения. Это будет самостоятельная часть проекта, и я не утверждаю, что она включена в какую-либо конфигурацию по умолчанию.
- Отсутствие записи не доказывает отсутствие входа. Если закрытый сервер по ошибке принимает SSH не только от bastion, то обходной путь не оставит следов на bastion вообще. Именно поэтому правило сетевого фильтра из предыдущего раздела не менее важно, чем сам журнал.
Хороший способ использовать журнал на практике: сопоставлять две стороны. На bastion фиксируется «пользователь X пришёл с адреса A в 10:02 и попросил соединение с 10.0.0.12». На закрытом сервере в его собственном журнале аутентификации записано «вход пользователя anna с адреса 10.0.0.2 в 10:02». Совпадение времени даёт связную историю. Расхождение, например вход на сервер без соответствующей записи на bastion, как раз и есть повод насторожиться.
И ещё одно практическое замечание. Закрытый сервер будет видеть в качестве источника соединения адрес bastion, а не адрес ноутбука администратора. Это естественное следствие того, как устроен переход, и из этого следует, что по одному только журналу закрытого сервера определить, кто пришёл, нельзя. Нужны оба журнала и, желательно, синхронизированное время на обоих узлах.
Единственная дверь: что будет, когда она не открывается
Теперь неприятный вопрос. Если весь вход в закрытую сеть идёт через один bastion, то он же становится единственной точкой отказа для администрирования. Сервер завис, закончилось место на диске, провайдер проводит работы на сети, кончился срок оплаты, обновление ядра не загрузилось, и вы видите хорошо защищённые серверы, до которых вам самим не добраться. Закрывая внешний доступ, мы на самом деле меняем одну угрозу на другую, поэтому о запасном пути нужно подумать до закрытия порта, а не после.
Есть несколько направлений мысли, и они не исключают друг друга.
- Независимый путь, не зависящий от bastion. Это может быть консольный доступ, который предоставляет хостинг, если он у вас есть. Какие именно средства аварийного входа доступны на вашей площадке и в вашем тарифе, лучше выяснить заранее, а не предполагать. Главное свойство такого пути: он работает, даже когда сеть между серверами или сам bastion недоступны.
- Второй bastion. Второй небольшой узел с отдельным адресом, возможно в другой локации. Здесь важно не обманываться: два VPS не дают отказоустойчивости автоматически. Они просто два узла, и их независимость нужно проверять, например по тому, у разных ли они сетей и хостов в вашем плане. Кроме того, второй узел удваивает поверхность, которую нужно обновлять и наблюдать.
- Аварийный ключ и процедура. Отдельный ключ, хранящийся офлайн, и записанный порядок: кто имеет право им воспользоваться, как это фиксируется и когда ключ после использования заменяется. Эта процедура неприятна ровно настолько, насколько должна быть: пользоваться ею хочется редко.
- Скучное, но важное: мониторинг самого bastion. Если доступность входа важна, то за узлом нужно следить так же внимательно, как за любым продуктовым сервисом. Занятый диск, переполненный журнал и истёкший сертификат на соседнем сервисе способны закрыть дверь без всякого злого умысла.
Отдельно скажу про порядок действий при переходе. Не закрывайте прямой SSH на существующих серверах, пока не убедились в нескольких вещах. Первое: вход через bastion работает со всех рабочих мест, с которых он нужен, а не только с того ноутбука, на котором настраивали. Второе: отрицательные тесты из предыдущего раздела прошли. Третье: аварийный путь проверен в действии, а не просто записан в документе. И только после этого закрывайте прямой доступ, причём сначала на одном некритичном сервере, потом на остальных. Если вы закрыли порт и поняли, что запасного пути нет, то выяснять это лучше на тестовом сервере, а не на базе данных.
Какой VPS нужен под такую дверь
Bastion не выполняет вычислений и не хранит данных: он принимает SSH-соединения и перекладывает байты. Поэтому ему обычно не нужна мощная конфигурация, и выбирать её стоит не по «чем больше, тем лучше», а по количеству одновременных соединений, которые вы ожидаете, и по запасу на обновления и журналы. Ресурсы тарифа сами по себе не обещание конкретной ёмкости, поэтому лучше один раз измерить нагрузку в вашем сценарии, чем верить цифре на странице.
В линейке VPS MAATRIX для такой роли вполне естественно начать с младшего тарифа Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) и посмотреть на фактическое потребление. Если команда выросла или вы добавляете сбор журналов на самом узле, можно перейти на Uab (2 vCPU, 4 ГБ, 80 ГБ). Это отправная точка для размышлений, а не гарантированный запас. На VPS предоставляется root-доступ, так что можно самостоятельно настроить sshd, фильтр и систему журналов именно так, как описано выше. В публичном предложении перечислены локации UK, US, RU и FR; расположение bastion стоит выбирать исходя из того, где находятся ваши закрытые серверы и администраторы. Наличие конкретных образов и операционных систем в нужной локации проверяйте в форме заказа, а не по памяти.
Одно замечание о соседстве. Не стоит класть на bastion ничего, кроме того, что нужно для его роли. Соблазн «раз уж узел стоит, повесим на него ещё и мониторинг, и VPN, и тестовый стенд» понятен, но каждая такая добавка расширяет поверхность атаки узла, который по замыслу должен быть самым скучным в вашей инфраструктуре. Если нужны другие сервисы, поставьте для них другой VPS.
Как выглядит рабочий день с такой дверью
Соберём всё в один сценарий с условными именами. Администратор Анна приходит утром с ноутбука. Она запускает ssh db-internal. Её клиент соединяется с bastion под транзитной учётной записью и своим ключом для двери. Bastion проверяет, что запрошенное назначение входит в короткий список, и передаёт соединение внутрь. Затем Анна на этом же соединении аутентифицируется на закрытом сервере вторым ключом и своей обычной учётной записью. В журнале bastion остаётся запись о проходе, в журнале закрытого сервера запись о входе с адреса bastion. Agent она не пересылает, оболочки на bastion у неё нет, а попытка попросить соединение с соседним сервером, не входящим в список, закончилась бы отказом.
Когда в команду приходит подрядчик, ему заводят транзитную учётную запись и разрешают один конкретный сервер. Он не получает ни доступа к остальным, ни оболочки на bastion. Когда подрядчик уходит, учётную запись удаляют в одном месте, а его ключ на закрытом сервере отзывают в другом. Два места, зато каждое отвечает на свой вопрос, и забыть одно из них заметнее, чем кажется: журнал входов покажет, пользуется ли кто-то уже отозванным доступом.
Если ночью bastion не отвечает, дежурный не пытается открыть прямой SSH «всего на пять минут». Он идёт по заранее проверенному аварийному пути, фиксирует факт использования и после починки возвращает всё как было. Так аварийный доступ остаётся исключением, а не тихо превращается в новую норму.
Узкая дверь вместо нового рабочего стола
Хороший bastion легко узнать по скуке. На нём нет ничего, что хотелось бы открыть и поковыряться: ни инструментов, ни секретов, ни копий конфигураций, ни привычки «зайти и посмотреть». Он знает только, кому разрешено постучаться, и к каким именно серверам можно протянуть соединение. Всё остальное происходит там, где ему и место: на самих закрытых серверах, с их собственными учётными записями и их собственными журналами.
Опасность этой схемы в том, что со временем дверь начинает расти: сначала добавили ещё пару серверов в список, потом разрешили пересылку для одного удобного случая, потом включили agent forwarding, чтобы не возиться со вторым ключом. Через год у вас вместо узкого прохода к разрешённому серверу оказывается привилегированный рабочий стол, на который заходят все и с которого видно всё. Чтобы этого не случилось, периодически перечитывайте списки назначений и права так же внимательно, как перечитываете рабочий код, и спрашивайте себя: что мы разрешили из удобства, а что из необходимости. Дверь должна пропускать одного человека к одному разрешённому серверу, и пусть она остаётся достаточно узкой, чтобы вы замечали, когда в неё пытаются протиснуться что-то лишнее.