Проброс SSH к GitLab через внешнюю VM без публичного IP

2026.01.30
Полное руководство по проксированию SSH‑трафика к GitLab через внешний и внутренний шлюзы без отдельного VPN. Применяем NAT в iptables, настраиваем DNAT и MASQUERADE, проверяем метрики и отлаживаем подключение.

Нужно было развернуть Gitlab на домашнем сервере, но чтобы из интернета можно было по ssh ходить. На домашнем сервере нет публичного IP, а значит нужна внешняя виртуалка. Ситуацию усложняет тот факт, что внешняя виртуалка не связана напрямую с локальным сервером, а общение происходит через посредника как-то так:

Схема сети: внешний шлюз ↔‑ WireGuard ↔‑ внутренний шлюз ↔‑ GitLab

Можно было бы создать VPN‑соединение между внешней виртуалкой и внутренней, где развернут GitLab, но тогда для каждой новой службы требовалось бы отдельное VPN‑соединение. В схеме выше нужно соединить только одну внешнюю и одну внутреннюю виртуалки под VPN, а дальше передавать трафик от внутреннего шлюза к локальным хостам. Кажется, это намного проще в понимании и инфраструктурно.

И тут нам поможет NAT ...

Цель статьи: показать кейс для понимания NAT, который как оказалось не такой уж и сложный :)

Почему NAT?

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

Трафик по схеме выше должен проходить через 2 шлюза/виртуалки к Gitlab и обратно, и на каждом шлюзе нужно изменять адреса источника и приемника у каждого IP-пакета. Эти операции можно писать так:

В Linux NAT реализуется через таблицу NAT в iptables в следующих цепочках:

Цепочка Когда срабатывает Что обычно делаем
PREROUTING На входе виртуалки, до роутинга DNAT (перенаправление входящих IP-пакетов)
POSTROUTING На выходе виртуалки, после роутинга SNAT / MASQUERADE (перенаправление исходящих IP-пакетов)

Настоятельно рекомендую ознакомиться с iptables в этой статье.

Открыть порты!

Прежде чем начнем необходимо открыть следующие порты на виртуалках:

Разрешить TCP-трафик на 2222 порту можно так:

$ sudo iptables -A INPUT -p tcp --dport 2222 -j ACCEPT

А просмотреть список открытых портов можно так (убрал все лишнее, оставил только результат работы прошлой команды):

$ sudo iptables -vnL
Chain INPUT (policy DROP 13 packets, 6051 bytes)
    1    40 ACCEPT     6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:2222

Реализация

Для начала нам нужно разрешить пересылку трафика через оба наших gateway так:

$ sudo sysctl -w net.ipv4.ip_forward=1

# или так чтобы работало и после перезагрузки
$ echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf >/dev/null

Теперь начнем с конца и будем сразу же тестировать, попробуем gateway-internal <=> gitlab.

gateway-internal - gitlab

На gateway-internal нужно:

# перенаправляем все TCP‑запросы на порт 2222 к GitLab (192.168.3.14:22)
$ sudo iptables -t nat -A PREROUTING -p tcp -m tcp --dport 2222 -j DNAT --to-destination 192.168.3.14:22

# подменяем исходный IP у пакетов из 10.0.0.0/24, идущих к 192.168.0.0/17,
# чтобы ответы возвращались через этот шлюз
$ sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -d 192.168.0.0/17 -j MASQUERADE

Протестируем что мы можем подключиться по ssh из gateway-internal к gitlab с детальным логом:

$ ssh git@192.168.3.14 -vvv

Если ничего не двигается дальше строки debug3: set_sock_tos: set socket 3 IP_TOS 0x10, но значит мы не можем установить соединение, а если видим debug1: Connection established и в конечном итоге либо удалось войти либо git@192.168.3.14: Permission denied (publickey) то все сработало.

Двигаемся дальше, теперь нужно настроить трафик внутри сети WireGuard от gateway-external в gateway-internal и обратно.

gateway-external - gateway-internal

На gateway-external нужно:

# перенаправляем все входящие TCP‑соединения, пришедшие на порт 2222 (на этом шлюзе),
# к внутреннему хосту 10.0.0.3, тоже на порт 2222
$ sudo iptables -t nat -A PREROUTING -p tcp -m tcp --dport 2222 -j DNAT --to-destination 10.0.0.3:2222

# подменяем исходный IP‑адрес пакетов, чтобы ответы от 10.0.0.3 шли обратно через этот же шлюз.
$ sudo iptables -t nat -A POSTROUTING -d 10.0.0.3/32 -p tcp -m tcp --dport 2222 -j MASQUERADE

Теперь нужно попробовать подключиться по ssh с gateway-external на gateway-internal на 2222 порт:

$ ssh root@10.0.0.3 -p 2222 -vvv

Анализируем вывод аналогично предыдущему тесту.

И финально пытаемся подключиться по ssh извне как если бы обычно пользовались Gitlab'ом:

$ ssh git@gitlab.example.com -p 2222 -vvv

Как понять что правила работают?

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

# вывести таблицу правил цепочки PREROUTING
$ sudo iptables -t nat -L PREROUTING -n -v

# вывести таблицу правил цепочки POSTROUTING
$ sudo iptables -t nat -L POSTROUTING -n -v

Скриншот метрик iptables

Обязательно убедитесь что нужные порты на ваших машинах открыты, иначе правила в таблице NAT не будут работать.

$ sudo iptables -vnL

Полезные команды

Если где-то ошиблись в команде и нужно очистить таблицу NAT то можно так:

# очистить все цепочки
$ sudo iptables -t nat -F

# очистить цепочку PREROUTING
$ sudo iptables -t nat -F PREROUTING

# очистить цепочку POSTROUTING
$ sudo iptables -t nat -F POSTROUTING

Вывести команды для установки правил в таблице NAT:

$ sudo iptables -S -t nat
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-A PREROUTING -p tcp -m tcp --dport 2222 -j DNAT --to-destination 10.0.0.3:2222
-A POSTROUTING -d 10.0.0.3/32 -p tcp -m tcp --dport 2222 -j MASQUERADE

Это позволит увидеть весь список правил для таблицы NAT.


Краткий чек‑лист:

Итого: с помощью двух простых правил в iptables мы получили полностью автоматический прокси для SSH‑доступа к GitLab во внутренней сети спрятанный за двумя виртуалками, без необходимости разворачивать отдельный VPN‑туннель для каждой машины.

В телеграм канале DevOps от первого лица можно оставить комментарий или почитать интересные истории из практики DevOps