SSH туннели на практике

У данного поста есть видео-версия: youtu.be/GBx3KEcuKFA

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

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

Первое знакомство с туннелями

Мое первое знакомство с туннелями произошло при моей попытке развернуть разработанный проект на сервере клиента. Я приехал к заказчику в офис, заранее сохранив в заметки ссылки на наши репозитории, названия докер образов и тд.

Но меня ждал сюрприз. Клиент просто дал мне клавиатуру, показал монитор и сказал "на, разворачивай". По политике безопасности клиента я не мог получить удаленный доступ к этому серверу, чтобы удобно заливать все из своего офиса. Поэтому мне предстояло руками разворачивать все на этом сервере, вводя все ссылки и названия по букве.

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

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

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

Основная идея туннелей

В целом суть туннеля наглядно изображена на анимации выше. Но дополню текстом:

  1. У вас есть сервер, к которому вы хотите получить доступ, но не можете ходить на него напрямую. Назовем его сервер 1;
  2. Между вами и целевым сервером есть сервер, который имеет доступ к целевому и к которому имеете SSH доступ вы. Назовем его сервер 2;
  3. Вы можете зайти по SSH на сервер 2 и, через консоль, уже отправлять любые запрос на сервер 1. Также вы можете зайти по SSH на сервер 1 и двигаться дальше.

Примерно так и работает SSH-туннели. Только нам не всегда обязательно заходить на один сервер и с него уже слать запросы на следующий. Для упрощения этого процесса мы можем воспользоваться переадресацией портов (Port Forwarding).

Port forwarding на пальцах

У меня ушло какое-то количество мыслетоплива, чтобы понять суть работы переадресации портов.

Расширим ситуацию, описанную выше:

  1. У нас есть сервер S1, к которому у нас есть доступ на любой порт;
  2. Есть сервер S2, к 80 порту которого хотим получить доступ мы через наш S1;
  3. Мы одной командой можем открыть на S1 80 порт, чтобы он пересылал эти запросы на 80 порт S2 и потом передавал нам ответ;
  4. Также, например, одной командой, мы можем открыть 81 порт, который будет переадресовывать запрос на 80 порт другого сервера S3.

Именно эта "пересылка" портов и называется переадресацией портов или Port Forwarding. Теперь давайте перейдем к реальным кейсам использования туннелей.

Кейс с nginx

Рассмотрим простой кейс. У нас есть сервер, который закрыт миру, но мы имеем к нему SSH доступ. На этом сервере запущен сервер nginx, который слушает 80 порт.

Для того, чтобы получить доступ к тому, что опубликовано на 80 порту сервера, мы можем открыть туннель, который подключится к серверу, и опубликовать на локальном 80 порту. То есть, мы будем стучаться на localhost:80 и видеть ровно то, что опубликовано на 80 порту удаленного сервера. Для этого вводим следующую команду:

ssh -L 80:127.0.0.1:80 remote.server.address

После ввода этой команды мы подключимся на сервер по SSH и, что важно, пока активно это соединение, у нас будет активен этот туннель. Теперь мы можем в браузере открыть localhost:80 и откроется то, что опубликовано на 80 порту удаленного сервера. Подробнее что-есть-что в этой команде:

  1. Первым параметром идет флаг "-L" который указывает на то, что нам необходимо открыть туннель и начать слушать порт
  2. Далее указываем интерфейс на котором мы хотим слушать (по-умолчанию localhost и в рамках данного примера он не указан). В нашем случае указываем порт 80 (на рис. "локальный порт").
  3. Далее указываем куда надо будет подключаться на удаленном сервере. В нашем примере 127.0.0.1, что значит, что туннель будет пробрасывать на сам же удаленный сервер. На что можно заменять это значение можно прочитать ниже.
  4. Порт на удаленном сервере - в нашем случае это 80. Если бы nginx, например, видел бы на 8080, то мы бы указали 8080 в этом месте.
  5. Адрес удаленного сервера. Указываем его тем же самым образом, что и при обычном подключении к SSH.

Теперь можно переходить на localhost в браузере и видеть то, что на сервере на 80 порту. В моем примере можно увидеть только ошибку nginx, которую выдает nginx-proxy на сервере

Кейс с базой данных Postgres в докере

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

  1. Есть я и мой ноутбук
  2. На ноутбуке есть код моего backend приложения
  3. В основном я работаю с локальной копией базы данных во время разработки
  4. Мне надо подключиться к базе данных, которая находится на сервере (тестовом или боевом) в изолированной сети Docker

