Візуальний гайд з SSH-тунелювання з прикладами та схемами

26.06.2025 7 хвилин Автор: Lady Liberty

SSH-тунелі — це потужний інструмент для безпечного з’єднання між локальними й віддаленими мережами. У цій статті ми пояснюємо, як працює SSH-тунелювання, що таке локальний (Local Forwarding), віддалений (Remote Forwarding) та динамічний (SOCKS-проксі) тунель. Замість складних термінів — інтуїтивні схеми, прості приклади та реальні кейси використання.

Візуальний гайд для новачків з прикладами, схемами та командами

SSH – це ще один приклад давньої технології, яка досі широко використовується. Цілком можливо, що вивчення кількох SSH-трюків у довгостроковій перспективі виявиться більш вигідним, ніж опанування десятка хмарних інструментів, які стануть застарілими наступного кварталу.

Одна з моїх улюблених частин цієї технології – це SSH-тунелі. Використовуючи лише стандартні інструменти та часто лише одну команду, ви можете досягти наступного:

  • Доступ до внутрішніх кінцевих точок VPC через публічний екземпляр EC2.

  • Відкрийте порт з локального хоста віртуальної машини розробки у браузері хоста.

  • Відкрийте будь-який локальний сервер з домашньої/приватної мережі для зовнішнього світу.

Але, незважаючи на те, що я щодня використовую SSH-тунелі, мені завжди потрібен деякий час, щоб зрозуміти правильну команду. Чи має це бути локальний чи віддалений тунель? Які прапорці? Це local_port:remote_port чи навпаки? Тож я вирішив нарешті розібратися в цьому, і в результаті вийшла серія лабораторних робіт та візуальна шпаргалка.

Передумови

SSH-тунелі призначені для підключення хостів через мережу, тому кожна лабораторна робота нижче, як і очікувалося, включає кілька “машин”. Однак, запуск кількох віртуальних машин на кожну лабораторну роботу може бути виснажливим, тому я використовував контейнери для імітації мережевих хостів. Таким чином, один сервер Linux з движком Docker (або подібним) може бути використаний для запуску всіх лабораторних робіт.

Для кожного прикладу потрібна дійсна пара ключів без парольної фрази на хості, яка потім монтується в контейнери для спрощення керування доступом. Якщо у вас її немає, у лабораторних роботах наведено приклад того, як її згенерувати.

Важливо: Демони SSH у контейнерах тут призначені виключно для освітніх цілей — контейнери в цій публікації призначені для представлення повноцінних «машин» з клієнтами та серверами SSH на них. Майте на увазі, що рідко буває гарною ідеєю розміщувати SSH-конструктор у реальних контейнерах!

Локальна переадресація портів

Починаючи з того, яким я користуюся найчастіше. Часто може бути служба, яка прослуховує localhost або приватний інтерфейс машини, до якої я можу підключитися лише через SSH через її публічну IP-адресу. І мені конче потрібен доступ до цього порту ззовні. Кілька типових прикладів:

  • Доступ до бази даних (MySQL, Postgres, Redis тощо) за допомогою зручного інструменту інтерфейсу користувача з вашого ноутбука.

  • Використання браузера для доступу до веб-застосунку, доступного лише для приватної мережі.

  • Доступ до порту контейнера з вашого ноутбука без його публікації у публічному інтерфейсі сервера.

Всі вищезазначені випадки використання можна вирішити за допомогою однієї sshкоманди:

ssh -L [local_addr:]local_port:remote_addr:remote_port [user@]sshd_addr

Прапорець -Lвказує на те, що ми починаємо локальне переадресування портів . Насправді це означає:

  • На вашому комп’ютері SSH-клієнт почне прослуховувати local_port(ймовірно, на localhost, але це залежить від обставин — перевірте GatewayPortsналаштування ).

  • Будь-який трафік на цей порт буде перенаправлено на remote_private_addr:remote_portкомп’ютер, до якого ви підключилися через SSH.

Ось як це виглядає на схемі:

Порада професіонала: Використовуйте ssh -f -N -Lдля запуску сеансу переадресації портів у фоновому режимі.

Лабораторна робота 1: Використання SSH-тунелів для локальної переадресації портів

Запустіть онлайн-майданчик

