Когда сервис живёт сразу в двух облаках - скажем, боевая база в одном провайдере, а фронтенды в другом, - возникает бытовая на первый взгляд задача: серверы должны видеть друг друга по приватным адресам, как будто стоят в одной стойке. Публичные IP с белыми списками быстро превращаются в неподдерживаемый список исключений, а сложные корпоративные решения требуют времени и денег. Wireguard решает задачу элегантно: один модуль ядра, один конфигурационный файл на сервер, криптография из коробки и минимум движущихся частей. В статье разбираем реальную постройку mesh-сети между двумя облаками: адресация, ключи, маршрутизация, NAT, keepalive и отказоустойчивость.
План адресации и построение ключей
Любая оверлейная сеть начинается с плана адресов. Практичная схема: выделяется отдельный диапазон, который гарантированно не пересекается с внутренними сетями обоих провайдеров, например 10.99.0.0/24. Каждый сервер получает фиксированный адрес из него: app-01 в облаке A это 10.99.0.11, db-01 в облаке B это 10.99.0.21. Адресации ведутся в инвентаре инфраструктурного репозитория, чтобы имя хоста, приватный облачный адрес и overlay-адрес были в одном месте.
Ключи генерируются на каждом сервере локально, приватный ключ никогда не покидает машину:
umask 077
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
Публичные ключи складываются в инфраструктурный репозиторий (ansible facts или terraform outputs) - они не секрет, их легко хранить рядом с кодом. Раскатка конфигов делается любой системой раскатки, в простейшем виде - loop в ansible по группе хостов с template wg0.conf.j2.
Минимальный конфиг /etc/wireguard/wg0.conf для сервера, который и принимает подключения, и сам инициирует:
[Interface]
Address = 10.99.0.11/24
ListenPort = 51820
PrivateKey = <private-key>
[Peer]
PublicKey = <peer-public-key>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.99.0.21/32
PersistentKeepalive = 25
Параметры здесь говорящие. ListenPort можно менять, но UDP/51820 это общепринятый дефолт. Endpoint - публичный адрес противоположной стороны; у сервера за NAT он может отсутствовать, тогда этот peer только инициирует. AllowedIPs на стороне интерфейса работает сразу как маршрутизация и как policy: какие адреса принимаются от этого пира и куда через него направляются. PersistentKeepalive 25 секунд держит открытым NAT-проход на домашних и облачных middlebox, иначе дырка в NAT протухнет, и входящие пакеты перестанут доходить.
Маршрутизация подсетей через выделенный шлюз
Mesh "каждый с каждым" удобен на трёх-пяти серверах. Когда серверов десятки, практичнее выделить в каждом облаке шлюз, который маршрутизирует трафик между оверлеем и внутренней сетью провайдера. Тогда хосты внутри облака даже не знают о wireguard: они просто получают статический маршрут "сеть 10.99.0.0/24 лежит через шлюз X".
На шлюзе включается форвардинг: net.ipv4.ip_forward=1 в /etc/sysctl.d/99-forward.conf и sysctl --system. В конфиге wireguard шлюза AllowedIPs у пира расширяется: endpoint облака B объявляет не только свой overlay-адрес, но и подсеть облака B, например AllowedIPs = 10.99.0.0/24, 10.20.0.0/16. На остальных хостах облака A в конфигах AllowedIPs симметрично добавляет подсеть облака B, а провайдерские таблицы маршрутизации (route table в AWS, routes у Hetzner) получают запись, что 10.20.0.0/16 ходит через шлюз.
Тонкость с MTU: wireguard добавляет 80 байт заголовков к пакету, и при стандартном MTU 1500 на транспорте полезная величина внутри туннеля это 1420. В конфиге интерфейса явно пишут MTU = 1420, иначе на некоторых провайдерах с дальнейшими инкапсуляциями (VLAN, VXLAN внутри облака) крупные пакеты начинают молча пропадать - классический симптом: пинг проходит, SSH подключается, а большая передача данных виснет. В патологических случаях встречается и необходимость MTU 1380 или 1360, это лечится именно явной установкой, а не надеждой на автоматику.
Запуск, systemd и повседневная эксплуатация
Из коробки есть wg-quick - обёртка, которая поднимает интерфейс с маршрутами одной командой: systemctl enable --now wg-quick@wg0. Проверка работы: wg show выводит список пиров, время последнего handshake, переданные байты. Здоровый признак: latest handshake не старше пары минут при включённом keepalive, transfer растёт в обе стороны. Если handshake пустой, смотрят по порядку: совпадают ли публичные ключи пиров, открыт ли UDP-порт в security group и в гостевом firewall, жив ли endpoint.
Для диагностики есть команда wg showconf wg0, которая показывает текущую конфигурацию интерфейса, и tcpdump -i eth0 udp port 51820 на предмет того, доходят ли вообще зашифрованные пакеты. Если пакеты доходят, а handshake не происходит, почти всегда это несовпадение ключей или кривой AllowedIPs.
Обновление ключей и добавление пиров не требуют перезапуска интерфейса: wg set wg0 peer <pubkey> allowed-ips 10.99.0.31/32 добавляет пира на лету, а wg-quick save или синхронизация конфига через ansible фиксирует изменение персистентно. Такая живая модификация удобна для автомасштабирования: новый инстанс при bootstrap сам регистрирует свой публичный ключ у координатора, и через секунды он уже в сети.
Именно поэтому связку часто дополняют управляющими слоями: headscale, Netmaker или простой ansible-скрипт, который собирает публикей из inventory и генерирует конфиги. На десятке машин ручной mesh терпим, на сотнях уже нужен координатор. Критерий простой: пока число пиров укладывается в голову, удерживайте mesh плоским; как только начинаете путаться, кто с кем соединён - пора вводить шлюзы и централизованную выдачу конфигов.
Производительность, шифрование и ограничения модели
Wireguard работает в ядре, и нагрузка по шифрованию ChaCha20-Poly1305 переваривается современными процессорами на гигабитных каналах без напряжения. Одно ядро CPU спокойно отдаёт 1-2 Гбит/с туннельного трафика. Задержка добавляет транзитное время между дата-центрами: если облако A в Германии, а облако B в Финляндии, RTT между ними останется какой был по интернету, wireguard тут ничего не сокращает. Поэтому чувствительные к latency взаимодействия (синхронная репликация БД) через континенты планируют с учётом физики: замеряют ping между регионами до проектирования, а не после.
Модель отказа тоже стоит понимать. Wireguard сам по себе не делает failover: если шлюз в облаке A упал, трафик через него остановился. Для отказоустойчивости поднимают два шлюза в каждом облаке с keepalived или просто вторым маршрутом с худшей метрикой. Тогда обрыв одного пути перестраивает трафик на второй за секунды.
Ещё одно ограничение - отсутствие встроенной аутентификации пользователей. Wireguard оперирует машинами и ключами, а не людьми и логинами. Для доступа сотрудников с ноутбуков к этой же mesh-сети применяют тот же протокол, но с выделенным сервером доступа, где у каждого сотрудника отдельный peer, и компрометация ноутбука лечится удалением его публичного ключа из конфига.
Полный рабочий пример конфигурации двух облаков
Соберём сценарий целиком. Облако A: сервера app-01 (10.99.0.11) и gw-a (10.99.0.1), внутренняя сеть 10.10.0.0/16. Облако B: db-01 (10.99.0.21) и gw-b (10.99.0.2), внутренняя сеть 10.20.0.0/16. Конфиг gw-a:
[Interface]
Address = 10.99.0.1/24
ListenPort = 51820
PrivateKey = ...
MTU = 1420
PostUp = sysctl -w net.ipv4.ip_forward=1
[Peer]
PublicKey = <gw-b-pub>
Endpoint = <public-ip-gw-b>:51820
AllowedIPs = 10.99.0.2/32, 10.20.0.0/16
PersistentKeepalive = 25
[Peer]
PublicKey = <app-01-pub>
AllowedIPs = 10.99.0.11/32
[Peer]
PublicKey = <db-01-pub>
AllowedIPs = 10.99.0.21/32
Конфиг app-01 симметричен: пиры это оба шлюза, AllowedIPs 10.99.0.0/24 плюс 10.20.0.0/16 через gw-a... точнее, идёт через те пиры, у которых эти сети объявлены. Подобные каркасы почти не меняются от проекта к проекту; меняются адреса и ключи.
Итоговая картина: wireguard между облаками это один из тех инструментов, где маленький конфиг покупает большую надёжность. Зашифрованные приватные адреса между провайдерами, маршруты на уровне шлюзов, keepalive для NAT, явный MTU и хотя бы два шлюза на площадку - и единая сеть живёт годами без обслуживания, а инциденты сводятся к "переключите трафик на второй шлюз", что занимает минуту.
Гигиена ключей и периодическая ротация
Долгоживущая сеть требует дисциплины с ключами. Приватные ключи лежат только на своих машинах с правами 0600, владелец root, и нигде больше: ни в git, ни в ansible vault в открытом виде, ни в комментариях задач. Если требуется хранить бэкап приватных ключей, это делается в менеджере секретов с шифрованием и аудитом доступа. Публичные ключи, наоборот, можно свободно хранить в инфраструктурном репозитории - без приватного они бесполезны.
Ротация ключей на практике делается не "раз в год по календарю", а по событию: увольнение человека с доступом к инфраструктуре, инцидент безопасности, пересоздание шлюза. Процедура проста, если всё автоматизировано: генерируется новая пара ключей, публичный ключ обновляется в inventory, система раскатки разворачивает конфиги, старые пиры удаляются командой wg set wg0 peer <старый-публичный-ключ> remove. Самое приятное: ротация не рвёт работающие соединения одновременно на всех пирах, если раскатывать её по очереди, начиная с резервного шлюза.
Pre-shared key есть смысл добавлять тем, кто переживает про постквантовую устойчивость: параметр PresharedKey в секции peer добавляет симметричный секрет поверх основного обмена ключами. В обычной лаборатории это избыточно, но держать возможность в голове полезно.
Тестирование сети до того, как её полюбят приложения
Свежеподнятая mesh-сеть проходит короткий прогон перед сдачей приложениям. Проверяется связность: ping с каждого хоста до каждого по overlay-адресам, затем ping с хоста облака A до хоста во внутренней сети облака B через шлюз. Затем полоса: iperf3 в обе стороны между типичными узлами, чтобы понять реальную скорость туннеля, а не маркетинговую. Третья проверка - MTU: ping -M do -s 1372 до пира должен проходить без фрагментации, иначе настройка MTU в конфиге не совпадает с реальностью транспорта.
Следом идёт тест отказа: выключается один из шлюзов, и наблюдается, перестроился ли трафик на второй и за какое время. Если используется keepalived с виртуальным адресом, переключение занимает секунды. Если маршрутизация статическая на уровне route tables облака, замена маршрута может требовать вызова API провайдера - такой механизм тоже репетируют заранее, иначе первый реальный отказ удивит долгим переключением.
Мониторинг сети минимален: экспортер метрик wireguard (есть готовый prometheus exporter) снимает состояние handshakes и байты по пирам; алерт на "handshake старше 5 минут" ловит почти любую неисправность раньше, чем заметят приложения. Дополнительно полезен простой blackbox-монитор: периодический ping между ключевыми узлами двух облаков, идущий в общий дашборд доступности. С таким набором сеть перестаёт быть загадкой и становится наблюдаемой инфраструктурой, где любая деградация видна раньше, чем скажется на пользователях.
Мелкие грабли, о которых стоит знать заранее
Первая грабля - rp_filter на шлюзах: строгий режим отбрасывает пакеты, пришедшие не с того интерфейса, откуда ядро ожидало бы обратный путь. Между облаками с маршрутизацией подсетей это всплывает регулярно; лечится выставлением net.ipv4.conf.all.rp_filter=2 (loose) на шлюзовых машинах. Вторая грабля - firewall внутри гостя: ufw и firewalld по умолчанию не пропускают форвардинг и UDP 51820, пока не добавлены правила. Третья - время: криптография чувствительна к рассинхрону часов при некоторых сценариях, и chrony или systemd-timesyncd на всех узлах это не украшение, а часть здоровья сети. Четвёртая - дубли адресов при ручной выдаче: держите единый реестр overlay-адресов, иначе два сервера с одним адресом будут молча перебивать друг друга, и поиск виновника отнимет вечер.