Когда мы говорим про работу через IDE для работы с БД, мы можем легко воспользоваться встроенным механизмом туннелей (инструкция тут). Но трудности возникать если мы хотим из кода нашего приложения подключиться к этой базе.

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

Для решения этого вопроса достаточно вбить следующую команду:

ssh -L 5432:postgres-db:5432

Тут ситуация аналогична предыдущей. Только вместо локального ip адреса самого сервера мы указываем ip адрес (в данном случае имя хоста postgres-db) в качестве целевого адреса.

В случае работы с Docker я использую этот контейнер, чтобы знать адреса всех контейнеров на сервере. После запуска имя хоста каждого контейнера можно получить в файле /etc/hosts. И после перезапуска ip адрес меняется, а хост остается

Дальше я уже могу указывать localhost:5432 в качестве сервера postgres в моем приложении и подключусь уже напрямую к базе данных в docker контейнере на том сервере.

Таких туннелей можно запускать сколько угодно. Все запускаются по аналогии. Например можно построить такую схему:

  1. Есть сервера S1, S2, S3.
  2. S1 имеет доступ к S2 и S2
  3. Я имею доступ только к S1 через SSH на 22 порту
  4. На серверах S2 и S3 запущены Postgres на 5432 и HTTP на 80 портах соответственно.
  5. Я запускаю 2 туннеля и работаю с этими серверами как будто с локальными. Команды для запуска туннелей ниже:
ssh -p 22 -l 5432:s2:5432 s1
ssh -p 22 -l 80:s3:80 s1

Обратные туннели

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

  1. Я работаю на своем ноутбуке и у меня крутится копия сайта клиента на 3000 порту
  2. Нахожусь при этом в кафе с бесплатным wifi
  3. Клиент находится у себя в офисе в закрытой сети
  4. Я хочу показать текущую версию сайта клиента прямо со своего ноутбука
  5. И я и клиент имеем доступ к одному серверу во внешней сети (например мой VPS)

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

Для решения этого вопроса нам как раз может помочь обратный туннель (Reverse port forwarding). Чтобы клиент мог видеть сайт на своем ПК, мне необходимо запустить следующую команду:

ssh -R 0.0.0.0:3001:127.0.0.1:3000 server.ru

Давайте разберемся с каждым параметром в этой команде:

Справа налево:

  1. Адрес сервера, который доступен и мне и клиенту
  2. Локальный адрес и порт, который я хочу сделать доступным (localhost:3000)
  3. Адрес и порт на общем сервере, на котором будет отображаться мой сайт с 3000 порту
  4. Флаг, передающий SSH серверу, что я намерен открыть обратный туннель на мой локальный порт

Клиент, после запуска такого туннеля, может видеть наш сайт по адресу server.ru:3001.

Правда сделать это может быть чуть сложнее чем "просто вбить одну команду". Дело в том, что "из коробки" sshd демон не допускает подключения на внешний интерфейс.

Другими словами - то, что мы указали "0.0.0.0" как адрес для прослушивания, говорит серверу следующее: "принимай любые соединения на любой интерфейс на порт 3001, даже если запрос идет извне". Но эта настройка, обычно, выключена. Для того, чтобы внешние соединения заработали необходимо в файле /etc/ssh/sshd_config добавить строчку GatewayPorts yes

Стоит быть крайне внимательным и аккуратным с этой настройкой. Ставя её в значение yes вы открываете порт всему миру и это может стать прекрасной дырой в безопасности для злоумышленников. ВСЕГДА ЗАКРЫВАЙТЕ ТУННЕЛИ после завершения работы через них.

Для чего еще можно использовать обратный туннель

Я использовал обратные туннели, также, при отладке проектов на PHP. В случае, когда код расположен на удаленном сервере и мы работаем с кодом через SFTP, бывает необходимость отладить код, который запущен на сервере, но через локальный XDebug. Тут на помощь также приходит SSH туннель, который запускается и дает доступ удаленному серверу к нашему ПК.

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

На этом все. Это была короткая сводка про те туннели, которые я использую в своей работе. Есть еще классная штука - socks proxy, который также можно поднять похожей командой, но про нее, может быть, я напишу отдельный пост в свое время.

Благодарю за внимание!