SSH-туннель переносит TCP-соединение внутри SSH-сессии, которой вы уже доверяете. Команда ssh -L 3307:127.0.0.1:3306 deploy@203.0.113.10 делает так, что порт 3307 на вашем ноутбуке ведёт на порт 3306 loopback-интерфейса сервера. База данных, которая слушает только 127.0.0.1, оказывается доступна с вашего рабочего места, и при этом не нужно открывать ни одного порта в firewall. Флаг -R работает наоборот: порт на сервере ведёт обратно к чему-то на вашей машине. Флаг -D превращает сессию в SOCKS-прокси. Эти три флага плюс -N, который отключает запуск shell, покрывают почти всё, для чего люди используют туннели.
Однострочные примеры умалчивают о том, что потом создаёт проблемы: к какому адресу привязывается проброс, что сервер вправе отклонить, почему туннель молча падает за ночь и почему проброс, привязанный не к тому интерфейсу, тихо отдаёт вашу базу данных в интернет. Эта статья разбирает всё это и предполагает, что вы уже умеете входить по ключу. Если нет, начните со статьи SSH-ключи и защита sshd_config.
Три вида проброса: коротко#
| Флаг | Слушает | Ведёт на |
|---|---|---|
-L local | Ваша машина | Хост и порт, как их видит сервер |
-R remote | Сервер | Хост и порт, как их видит ваша машина |
-D dynamic | Ваша машина | Куда угодно, выбирается для каждого соединения (SOCKS) |
Правило, которое делает синтаксис читаемым: сторона, на которой порт слушает, - это сторона, названная флагом, а имя хоста назначения в спецификации разрешается на другом конце. В -L 3307:db.internal:3306 имя db.internal ищет SSH-сервер, а не ваш ноутбук. Именно поэтому туннель может достучаться до имён и адресов, о которых ваша машина никогда не слышала.
Любой проброс идёт по одному зашифрованному TCP-соединению к порту 22 (или к тому, где слушает sshd). Сервису-назначению не нужно знать, что его туннелируют: он видит обычное TCP-соединение от сервера, на котором работает, или от того хоста, к которому сервер подключился сам.
Перед тем как строить планы вокруг туннелей, стоит знать два ограничения. SSH пробрасывает только TCP: UDP-проброса нет, поэтому игровой трафик, DNS поверх UDP и большинство голосовых протоколов через -L или -R не пройдут. Кроме того, все каналы делят одно TCP-соединение, поэтому одна большая передача замедляет все остальные пробросы в той же сессии, а потери пакетов на маршруте останавливают их все разом.
Локальный проброс: доступ к базе, которая слушает localhost#
С этого случая обычно и приходят. MySQL или PostgreSQL на VDS правильно настроен слушать только loopback (bind-address = 127.0.0.1 в MySQL, listen_addresses = 'localhost' в PostgreSQL), а вы хотите открыть его в GUI-клиенте на своём компьютере.
$ ssh -N -L 3307:127.0.0.1:3306 deploy@203.0.113.10Читается так: слушать локальный порт 3307 и отправлять каждое соединение на 127.0.0.1:3306 с точки зрения сервера. Флаг -N не даёт SSH запускать удалённый shell, поэтому команда просто висит и держит туннель открытым, пока вы не нажмёте Ctrl+C. Затем во втором терминале:
$ mysql -h 127.0.0.1 -P 3307 -u app -p appdb$ psql "host=127.0.0.1 port=5433 user=app dbname=appdb" # for a -L 5433:...:5432От трёх деталей зависит, заработает ли всё с первого раза.
- На стороне клиента используйте `127.0.0.1`, а не `localhost`. Клиент MySQL воспринимает
localhostкак "использовать локальный Unix-сокет" и игнорирует порт, поэтомуmysql -h localhost -P 3307подключится к MySQL на вашем ноутбуке (или завершится ошибкой), но к туннелю не обратится никогда. Подробно это поведение разобрано в статье удалённые подключения к MySQL. - Выберите свободный локальный порт. Проброс
3306на3306завершится ошибкойbind [127.0.0.1]:3306: Address already in use, как только у вас запущен локальный MySQL. Привычка брать настоящий порт плюс один (3307,5433) помогает этого избежать. В Linux привязка к локальному порту ниже1024требует root, и это ещё одна причина выбирать большие номера. - База видит соединение как идущее с самого сервера. Для MySQL это значит, что учётная запись должна существовать для
127.0.0.1илиlocalhost(последнее совпадает только при включённом разрешении имён, а оно включено по умолчанию). Для PostgreSQL соединение приходит по TCP с127.0.0.1, поэтому вpg_hba.confнужна строкаhostдля127.0.0.1/32; в большинстве пакетов дистрибутивов она есть. Туннель не обходит аутентификацию базы данных - он лишь доводит вас до двери.
Проброс на другую машину или на Unix-сокет
Назначением не обязательно должен быть сам SSH-сервер. Если база живёт на приватном адресе за машиной, на которую вы можете войти, укажите её:
$ ssh -N -L 5433:10.0.0.20:5432 deploy@203.0.113.10Теперь SSH-сервер открывает соединение с 10.0.0.20:5432 от вашего имени. Этот участок между сервером и базой данных не шифруется средствами SSH: это обычное TCP-соединение в приватной сети. В сети, которую вы контролируете, это обычно нормально, но когда это не так, об этом стоит сказать вслух.
OpenSSH 6.7 и новее умеет пробрасывать и на Unix-сокет на сервере, что позволяет добраться до базы, у которой вообще нет TCP-listener:
$ ssh -N -L 5433:/var/run/postgresql/.s.PGSQL.5432 deploy@203.0.113.10PostgreSQL в этом случае видит соединение через локальный сокет, и в pg_hba.conf применяются правила local, а не host. Проброс сокетов отдельно регулируется на сервере директивой AllowStreamLocalForwarding, по умолчанию она равна yes.
Постоянные туннели через ~/.ssh/config#
Набирать спецификацию каждый раз надоедает, а конфигурационный файл принимает то же самое под именем:
Host db-tunnel HostName 203.0.113.10 User deploy IdentityFile ~/.ssh/id_ed25519 LocalForward 3307 127.0.0.1:3306 LocalForward 5433 127.0.0.1:5432 ExitOnForwardFailure yes ServerAliveInterval 30 ServerAliveCountMax 3Теперь ssh -N db-tunnel открывает оба проброса. Обратите внимание: в LocalForward слушающий порт и назначение разделяются пробелом, а не двоеточием. RemoteForward и DynamicForward существуют в той же форме.
ExitOnForwardFailure yes стоит добавлять всегда. Без него, если порт уже занят, SSH выведет предупреждение, всё равно впустит вас и оставит сессию без туннеля. Она выглядит так же, как рабочая, пока клиент не сможет подключиться. С этой опцией SSH завершается с ошибкой, и всё, что за ним присматривает, может попробовать снова.
Если до хоста с базой можно добраться только через bastion, сочетайте проброс с ProxyJump, а не стройте два туннеля вручную цепочкой. Команда ssh -J bastion -N -L 5433:127.0.0.1:5432 deploy@10.0.0.20 подключается через bastion к внутренней машине и пробрасывает порт оттуда, а аутентификация на каждом участке происходит на вашем ноутбуке.
Стоит знать ещё два флага. -f отправляет SSH в фон после аутентификации, что удобно для скриптов: ssh -f -N db-tunnel. А добавить проброс в уже открытую сессию можно escape-последовательностью ~C в начале строки, после которой вводится -L 8080:127.0.0.1:80. Начиная с OpenSSH 9.2 эта командная строка по умолчанию отключена, и в конфигурации клиента нужен EnableEscapeCommandline yes.
Удалённый проброс: открываем что-то со своей машины#
-R открывает слушающий порт на сервере и отправляет соединения обратно через сессию на вашу сторону.
$ ssh -N -R 8080:127.0.0.1:3000 deploy@203.0.113.10Соединения с портом 8080 на сервере теперь попадают на 127.0.0.1:3000 на вашем ноутбуке: это может быть веб-сервер для разработки, приёмник webhook, который вы отлаживаете, или сервис в домашней сети без публичного адреса. Это та же идея, которую упаковывают коммерческие туннельные сервисы, только без третьей стороны.
Вот сюрприз, с которым сталкиваются почти все: по умолчанию удалённый порт слушает только на loopback сервера. Выполните ss -tlnp на сервере, и вы увидите 127.0.0.1:8080, а не 0.0.0.0:8080, так что из интернета до него никто не дотянется. Это поведение задаётся директивой GatewayPorts в sshd_config на сервере:
GatewayPorts | Удалённые пробросы привязываются к | Когда использовать |
|---|---|---|
no (по умолчанию) | Только loopback | Почти всегда |
yes | Всем интерфейсам | Редко; каждый -R становится публичным |
clientspecified | Тому, что запросит клиент | Когда часть пробросов должна быть публичной |
При clientspecified адрес выбирает клиент в спецификации: -R 0.0.0.0:8080:127.0.0.1:3000 запрашивает все интерфейсы, а -R 8080:... по-прежнему получает loopback. Из двух нестандартных значений это более безопасное: открытие наружу становится осознанным действием для каждого туннеля, а не побочным эффектом каждого из них.
Более распространённый и безопасный вариант - оставить GatewayPorts no и поставить на сервере reverse proxy перед loopback-портом. Nginx, слушающий 443 с сертификатом и проксирующий на 127.0.0.1:8080, даёт пробрасываемому сервису нормальное имя и TLS, а сам порт туннеля никогда не становится публичным. Если же вы открываете порт в firewall, в руководстве по ufw описано, как разрешить его только тем адресам, которым он нужен.
Удалённый порт 0 просит сервер выбрать свободный порт и вывести его, что полезно в скриптах. А начиная с OpenSSH 7.6, -R только с портом и без назначения (-R 1080) создаёт на сервере SOCKS-прокси, выходящий в сеть через вашу машину, - зеркальное отражение -D.
Динамический проброс: SOCKS-прокси одним флагом#
$ ssh -N -D 1080 deploy@203.0.113.10Эта команда запускает прокси SOCKS4 и SOCKS5 на 127.0.0.1:1080. Для любого приложения, настроенного на него, каждое соединение открывается с сервера, а не с вашей машины, к тому назначению, которое оно запрашивает. Направьте на него браузер, и вы будете ходить по сайтам с адреса сервера; направьте инструмент для работы с базами, и вы сможете достучаться до всех приватных хостов, доступных серверу.
$ curl --socks5-hostname 127.0.0.1:1080 http://10.0.0.20:9000/health--socks5-hostname важен: он передаёт имя прокси для разрешения, а не разрешает его локально. Простой --socks5 сначала ищет имя на вашей машине, что не работает для внутренних имён и отдаёт запрос вашему локальному резолверу. У браузеров есть такой же переключатель: в Firefox это опция проксирования DNS при использовании SOCKS v5.
SOCKS-прокси поверх SSH - разумный способ добраться до интерфейса администратора в приватной сети или проверить, как сайт отвечает с сервера в другой стране. Это не VPN: через прокси идут только приложения, настроенные на его использование, UDP не идёт, а всё, что вы забыли на него направить, уходит напрямую. Если вам на самом деле нужно, чтобы весь трафик устройства шёл через сервер, правильный инструмент - настоящий VPN, а что VPN защищает и что не защищает проводит эту границу.
Как держать туннель поднятым: autossh и systemd#
Туннель, открытый вручную, умирает вместе с wifi ноутбука, с роутером, который сбрасывает простаивающие сессии, и с каждой перезагрузкой сервера. Если от туннеля что-то зависит, например отчётное задание читает удалённую базу или домашний сервис открыт через -R, за ним нужен присмотр.
Первая линия защиты - пара keepalive: ServerAliveInterval 30 и ServerAliveCountMax 3. Клиент отправляет проверочный пакет каждые 30 секунд по зашифрованному каналу и сдаётся после трёх без ответа, поэтому оборванное соединение замечается примерно за 90 секунд, а не висит вечно. Именно сдаться и нужно: процесс, который завершился, можно перезапустить.
autossh - классическое средство перезапуска. Он запускает ssh и стартует его заново, когда тот завершается.
$ autossh -M 0 -f -N \ -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" \ -o "ExitOnForwardFailure yes" \ -L 3307:127.0.0.1:3306 deploy@203.0.113.10-M 0 отключает собственный мониторинговый порт autossh и полагается на keepalive самого SSH, что и есть обычная современная схема. Одна особенность: по умолчанию autossh считает ssh, завершившийся в первые 30 секунд, фатальной ошибкой и останавливается (это регулирует переменная окружения AUTOSSH_GATETIME), так что неверный пароль или ключ хоста обнаруживаются, а не повторяются бесконечно. При запуске с -f время этого порога обнуляется, и autossh продолжает попытки.
На Linux-машине с systemd autossh вообще не нужен. Unit с Restart=always делает ту же работу и складывает логи в журнал:
[Unit]Description=SSH tunnel to the databaseAfter=network-online.targetWants=network-online.target[Service]User=tunnelExecStart=/usr/bin/ssh -N -o ExitOnForwardFailure=yes \ -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \ -i /home/tunnel/.ssh/id_ed25519 -L 3307:127.0.0.1:3306 tunnel@203.0.113.10Restart=alwaysRestartSec=10[Install]WantedBy=multi-user.target$ sudo systemctl daemon-reload$ sudo systemctl enable --now db-tunnel$ journalctl -u db-tunnel -fДве вещи делают такую конфигурацию безопасной для постоянной работы. Во-первых, ключ, который она использует, не имеет парольной фразы (иначе нельзя: вводить её некому), поэтому это должен быть отдельный ключ для отдельной учётной записи, ограниченный на сервере единственным нужным пробросом, как показано в следующем разделе. Во-вторых, первое подключение нужно один раз выполнить вручную от имени этого пользователя, чтобы принять ключ хоста в его known_hosts: служба не может ответить на вопрос об отпечатке, и ради обхода этого её никогда не следует настраивать с StrictHostKeyChecking no.
Что разрешает сервер: AllowTcpForwarding и другие директивы#
Всё описанное выше работает потому, что сервер OpenSSH по умолчанию разрешает проброс. Для учётной записи самого администратора это правильное значение по умолчанию, а для большинства остальных - неправильное. Вот директивы, которые это регулируют, все они находятся в sshd_config:
| Директива | По умолчанию | Что делает |
|---|---|---|
AllowTcpForwarding | yes | no, local, remote или all |
GatewayPorts | no | Привязываются ли порты -R за пределами loopback |
PermitOpen | any | Назначения, куда может вести -L |
PermitListen | any | Порты, которые может слушать -R (OpenSSH 7.8+) |
AllowStreamLocalForwarding | yes | Проброс Unix-сокетов |
PermitTunnel | no | Устройства tun уровня 3 через ssh -w |
DisableForwarding | no | Отключает сразу все виды проброса |
Люди упускают из виду, что для проброса shell не нужен. Пользователь, чья учётная запись ограничена SFTP через ForceCommand internal-sftp или чей shell - /usr/sbin/nologin, всё равно может выполнить ssh -N -L ... и использовать ваш сервер как ступеньку в его приватную сеть, если проброс для него не отключён. Поэтому блок Match для ограниченных учётных записей должен говорить об этом явно:
Match Group sftp-only AllowTcpForwarding no AllowStreamLocalForwarding no X11Forwarding noMatch User tunnel AllowTcpForwarding local PermitOpen 127.0.0.1:3306 X11Forwarding noТе же ограничения можно задать для каждого ключа в authorized_keys, и это аккуратнее для служебной учётной записи с одной задачей:
restrict,port-forwarding,permitopen="127.0.0.1:3306",command="/bin/false" ssh-ed25519 AAAAC3Nza... tunnel@reportingrestrict отключает всё необязательное, port-forwarding снова включает проброс, permitopen ограничивает его портом базы данных, а принудительная команда означает, что ключ не сможет ничего запустить, даже если кто-то попросит shell. -N никогда не запускает команду, так что туннель это не затрагивает. permitlisten - аналог для ключа, который должен создавать только один порт -R. Украденная копия такого ключа стоит ровно одного подключения к базе данных, которое всё равно должно пройти аутентификацию в базе.
Как всегда с sshd, проверяйте конфигурацию через sshd -t и держите открытой уже существующую сессию перед перезапуском.
Предупреждения по безопасности, к которым стоит отнестись всерьёз#
Туннель шифруется, аутентифицируется и удобен, и каждое из этих свойств облегчает забывчивость: он ещё и дыра в той границе, которую вы провели.
- Локальный проброс открыт для всех на вашей машине.
-L 3307:...привязывается к127.0.0.1, поэтому любой процесс и любой пользователь этого компьютера может к нему подключиться. На ноутбуке, которым пользуетесь только вы, это нормально. На общем jump-хосте или build-сервере это отдаёт ваше подключение к базе всем, у кого есть логин. Пароль базы данных по-прежнему действует, но рассчитывайте так, будто порт публичный. - Никогда не привязывайте проброс ко всем интерфейсам случайно. Флаг
-g,GatewayPorts yesв конфигурации клиента или спецификация вроде-L 0.0.0.0:3307:...заставляют проброс слушать и в вашей сети. В сети кафе это ваша база данных, выставленная на весь зал.-D 0.0.0.0:1080ещё хуже: открытый прокси в приватную сеть сервера. - Удалённые пробросы могут обойти ваш же firewall. Команда
-Rс ноутбука внутри офисной сети на сервер снаружи публикует внутренний сервис для всех, кто может достучаться до этого порта на сервере. Организации блокируют исходящий SSH именно по этой причине. Не пробрасывайте туннели наружу из сети, владелец которой был бы против. - Последний участок может идти открытым текстом.
-L 5433:10.0.0.20:5432шифрует трафик до SSH-сервера и не дальше. Для всего, что идёт через недоверенную сеть, используйте собственный TLS базы данных. - Проброс агента - это не проброс портов. Флаг
-Aоткрывает ваши ключи root-пользователю удалённого хоста и не нужен ни для чего из описанного выше. В статье про SSH-ключи объясняется, почемуProxyJumpего заменяет.
Честное сравнение - с простым выставлением сервиса наружу. База данных, открытая в интернет с TLS и надёжным паролем и доступная напрямую, - вполне законная схема, и что для неё нужно, описано в чек-листе безопасности баз данных. База на loopback, доступная только через SSH, полностью убирает вход в базу данных из поверхности атаки. Для администраторов и разовых инструментов туннель обычно лучший выбор, а для приложений, которым нужно подключаться весь день без процесса-надсмотрщика, обычно лучше прямое соединение.
Разбор ошибок, с которыми сталкиваются чаще всего#
`bind [127.0.0.1]:3306: Address already in use` и `Could not request local forwarding.` Что-то на вашей машине уже слушает этот порт: часто это локальная база данных или вчерашний туннель, всё ещё работающий в фоне. Выберите другой локальный порт или найдите старый процесс через ss -tlnp в Linux, lsof -iTCP:3306 -sTCP:LISTEN в macOS или netstat -ano | findstr 3306 в Windows.
`channel 2: open failed: connect failed: Connection refused`. С туннелем всё в порядке, отказало назначение. SSH-сервер попытался подключиться к хосту и порту из вашей спецификации, но там никто не слушал. Проверьте, что сервис запущен и работает на указанном вами адресе: база, привязанная к приватному адресу, откажет 127.0.0.1, а привязанная к loopback откажет приватному адресу.
`channel 2: open failed: administratively prohibited: open failed`. Политика сервера сказала нет: AllowTcpForwarding no, PermitOpen, в котором нет вашего назначения, или ключ с restrict без port-forwarding. Это ответ конфигурации, а не сети.
`Warning: remote port forwarding failed for listen port 8080`. Сервер не смог открыть порт для вашего -R: он занят (часто предыдущей сессией, у которой ещё не истёк тайм-аут), он ниже 1024, а вы не root, или PermitListen его исключает. Мёртвая сессия, удерживающая порт, - обычное дело для туннелей под присмотром; её убирает ClientAliveInterval на сервере.
Туннель подключается, но клиент зависает. Обычно клиент вообще не использует туннель: localhost идёт через путь к сокету или у GUI-инструмента есть собственная настройка SSH-туннеля в дополнение к вашей. Сначала проверьте через nc -vz 127.0.0.1 3307: если соединение проходит, туннель работает, а проблема в клиенте.
Туннель падает после некоторого простоя. Роутер или firewall между вами и сервером сбросил простаивающее TCP-соединение. Добавьте ServerAliveInterval 30 и поставьте под присмотр всё, что должно работать постоянно.
Где туннели вписываются в RE:NODE#
Каждая техника из этой статьи требует машины с sshd, которой вы управляете, а на RE:NODE это VDS: полный root-доступ, так что sshd_config, GatewayPorts и ограничения проброса вы настраиваете сами. В первом часе на новом VDS описано, как довести машину до состояния, когда следующий шаг - туннель.
Тарифы с базами данных устроены иначе, и туннель им вообще не нужен. Сервер PostgreSQL, MySQL, MongoDB, SQL Server или Valkey на хостинге баз данных доступен напрямую по хосту и порту, указанным в панели, с учётными данными, созданными для этого сервера, поэтому клиент подключается к нему как к любой удалённой базе: работу, которую в других случаях выполняет туннель, здесь делают TLS и надёжный пароль. Игровые серверы, серверы приложений и веб-серверы поставляются с SFTP на каждом сервере и консолью в панели, так что привычные причины открывать туннели к собственным машинам решаются там иначе.
FAQ#
SSH-туннель - это то же самое, что VPN?
Нет. Туннель пробрасывает конкретные TCP-порты или проксирует приложения, настроенные на -D, по одному SSH-соединению. VPN маршрутизирует весь трафик устройства на уровне IP, включая UDP и DNS. SSH умеет туннелирование уровня 3 через ssh -w и PermitTunnel, но это неудобно, и специализированный VPN справляется лучше.
Можно ли пробросить UDP через SSH?
Средствами проброса портов OpenSSH нельзя: он работает только с TCP. Обходные пути, которые оборачивают UDP в TCP через socat на каждом конце, существуют, но они добавляют задержку и ломают тайминг, на который рассчитаны протоколы реального времени, например игровой трафик. Для игрового сервера используйте специальный туннель или хостинг сервера, а не SSH.
Замедляет ли туннель запросы к моей базе данных?
Немного, и редко настолько, чтобы это имело значение. Шифрование на современных CPU стоит очень мало; реальные издержки - дополнительная задержка на пути и то, что все пробросы делят одно TCP-соединение. Для сессии администратора или ночного отчёта это незаметно. Для приложения, которое выполняет тысячи мелких запросов в секунду, лучше подключаться напрямую с TLS.
Какой порт использовать на моей стороне?
Любой свободный порт выше 1024. Настоящий порт сервиса плюс один (3307 для MySQL, 5433 для PostgreSQL) легко запомнить, и он не конфликтует с тем, что запущено локально. Номера портов на вашей стороне и на стороне сервера независимы друг от друга.
Можно ли использовать SSH-туннели в Windows?
Да. В Windows 10 и 11 есть клиент OpenSSH, и все команды отсюда работают в PowerShell или командной строке. В PuTTY та же возможность находится в разделе Connection, SSH, Tunnels с вариантами Local, Remote и Dynamic. Во многих GUI-инструментах для баз данных, включая DBeaver и MySQL Workbench, тоже есть встроенная вкладка SSH-туннеля.


