Когда мы передаём запросы к базам данных, команды управления или вызовы внутренних API через публичную сеть, пакеты проходят через множество неизвестных маршрутизаторов. Передача в открытом виде означает, что любой посредник может не только просмотреть содержимое, но и незаметно его изменить. Именно здесь раскрывается ценность SSH-туннелей: они используют мощное шифрование и аутентификацию, уже встроенные в протокол SSH, чтобы поверх недоверенной сети построить логически «частный» канал, и трафик между двумя серверами идёт так, будто они находятся в одной защищённой локальной сети. По сравнению с VPN преимущество SSH-туннеля в том, что не требуется сложная настройка сетевого уровня — достаточно уже работающего SSH-сервиса, чтобы быстро поднять зашифрованное соединение точка-точка.
В предыдущей статье «Репликация баз данных: как заставить данные жить в нескольких местах одновременно» мы подробно разбирали модели master-slave и master-master, синхронный и асинхронный режимы, задержку репликации и автоматическое переключение при отказе. Тогда мы оставили важный вопрос за кадром: а безопасен ли сам трафик репликации в публичной сети? Встроенный протокол репликации MySQL по умолчанию не шифруется, и передача binlog между двумя серверами может быть перехвачена. SSH-туннель — это прямой способ решить проблему: инкапсулировав порт репликации в зашифрованный туннель, можно гарантировать конфиденциальность и целостность данных даже при передаче через интернет. В этой статье мы пройдём путь от теории к практике.
Теоретическая часть: как работает SSH-туннель
Суть SSH-туннеля (его также называют пробросом портов) состоит в инкапсуляции трафика одного сетевого протокола внутрь SSH-соединения. SSH-клиент слушает локальный порт; когда на этот порт приходит соединение, клиент отправляет трафик через уже установленный зашифрованный SSH-канал на удалённый сервер, а тот, в свою очередь, перенаправляет его по целевому адресу. В зависимости от направления передачи данных выделяют два основных режима.
Локальный проброс (Local Forwarding, -L) — на локальной машине (той, что инициирует SSH) открывается порт, трафик через SSH-соединение отправляется на удалённый сервер, а тот перенаправляет его по целевому адресу. Типичный сценарий: рабочей машине нужен доступ к базе данных, находящейся в глубине внутренней сети, но прямого маршрута к ней нет. Вы подключаетесь по SSH к машине-бастиону, которая видит внутреннюю сеть, и открываете на локальной машине порт: всё, что отправляется на localhost:3306, в зашифрованном виде уходит на бастион, а тот перенаправляет трафик на настоящую базу данных.
Удалённый проброс (Remote Forwarding, -R) — порт открывается на удалённой машине (той, к которой подключаются по SSH). Трафик, пришедший на этот порт, по SSH-соединению отправляется обратно на локальную машину и уже там перенаправляется по целевому адресу. Типичный сценарий: внутреннему серверу нужно опубликовать сервис наружу, но Межсетевой экран брандмауэр не разрешает входящие подключения. Машина внутренней сети сама инициирует SSH-соединение к внешнему серверу и открывает на нём порт; когда внешний пользователь обращается к этому порту, трафик по SSH-туннелю возвращается к внутреннему сервису.
Для сценария репликации баз данных между серверами чаще используется локальный проброс: на реплике (slave) строится SSH-туннель к мастеру (master), и экземпляр MySQL на реплике подключается к мастеру через локальный зашифрованный порт.
Усиление безопасности: сам туннель тоже должен быть защищён
Если SSH-туннель использует слабые пароли или устаревшие алгоритмы шифрования, его безопасность становится фикцией. Прежде чем строить туннель, необходимо укрепить сам SSH-сервис. Ключевые меры: отключить аутентификацию по паролю, разрешив вход только по открытым ключам; запретить прямой вход под root, обязав использовать обычного пользователя с sudo; отдавать предпочтение ключам Ed25519 или ECDSA — они безопаснее и производительнее RSA. Кроме того, если ключ используется только для конкретного проброса, в authorized_keys можно применить опции command= и permitopen=, ограничив его только определёнными операциями и минимизировав последствия компрометации ключа.
Для туннелей, которые должны работать постоянно, случайное завершение процесса или обрыв сети приведут к недоступности сервиса. autossh — лёгкий инструмент-демон, который следит за состоянием SSH-соединения и автоматически переподключается при разрыве. В связке с systemd-сервисом можно добиться автозапуска при загрузке и автоматического подъёма после сбоя. Важно: при запуске SSH из systemd не используйте параметр -f, потому что -f переводит SSH в фоновый режим, и systemd не сможет корректно отслеживать состояние процесса.
Практика: строим зашифрованный туннель для репликации базы данных
Предположим, есть два сервера: мастер 10.0.1.10 (порт MySQL 3306) и реплика 10.0.2.20. Цель — на реплике открыть локальный порт 13306: всё, что отправляется на этот порт, после SSH-шифрования доходит до порта 3306 мастера. В конфигурации MySQL на реплике master_host будет указан как 127.0.0.1, а master_port — как 13306.
Шаг 1: генерируем выделенный ключ на реплике (если его ещё нет)
ssh-keygen -t ed25519 -f ~/.ssh/tunnel_key -N ""Публичный ключ размещаем в ~/.ssh/authorized_keys на мастере. Чтобы ограничить ключ только пробросом, в authorized_keys на мастере перед этим ключом добавляем ограничения:
command="/bin/false",no-pty,no-agent-forwarding,no-X11-forwarding,permitopen="127.0.0.1:3306" ssh-ed25519 AAAA...Это означает: ключ не может выполнять shell, не получает терминал и может перенаправлять трафик только на локальный порт 3306.
Шаг 2: проверяем туннель вручную
На реплике выполняем:
ssh -i ~/.ssh/tunnel_key -N -L 127.0.0.1:13306:127.0.0.1:3306 tunneluser@10.0.1.10-N означает «не выполнять удалённую команду, только проброс». Не закрывая этот терминал, открываем второй и проверяем:
mysql -h 127.0.0.1 -P 13306 -u repl_user -p -e "SELECT 1;"Если подключение прошло успешно, значит туннель работает.
Шаг 3: настраиваем systemd для постоянной работы
Создаём /etc/systemd/system/mysql-tunnel.service:
[Unit]
Description=SSH Tunnel for MySQL Replication
After=network-online.target
Wants=network-online.target
[Service]
User=mysql
ExecStart=/usr/bin/ssh -i /home/mysql/.ssh/tunnel_key -N \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=yes \
-L 127.0.0.1:13306:127.0.0.1:3306 \
tunneluser@10.0.1.10
Restart=always
RestartSec=10
StartLimitIntervalSec=0
[Install]
WantedBy=multi-user.targetПояснение ключевых параметров: ExitOnForwardFailure=yes гарантирует, что при неудачной привязке порта SSH сразу завершится, и systemd заметит это и перезапустит сервис; ServerAliveInterval и ServerAliveCountMax служат для обнаружения «зависших» соединений; StartLimitIntervalSec=0 снимает ограничение частоты перезапусков, чтобы systemd не сдался в экстремальных условиях.
Включаем и запускаем сервис:
sudo systemctl daemon-reload
sudo systemctl enable --now mysql-tunnelШаг 4: настраиваем репликацию MySQL на использование порта туннеля
В конфигурации MySQL на реплике (или через команду CHANGE MASTER TO) указываем адрес локального туннеля:
CHANGE MASTER TO
MASTER_HOST='127.0.0.1',
MASTER_PORT=13306,
MASTER_USER='repl_user',
MASTER_PASSWORD='...',
MASTER_SSL=0;
START SLAVE;Обратите внимание: MASTER_SSL=0 здесь безопасно, поскольку SSH-туннель уже обеспечивает шифрование на транспортном уровне, и накладывать ещё и SSL на уровне MySQL не требуется.
Проверка и устранение неполадок
После установки туннеля проверку выполняем на двух уровнях. Сначала проверяем процесс туннеля и состояние порта:
sudo systemctl status mysql-tunnel
sudo ss -tlnp | grep 13306Затем проверяем саму репликацию MySQL:
SHOW SLAVE STATUS\GСмотрим, что Slave_IO_Running и Slave_SQL_Running оба равны Yes, а Seconds_Behind_Master находится в разумных пределах.
Если репликация прервалась, первым делом убеждаемся, что туннель жив. Частая ловушка: процесс туннеля работает, порт слушается, но фактическое соединение уже разорвано (таймаут NAT, промежуточное устройство сбросило таблицу состояний и т.п.). В этом случае ss всё ещё показывает порт в режиме прослушивания, но соединение не устанавливается. Параметры ServerAliveInterval как раз и нужны для того, чтобы SSH-клиент активно проверял состояние и обнаруживал такие ситуации; вместе с ExitOnForwardFailure и Restart=always в systemd это позволяет довольно быстро восстановиться.
Заключение
SSH-туннель — не самая «модная» технология, но исключительно практичный выбор: не нужно внедрять новый стек программного обеспечения, не нужно менять сетевую архитектуру — достаточно OpenSSH, который есть на каждом Linux-сервере, чтобы за несколько минут построить канал с надёжным шифрованием и аутентификацией. Для репликации баз данных, вызовов внутренних API, публикации панелей управления и подобных сценариев он обеспечивает немедленно доступный уровень защиты. Настоящая сложность не в самом туннеле, а в усилении SSH на обоих концах, в мониторинге жизнеспособности процесса и в сквозной проверке цепочки репликации. Если эти три вещи сделаны добротно, зашифрованный коридор будет стабильно пропускать самые чувствительные потоки данных.