Лабораторна робота відтворює налаштування зі схеми вище. Спочатку нам потрібно підготувати сервер — машину з демоном SSH та простим веб-сервісом, що прослуховує 127.0.0.1:80:

$ docker buildx build -t server:latest -<<'EOD'
# syntax=docker/dockerfile:1
FROM alpine:3

# Install the dependencies:
RUN apk add --no-cache openssh-server curl python3
RUN mkdir /root/.ssh && chmod 0700 /root/.ssh && ssh-keygen -A

# Prepare the entrypoint that starts the daemons:
COPY --chmod=755 <<'EOF' /entrypoint.sh
#!/bin/sh
set -euo pipefail

for file in /tmp/ssh/*.pub; do
  cat ${file} >> /root/.ssh/authorized_keys
done
chmod 600 /root/.ssh/authorized_keys

# Minimal config for the SSH server:
sed -i '/AllowTcpForwarding/d' /etc/ssh/sshd_config
sed -i '/PermitOpen/d' /etc/ssh/sshd_config
/usr/sbin/sshd -e -D &

python3 -m http.server --bind 127.0.0.1 ${PORT} &

sleep infinity
EOF

# Run it:
CMD ["/entrypoint.sh"]
EOD

Запуск сервера та запис його IP-адреси:

$ yes no | ssh-keygen -t rsa -N "" -f ~/.ssh/id_iximiuz_lab

$ docker run -d --rm \
   -e PORT=80 \
   -v $HOME/.ssh:/tmp/ssh \
   --name server \
   server:latest

SERVER_IP=$(
  docker inspect \
    -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
  server
)

Оскільки веб-сервіс прослуховує локальний хост, він не буде доступний ззовні (тобто, в цьому конкретному випадку, з хост-системи):

$ curl ${SERVER_IP}
curl: (7) Failed to connect to 172.17.0.2 port 80: Connection refused

Але зсередини “сервера” все працює чудово:

$ ssh -i $HOME/.ssh/id_iximiuz_lab -o StrictHostKeyChecking=no \
  root@${SERVER_IP}
7b3e49181769:$# curl localhost
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
...

А ось у чому хитрість: прив’яжіть сервер localhost:80до хоста, localhost:8080використовуючи локальну переадресацію портів:

ssh -i "$HOME/.ssh/id_iximiuz_lab" \
-o StrictHostKeyChecking=no \
-f -N -L 8080:localhost:80 \
remote_addr

Тепер ви маєте змогу отримати доступ до веб-сервісу на локальному порту хост-системи:

$ curl localhost:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
...

Трохи більш детальний (але більш явний та гнучкий) спосіб досягнення тієї ж мети — використання local_addr:local_port:remote_addr:remote_portформи:

$ ssh -i $HOME/.ssh/id_iximiuz_lab -o StrictHostKeyChecking=no \
  -f -N -L localhost:8080:localhost:80 root@${SERVER_IP}

Локальна переадресація портів з хостом Bastion

Спочатку це може бути неочевидним, але ssh -Lкоманда дозволяє переадресовувати локальний порт на віддалений порт на будь-якій машині , а не лише на самому SSH-сервері. Зверніть увагу, що ` remote_addrand` sshd_addrможе мати однакове значення, а може й не мати:

ssh -L [local_addr:]local_port:remote_addr:remote_port [user@]sshd_addr

Не впевнений, наскільки правомірним є використання терміна “бастіонний хост” тут, але саме так я уявляю собі цей сценарій:

Я часто використовую вищезгаданий трюк для виклику кінцевих точок, доступних з бастіонного хоста , але не з мого ноутбука (наприклад, використовую екземпляр EC2 з приватним та публічним інтерфейсами для підключення до кластера OpenSearch, повністю розгорнутого в VPC).

Лабораторна робота 2: Локальна переадресація портів з хостом Bastion

Запустіть онлайн-майданчик

Знову ж таки, лабораторна робота відтворює налаштування зі схеми вище. Спочатку нам потрібно підготувати хост-бастіон — машину, на якій встановлено лише демон SSH:

$ docker buildx build -t bastion:latest -<<'EOD'
# syntax=docker/dockerfile:1
FROM alpine:3

# Install the dependencies:
RUN apk add --no-cache openssh-server
RUN mkdir /root/.ssh && chmod 0700 /root/.ssh && ssh-keygen -A

# Prepare the entrypoint that starts the SSH daemon:
COPY --chmod=755 <<'EOF' /entrypoint.sh
#!/bin/sh
set -euo pipefail

for file in /tmp/ssh/*.pub; do
  cat ${file} >> /root/.ssh/authorized_keys
done
chmod 600 /root/.ssh/authorized_keys

# Minimal config for the SSH server:
sed -i '/AllowTcpForwarding/d' /etc/ssh/sshd_config
sed -i '/PermitOpen/d' /etc/ssh/sshd_config
/usr/sbin/sshd -e -D &

sleep infinity
EOF

# Run it:
CMD ["/entrypoint.sh"]
EOD

Запуск хоста бастіону та запис його IP-адреси:

$ yes no | ssh-keygen -t rsa -N "" -f ~/.ssh/id_iximiuz_lab

$ docker run -d --rm \
    -v $HOME/.ssh:/tmp/ssh \
    --name bastion \
    bastion:latest

BASTION_IP=$(
  docker inspect \
    -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
  bastion
)

Тепер, запустивши цільовий веб-сервіс на окремій “машині”:

$ docker run -d --rm \
    --name server \
    python:3-alpine \
    python3 -m http.server 80

SERVER_IP=$(
  docker inspect \
    -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
  server
)

Уявимо, що дзвінок curl ${SERVER_IP}безпосередньо з хоста з якоїсь причини неможливий (наприклад, ніби немає маршруту від хоста до цієї IP-адреси). Отже, нам потрібно запустити переадресацію портів:

$ ssh -i $HOME/.ssh/id_iximiuz_lab -o StrictHostKeyChecking=no \
  -f -N -L 8080:${SERVER_IP}:80 root@${BASTION_IP}

Зверніть увагу, що змінні SERVER_IP та BASTION_IP мають різні значення у наведеній вище команді.

Перевірка його роботи:

$ curl localhost:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
...

Віддалене переадресування портів

Ще один популярний (але радше зворотний) сценарій — це коли ви хочете тимчасово надати локальний сервіс зовнішньому світу. Звичайно, для цього вам знадобиться публічний сервер вхідного шлюзу . Але не бійтеся! Будь-який публічний сервер з демоном SSH можна використовувати як такий шлюз:

ssh -R [remote_addr:]remote_port:local_addr:local_port [user@]gateway_addr

Вищезгадана команда виглядає не складнішою за свою ssh -Lаналогічну. Але є один підводний камінь…

За замовчуванням, вищезгаданий SSH-тунель дозволить використовувати лише локальний хост шлюзу як віддалену адресу. Іншими словами, ваш локальний порт стане доступним лише зсередини самого сервера шлюзу, і, ймовірно, це вам насправді не потрібно. Наприклад, я зазвичай хочу використовувати публічну адресу шлюзу як віддалену адресу, щоб надати доступ до моїх локальних служб публічному Інтернету. Для цього SSH-сервер потрібно налаштувати за допомогою цього GatewayPorts yesпараметра.

Ось для чого можна використовувати віддалену переадресацію портів:

  • Відкриття демонстраційного доступу до сервісу розробки з вашого ноутбука до загальнодоступного Інтернету.

  • Хм… Я можу придумати кілька езотеричних прикладів, але сумніваюся, що варто ними тут ділитися. Цікаво почути, для чого інші люди можуть використовувати віддалене переадресування портів!

Ось як виглядає віддалене переадресування портів на схемі:

Порада професіонала: Використовуйте ssh -f -N -Rдля запуску сеансу переадресації портів у фоновому режимі.

Лабораторна робота 3: Використання SSH-тунелів для віддаленого переадресування портів

Запустіть онлайн-майданчик

Лабораторна робота відтворює налаштування зі схеми вище. Спочатку нам потрібно підготувати «машину розробника» – комп’ютер із SSH-клієнтом та локальним веб-сервером:

$ docker buildx build -t devel:latest -<<'EOD'
# syntax=docker/dockerfile:1
FROM alpine:3

# Install dependencies:
RUN apk add --no-cache openssh-client curl python3
RUN mkdir /root/.ssh && chmod 0700 /root/.ssh

# Prepare the entrypoint that starts the web service:
COPY --chmod=755 <<'EOF' /entrypoint.sh
#!/bin/sh
set -euo pipefail

cp /tmp/ssh/* /root/.ssh
chmod 600 /root/.ssh/*

python3 -m http.server --bind 127.0.0.1 ${PORT} &

sleep infinity
EOF

# Run it:
CMD ["/entrypoint.sh"]
EOD

Запуск розробницької машини:

$ yes no | ssh-keygen -t rsa -N "" -f ~/.ssh/id_iximiuz_lab

$ docker run -d --rm \
    -e PORT=80 \
    -v $HOME/.ssh:/tmp/ssh \
    --name devel \
    devel:latest

Підготовка сервера вхідного шлюзу — простого SSH-сервера з GatewayPortsналаштуванням :yessshd_config

$ docker buildx build -t gateway:latest -<<'EOD'
# syntax=docker/dockerfile:1
FROM alpine:3

# Install the dependencies:
RUN apk add --no-cache openssh-server
RUN mkdir /root/.ssh && chmod 0700 /root/.ssh && ssh-keygen -A

# Prepare the entrypoint that starts the SSH server:
COPY --chmod=755 <<'EOF' /entrypoint.sh
#!/bin/sh
set -euo pipefail

for file in /tmp/ssh/*.pub; do
  cat ${file} >> /root/.ssh/authorized_keys
done
chmod 600 /root/.ssh/authorized_keys

sed -i '/AllowTcpForwarding/d' /etc/ssh/sshd_config
sed -i '/PermitOpen/d' /etc/ssh/sshd_config
sed -i '/GatewayPorts/d' /etc/ssh/sshd_config
echo 'GatewayPorts yes' >> /etc/ssh/sshd_config

/usr/sbin/sshd -e -D &

sleep infinity
EOF

# Run it:
CMD ["/entrypoint.sh"]
EOD

Запуск сервера шлюзу та запис його IP-адреси:

$ docker run -d --rm \
    -v $HOME/.ssh:/tmp/ssh \
    --name gateway \
    gateway:latest

GATEWAY_IP=$(
  docker inspect \
    -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
  gateway
)

Тепер зсередини машини розробника запустіть віддалену переадресацію портів :

$ docker exec -it -e GATEWAY_IP=${GATEWAY_IP} devel sh
/ $# ssh -i $HOME/.ssh/id_iximiuz_lab -o StrictHostKeyChecking=no \
  -f -N -R 0.0.0.0:8080:localhost:80 root@${GATEWAY_IP}
/ $# exit  # or detach with ctrl-p, ctrl-q

І перевірте, чи локальний порт машини розробника став відкритим на публічному інтерфейсі шлюзу (з хост-системи):

$ curl ${GATEWAY_IP}:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
...

Віддалене переадресування портів з домашньої/приватної мережі

Подібно до локального переадресації портів, віддалена переадресація портів має власний режим хоста-бастіону . Але цього разу машина з SSH-клієнтом (наприклад, ваш ноутбук розробника) грає роль бастіону. Зокрема, це дозволяє відкривати порти домашньої (або приватної) мережі, до якої ваш ноутбук має доступ, зовнішньому світу через вхідний шлюз:

ssh -R [remote_addr:]remote_port:local_addr:local_port [user@]gateway_addr

Виглядає майже ідентично простому віддаленому SSH-тунелю, але local_addr:local_portпара стає адресою пристрою в домашній мережі. Ось як це можна зобразити на діаграмі:

Оскільки я зазвичай використовую свій ноутбук як тонкий клієнт, а фактична розробка відбувається на домашньому сервері, я покладаюся на таку віддалену переадресацію портів, коли мені потрібно надати доступ до служби розробки з домашнього сервера до публічного Інтернету, і єдина машина з доступом до шлюзу — це мій тонкий ноутбук.

Лабораторна робота 4: Віддалене переадресування портів з домашньої/приватної мережі

Запустіть онлайн-майданчик

Як завжди, лабораторія відтворює налаштування зі схеми вище. Спочатку нам потрібно підготувати «тонку машину розробки»:

$ docker buildx build -t devel:latest -<<'EOD'
# syntax=docker/dockerfile:1
FROM alpine:3

# Install the dependencies:
RUN apk add --no-cache openssh-client
RUN mkdir /root/.ssh && chmod 0700 /root/.ssh

# This time we run nothing (at first):
COPY --chmod=755 <<'EOF' /entrypoint.sh
#!/bin/sh
set -euo pipefail

cp /tmp/ssh/* /root/.ssh
chmod 600 /root/.ssh/*

sleep infinity
EOF

# Run it:
CMD ["/entrypoint.sh"]
EOD

Запуск “розробничої машини”:

$ yes no | ssh-keygen -t rsa -N "" -f ~/.ssh/id_iximiuz_lab

$ docker run -d --rm \
    -v $HOME/.ssh:/tmp/ssh \
    --name devel \
    devel:latest

Запуск приватного сервера розробки з використанням окремої “машини” та запис його IP-адреси:

$ docker run -d --rm \
    --name server \
    python:3-alpine \
    python3 -m http.server 80

SERVER_IP=$(
  docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
  server
)

Підготовка сервера вхідного шлюзу:

$ docker buildx build -t gateway:latest -<<'EOD'
# syntax=docker/dockerfile:1
FROM alpine:3

# Install the dependencies:
RUN apk add --no-cache openssh-server
RUN mkdir /root/.ssh && chmod 0700 /root/.ssh && ssh-keygen -A

# Prepare the entrypoint that starts the SSH daemon:
COPY --chmod=755 <<'EOF' /entrypoint.sh
#!/bin/sh
set -euo pipefail

for file in /tmp/ssh/*.pub; do
  cat ${file} >> /root/.ssh/authorized_keys
done
chmod 600 /root/.ssh/authorized_keys

sed -i '/AllowTcpForwarding/d' /etc/ssh/sshd_config
sed -i '/PermitOpen/d' /etc/ssh/sshd_config
sed -i '/GatewayPorts/d' /etc/ssh/sshd_config
echo 'GatewayPorts yes' >> /etc/ssh/sshd_config

/usr/sbin/sshd -e -D &

sleep infinity
EOF

# Run it:
CMD ["/entrypoint.sh"]
EOD

І починаємо це:

$ docker run -d --rm \
    -v $HOME/.ssh:/tmp/ssh \
    --name gateway \
    gateway:latest

GATEWAY_IP=$(
  docker inspect \
    -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
  gateway
)

Тепер, зсередини “розробничої машини”, запустіть віддалену переадресацію портів SERVER-GATEWAY :

$ docker exec -it -e GATEWAY_IP=${GATEWAY_IP} -e SERVER_IP=${SERVER_IP} devel sh
/ $# ssh -i $HOME/.ssh/id_iximiuz_lab -o StrictHostKeyChecking=no \
  -f -N -R 0.0.0.0:8080:${SERVER_IP}:80 root@${GATEWAY_IP}
/ $# exit  # or detach with ctrl-p, ctrl-q

Нарешті, перевірте, чи став сервер розробки доступним у публічному інтерфейсі шлюзу (з хост-системи):

$ curl ${GATEWAY_IP}:8080
<!DOCTYPE HTML>
<html lang="en">
<head>
...

Підсумовуючи

Після виконання всіх цих лабораторних робіт та малюнків я помітив, що:

  • Слово «локальний» може означати як клієнтську машину SSH , так і хост вище за течією, доступний з цієї машини.

  • Слово «віддалений» може означати або SSH-сервер (sshd) вищестоящого хоста, доступного з нього.

  • Локальне переадресування портів ( ssh -L) означає, що саме sshклієнт починає прослуховувати новий порт.

  • Віддалене переадресування портів ( ssh -R) означає, що саме sshdсервер починає прослуховувати додатковий порт.

  • Мнемоніки — це “ssh -L l ocal:remote” та “ssh -R remote :local”, і новий порт завжди відкривається лівим рядком.

Сподіваюся, що вищезазначені матеріали трохи допомогли вам стати майстром SSH-тунелів.

Підписатися
Сповістити про
0 Коментарі
Найстаріші
Найновіше Найбільше голосів
Знайшли помилку?
Якщо ви знайшли помилку, зробіть скріншот і надішліть його боту.