SSH-туннель — это зашифрованный канал внутри существующего SSH-подключения, через который перенаправляется трафик с одного порта на другой. Три режима:
- LocalForward (-L): локальный порт → удалённая цель.
- RemoteForward (-R): удалённый порт на сервере → локальная цель.
- DynamicForward (-D): локальный SOCKS5-прокси, через который приложения выходят в сеть «через сервер».
Это не VPN: туннели точечные (порт/сервис), а не полноценный сетевой интерфейс.
Когда туннели реально нужны
- Доступ к БД/админке, доступной только из внутренней сети (-L).
- Показ локального dev-сервиса внешнему миру на короткое время (-R).
- «Выйти в интернет как с сервера» для отладки/geo-кейсов (-D).
- Обход NAT/CGNAT, работа через бастион, «не тянуть VPN ради одного порта».
Если SSH ещё не настроен — начните с базовой статьи: SSH в Linux.

Local port forward (-L): доступ к внутреннему ресурсу
Подключаемся к серверу, у себя получаем локальный порт, который «ходит» к внутреннему сервису через SSH.
Пример без бастиона:
ssh -L [LOCAL_ADDR:]LPORT:TARGET_HOST:TPORT user@SSH_HOST
# часто — так:
ssh -L 127.0.0.1:5432:db.internal:5432 user@bastion
Пояснения:
127.0.0.1
— безопасный адрес привязки (binding-адрес), чтобы никто извне не подключился к туннелю.TARGET_HOST:TPORT
— реальная внутренняя цель, видимая с точки зренияSSH_HOST
(например,db.internal:5432
).- Добавьте
-N
(не запускать шелл) и-f
(в фон):
ssh -N -f -L 127.0.0.1:5432:db.internal:5432 user@bastion
Проверка:
psql -h 127.0.0.1 -p 5432 -U app dbname # пример для PostgreSQL
# или
curl -v http://127.0.0.1:8080/health
Через бастион (-J):
ssh -J user@bastion user@internal -L 127.0.0.1:5432:db.internal:5432 -N -f
Чтобы не печатать длинные команды и управлять туннелем коротким алиасом, вынесите параметры в профиль ~/.ssh/config
. Тогда туннель поднимается командой ssh -N -f bastion
и закрывается ssh -O exit bastion
.
Host bastion
HostName bastion.example.com
User user
ServerAliveInterval 30
ServerAliveCountMax 3
ControlMaster auto
ControlPersist 4h
ExitOnForwardFailure yes
LocalForward 127.0.0.1:5432 db.internal:5432
Что дальше: см. рецепты PostgreSQL через бастион и VNC через SSH (для RDP меняем порт на 3389).
Remote port forward (-R): показать локальный сервис наружу
Вы за NAT/фаерволом, но хотите временно показать свой локальный порт (например, dev-сайт на localhost:3000
) через удалённый сервер (VPS).
Синтаксис (клиент):
ssh -R [RADDR:]RPORT:LOCAL_HOST:LPORT user@VPS
# пример: публикуем локальный 3000 на VPS:8080
ssh -R 127.0.0.1:8080:127.0.0.1:3000 user@vps.example.com
Важные детали.
- По умолчанию (на большинстве конфигов)
-R
слушает только на loopback сервера. - Чтобы открыть порт на внешнем интерфейсе VPS, нужна серверная опция
GatewayPorts
:
GatewayPorts clientspecified
— доверять адресу из клиента (0.0.0.0 / ::).
ИлиGatewayPorts yes
— слушать на всех адресах. - Разрешите форвардинг на сервере:
AllowTcpForwarding yes
(илиremote
).
Пример: публичный 8080 на VPS
1. На VPS: проверьте sshd_config
:
AllowTcpForwarding remote
GatewayPorts clientspecified
Примените: sudo sshd -t && sudo systemctl reload sshd
2. На клиенте:
ssh -R 0.0.0.0:8080:127.0.0.1:3000 user@vps.example.com -N -f
3. Откройте фаервол на VPS (если нужно):
# Debian-based (UFW):
sudo ufw allow 8080/tcp && sudo ufw reload
# RHEL-based (firewalld):
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload
# Arch: UFW или firewalld — по аналогии.
Проверяйте доступ извне (например, нашим онлайн-сканером портов или curl http://VPS:8080
с внешнего хоста).
Безопасность: если публикуете наружу, используйте временные учётки, ограничение Match User
и по возможности оставляйте RADDR
= 127.0.0.1
+ делайте доступ через реверс-прокси/VPN. Публичный 0.0.0.0
нужен только осознанно и ненадолго.
Что дальше: см. рецепт Показ локального dev на VPS (-R).
Dynamic SOCKS (-D): гибкий прокси через SSH
Поднимаем локальный SOCKS5-порт, через который приложения выходят «как с сервера».
Команда:
ssh -D 1080 -N user@server
Проверка в CLI:
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me
Браузер/приложения. Укажите SOCKS-прокси 127.0.0.1:1080
. Используйте режим socks5-hostname
, чтобы DNS-запросы шли через прокси, а не локально. Помните, что не все программы умеют SOCKS.
Что дальше: см. рецепт SOCKS/прокси через SSH.
Через бастион и мульти-хопы
Совмещайте -L/-R/-D
с бастионом:
# local forward через бастион
ssh -J user@bastion user@internal -L 127.0.0.1:5432:db.internal:5432 -N -f
# SOCKS через бастион
ssh -J user@bastion -D 1080 -N user@internal
Альтернатива старой школы — ProxyCommand -W %h:%p
, но -J
проще.
Полезные опции клиента и конфиг
В ~/.ssh/config
удобно держать всё, что нужно ежедневно:
Host vps
HostName vps.example.com
User user
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
ControlMaster auto
ControlPersist 4h
# Примеры пробросов:
# LocalForward 127.0.0.1:5432 db.internal:5432
# RemoteForward 127.0.0.1:8080 127.0.0.1:3000
# DynamicForward 1080
ExitOnForwardFailure yes
— упасть, если форвард не встал.ServerAlive*
— держать соединение живым; помогает autossh/systemd.ControlMaster/ControlPersist
— мультиплексирование, меньше RTT при множестве команд.
Политика безопасности на сервере (sshd_config)
Ограничивайте возможности туннелей на стороне сервера — глобально или выборочно (через Match
):
# Разрешить только нужное
AllowTcpForwarding local|remote|yes|no
# Управлять доступностью удалённых адресов/портов (для -L и, в новых версиях, -R)
# PermitOpen host:port
# (в свежих OpenSSH также есть PermitListen для -R — проверьте `sshd -T`)
# Публикация удалённого порта наружу
GatewayPorts no|clientspecified|yes
# Блокируем лишнее
X11Forwarding no
AllowAgentForwarding no
# Ограничения по пользователям/группам
Match User dev
AllowTcpForwarding remote
PermitOpen 127.0.0.1:3000
Перед применением —
sudo sshd -t
. Если директивы не поддерживаются вашей версией OpenSSH, они засветятся ошибкой; посмотрите, что реально активно:sudo sshd -T | grep -i forward
.
Диагностика и типичные ошибки
channel 0: open failed: administratively prohibited
— сервер запрещает форвардинг (AllowTcpForwarding no
) или не разрешён нужный хост/порт (PermitOpen
).bind: Address already in use
— порт уже занят (проверьтеss -lntp
).connect to 127.0.0.1 port N: Connection refused
— целевой сервис не слушает или неверная цель (TARGET_HOST:TPORT
).Broken pipe
/ обрывы — добавьтеServerAliveInterval/CountMax
, посмотрите сеть/фаервол.- Снаружи «не видно» при -R — откройте порт на VPS-фаерволе; при необходимости используйте наш онлайн-сканер портов.
- Смотрите логи:
ssh -vvv user@host # подробности с клиента
sudo journalctl -u sshd -b # серверные логи
ss -lntp; lsof -i -nP # кто слушает/куда подключаемся
Автоподнятие и восстановление туннеля
Вариант A — autossh (просто и надёжно):
# Local forward
autossh -M 0 -N -f -L 127.0.0.1:5432:db.internal:5432 user@bastion \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes
# Remote forward
autossh -M 0 -N -f -R 0.0.0.0:8080:127.0.0.1:3000 user@vps \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes
Вариант B — systemd-юнит (без autossh): /etc/systemd/system/ssh-tunnel.service
[Unit]
Description=Persistent SSH tunnel (-R 8080 -> localhost:3000)
After=network-online.target
Wants=network-online.target
[Service]
User=tunnel # выделенный пользователь
ExecStart=/usr/bin/ssh -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes -R 0.0.0.0:8080:127.0.0.1:3000 user@vps.example.com
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel
Рекомендуется выделить отдельного пользователя/ключ, ограничить права Match
-блоком и PermitOpen/GatewayPorts
.
Unix-сокеты и нестандартные кейсы
SSH поддерживает проброс UNIX-сокетов (StreamLocalForward):
# Локально создаём сокет .pg.sock, который идёт к /var/run/postgresql/.s.PGSQL.5432 на сервере
ssh -L ~/pg.sock:/var/run/postgresql/.s.PGSQL.5432 user@db-host -N
Следите за правами на сокеты и учитывайте, что некоторые менеджеры (Docker) не любят такой проброс в проде.
Готовые рецепты (скопируй и пользуйся)
VNC через SSH
Задача A. Подключиться к удалённому VNC-серверу, не открывая 5900 наружу
1. Убедитесь, что на удалённой машине VNC действительно слушает (часто только на loopback):
# на удалённом хосте
ss -lntp | grep 59
# примеры запуска:
tigervncserver :1 -localhost yes # порт 5901
# или для :0 будет 5900
2. Поднимите локальный туннель SSH (без шелла):
ssh -N -f -L 5901:127.0.0.1:5901 user@REMOTE
3. Откройте свой VNC-клиент и подключитесь к localhost:5901
- macOS:
open vnc://localhost:5901
- Linux:
vncviewer localhost:5901
(TigerVNC / Remmina и т. п.) - Windows (PuTTY): Connection → SSH → Tunnels → Source port: 5901, Destination: 127.0.0.1:5901 → Add → Open; затем в VNC-клиенте —
localhost:5901
.
Почему безопасно: VNC по умолчанию не шифрует трафик;
-L
шифрует соединение и держит сам VNC слушающим только на127.0.0.1
.
Задача B. Показать локальный VNC наружу через свой VPS (редко, осознанно)
1. На VPS в sshd_config
разрешите публикацию порта:
AllowTcpForwarding remote
GatewayPorts clientspecified
sudo sshd -t && sudo systemctl reload sshd
2. На своей машине поднимите обратный туннель:
ssh -N -f -R 0.0.0.0:5901:127.0.0.1:5901 user@VPS
Теперь к вашему локальному VNC можно подключаться на VPS:5901 (лучше ограничить IP на уровне фаервола VPS).
Этот вариант потенциально опаснее: включайте его временно, с ограничениями (firewalld/UFW,
Match User
, возможен реверс-прокси с ACL).
Полезно знать
- Порты VNC:
:0 → 5900
,:1 → 5901
,:2 → 5902
... - Завершить туннель: если используете ControlMaster-профиль —
ssh -O exit <alias>;
иначеpkill -f 'ssh -N -f -L 5901'
.
# Примечание про RDP (Windows):
# тот же приём -L, только порт 3389
ssh -N -f -L 127.0.0.1:3389:WIN_HOST:3389 user@bastion
# через ProxyJump:
ssh -J user@bastion user@internal -N -f -L 127.0.0.1:3389:WIN_HOST:3389
# затем подключайтесь RDP-клиентом к localhost:3389 (MSTSC/Remmina/MRD)
PostgreSQL через бастион (-L + -J)
Задача. Получить доступ к БД db.internal:5432
, которая видна только из внутренней сети сервера.
1. Проверка с бастиона (доступ к цели):
# на бастионе
nc -vz db.internal 5432 || ss -n 'dport = :5432' # есть ли коннект
# (если есть psql)
psql -h db.internal -p 5432 -U app -c 'select 1;' || true
2. Поднять туннель с ноутбука:
ssh -J user@bastion.example.com user@internal.example.com \
-N -f -o ExitOnForwardFailure=yes \
-L 127.0.0.1:5432:db.internal:5432
3. Подключиться локально:
psql -h 127.0.0.1 -p 5432 -U app dbname
Зачем 127.0.0.1
? Чтобы локальный порт был доступен только вам.
IPv6: цель можно писать в скобках: -L 127.0.0.1:5432:[2001:db8::10]:5432
.
Удобно вынести в ~/.ssh/config
:
Host db-tunnel
HostName bastion.example.com
User user
ProxyJump user@bastion.example.com
ExitOnForwardFailure yes
ServerAliveInterval 30
ServerAliveCountMax 3
LocalForward 127.0.0.1:5432 db.internal:5432
Зачем это нужно: чтобы не печатать длинные команды. С профилем достаточно
ssh -N -f db-tunnel
(поднять) иssh -O exit db-tunnel
(закрыть).
Показ локального dev на VPS (-R):
Задача. Вы за NAT, хотите показать http://localhost:3000
наружу как http://VPS:8080
.
1. На VPS в /etc/ssh/sshd_config
:
AllowTcpForwarding remote
GatewayPorts clientspecified # или yes (шире)
Применить:
sudo sshd -t && sudo systemctl reload sshd
2. С ноутбука поднять обратный туннель:
ssh -N -f -o ExitOnForwardFailure=yes \
-R 0.0.0.0:8080:127.0.0.1:3000 user@vps.example.com
3. Открыть фаервол на VPS:
# UFW (Debian/Ubuntu):
sudo ufw allow 8080/tcp && sudo ufw reload
# firewalld (RHEL/Alma/Rocky/Fedora):
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload
4. Проверка снаружи:
curl -I http://vps.example.com:8080/
Безопаснее вариант. Слушать только на loopback VPS:
ssh -N -f -R 127.0.0.1:8080:127.0.0.1:3000 user@vps
И отдать наружу через Nginx (ACL/базовая auth/HTTPS).
Ограничения: сделайте отдельного юзера и Match
-политику:
Match User devpub
AllowTcpForwarding remote
PermitOpen 127.0.0.1:3000
Локальный браузер «через сервер» (-D SOCKS)
Задача. Отправлять трафик приложений через сервер (гео/отладка).
1. Поднять SOCKS-прокси:
ssh -D 1080 -N -f user@server.example.com
2. Проверка DNS и выхода:
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me
Используйте именно --socks5-hostname
, чтобы DNS-запросы шли через прокси.
3. Браузер: укажите SOCKS5 127.0.0.1:1080
.
CLI: можно выставить переменные:
export ALL_PROXY=socks5h://127.0.0.1:1080
export NO_PROXY=localhost,127.0.0.1
Через бастион:
ssh -J user@bastion user@internal -D 1080 -N -f
Закрыть:
pkill -f 'ssh -D 1080'
# или, если запускали алиасом:
ssh -O exit socks-tunnel
Чек-лист перед продакшеном
- Вход по ключам + (по ситуации) 2FA включены.
- Фаервол/ACL согласованы (нужные порты на нужных узлах).
- В
sshd_config
настроеныAllowTcpForwarding
,GatewayPorts
,PermitOpen/PermitListen
(если поддерживается). - Туннель стартует с
ExitOnForwardFailure
, живёт сServerAlive*
. - Логи на контроле, алиасы в
~/.ssh/config
работают.

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