Частная сеть между серверами
Инструкция соединяет несколько серверов частной сетью на WireGuard: у каждой машины появляется адрес, по которому её видят остальные машины контура и не видит интернет. После выполнения сервисы обращаются друг к другу по этим адресам, а наружу по-прежнему смотрят только порты 80 и 443 на сервере шлюза.
26 минутИнструкция соединяет несколько серверов частной сетью на WireGuard: у каждой машины появляется адрес, по которому её видят остальные машины контура и не видит интернет. После выполнения сервисы обращаются друг к другу по этим адресам, а наружу по-прежнему смотрят только порты 80 и 443 на сервере шлюза.
Исходное состояние: две или более машины, у каждой есть публичный адрес и вход по SSH, между собой
они связаны только через интернет. Конечное состояние: интерфейс wg0 на каждой машине, адреса из
одного плана, рукопожатие между пирами, и контейнер-клиент рядом с сервисом, к сетевому
пространству которого подключается его nginx.
Каждый шаг заканчивается проверкой с однозначным ожидаемым результатом. Результат другой — переходите к разделу «Типичные отказы», не выполняя следующий шаг.
Что нужно до начала:
- две машины с Ubuntu 22.04/24.04 LTS или Debian 12, доведённые до состояния из
базовой настройки сервера: рабочий пользователь с
sudo, включённыйufw, синхронизированное время; - на машинах, где будут контейнеры, — установленный Docker;
- у машины, которая станет узлом-концентратором, — постоянный публичный адрес или доменное имя, указывающее на неё;
- аварийная консоль провайдера. Ошибка в правилах пересылки доступ по SSH не отнимает, но восстанавливать связность быстрее из консоли.
Проверки предусловий — на каждой машине:
. /etc/os-release && echo "$ID $VERSION_ID" # ожидается: ubuntu 22.04 / ubuntu 24.04 / debian 12
uname -r # ожидается: 5.6 или новее
sudo modprobe wireguard && lsmod | grep -c '^wireguard' # ожидается: 1
timedatectl show -p NTPSynchronized --value # ожидается: yesНачиная с версии 5.6 модуль wireguard входит в состав ядра, поэтому modprobe отрабатывает до
установки каких-либо пакетов. Синхронное время нужно самому протоколу: рукопожатие несёт метку
времени, и инициатор с отставшими часами отвергается стороной, которая уже видела более позднюю
метку.
Проверка, что выбранный план адресов ни с чем не пересекается, — на каждой машине:
ip -o -4 addr show | awk '{print $2, $4}'
docker network ls -q | xargs -r docker network inspect \
-f '{{.Name}} {{range .IPAM.Config}}{{.Subnet}}{{end}}'Ожидается: ни один адрес и ни одна подсеть не попадают в диапазон, отведённый под туннель. Пересечение — самая частая причина того, что пир поднимается, а трафик до него не доходит: ядро отправляет пакет на локальный мост, а не в туннель.
Если у провайдера включён облачный межсетевой экран, разрешите в нём 51820/udp отдельно: правила
ufw на него не действуют.
Место в цепочке
| Откуда пришли | Этот документ | Куда ведёт |
|---|---|---|
| настроенный сервер и установленный Docker на каждой машине | узел-концентратор, план адресов, пиры, клиент в контейнере рядом с сервисом | сетевой контур: шлюз обращается к микросервисам по адресам этой сети |
Что предыдущие звенья обязаны обеспечить:
- включённый
ufwс разрешённым SSH — иначе открытие одного UDP-порта не имеет смысла: открыто всё; - синхронизированное время — см. выше;
- правило публикации портов из документа о Docker. Частная сеть заменяет публикацию, а не дополняет её: если порт сервиса опубликован на всех интерфейсах, туннель ничего не закрывает.
Что этот документ оставляет следующему звену:
- адрес каждой машины в туннеле — именно он подставляется в
upstreamшлюза и в переменные окружения видаCORE_SERVICE_ADDRESS; - контейнер
vpn_clientв Compose-файле микросервиса — тот самый, к которому сетевой контур подключает nginx черезnetwork_mode: "service:vpn_client"; - непересекающийся план подсетей, на который опираются
ipam-блоки Compose.
Порядок принципиален в трёх местах:
- Ключи создаются до конфигурации: в
wg0.confподставляется значение закрытого ключа, а не ссылка на файл. - Блок
[Peer]появляется на концентраторе до первого запуска клиента. Рукопожатие с неизвестным ключом отбрасывается молча — на клиенте это выглядит так же, как закрытый порт, и разбирать придётся две причины сразу. - UDP-порт открывается до запуска клиента — по той же причине.
Зачем частная сеть
Сервис, доступный из интернета, доступен всем: любой запрос доходит до него напрямую, минуя шлюз с его авторизацией, ограничением частоты запросов и предельным размером тела. Частная сеть убирает такой путь. Микросервис слушает адрес, которого в интернете не существует: маршрута к нему нет ни у кого, кроме машин с ключом от туннеля.
| Свойство | Обращение по публичному адресу | Обращение по адресу частной сети |
|---|---|---|
| кто может отправить пакет | любой хост в интернете | только пиры туннеля |
| что открыто на сервере микросервиса | порт сервиса | один UDP-порт на концентраторе |
| чем ограничен доступ | правилами приложения и экрана | наличием закрытого ключа |
| что видно при сканировании адреса | открытый порт | ничего |
Это не отменяет проверок на входе: nginx микросервиса и шлюз остаются на своих местах. Частная сеть убирает обходной путь, а не заменяет то, что стоит на пути основном.
Схема адресации
WireGuard адреса не раздаёт: DHCP в нём нет. Адрес пира записан в двух местах — в его собственном
[Interface] Address и на концентраторе в AllowedIPs его блока [Peer]. Расхождение между ними
означает, что пакеты пира отбрасываются как пришедшие с чужого адреса. Поэтому адрес назначается
один раз и фиксируется в журнале.
Журнал соответствия «адрес — машина» ведётся в одном месте: комментарием над каждым блоком [Peer]
в wg0.conf концентратора. Отдельный файл со списком расходится с реальностью на первой же правке.
Весь контур живёт внутри 10.0.0.0/8, разделённого на непересекающиеся планы:
| План | Диапазон | Что адресует | Кто выдаёт |
|---|---|---|---|
| туннель | 10.0.0.0/24 | интерфейсы wg0 всех машин | человек, вручную, с записью в wg0.conf концентратора |
| мост частной сети сервера N | 10.21.N.0/24 | контейнеры, которым нужен выход в туннель | Compose, блок ipam |
| мост хоста сервера N | 10.20.N.0/24 | контейнеры, доступные с этой же машины | Compose, блок ipam |
Пример на две машины:
| Роль | Адрес в туннеле | vpn_net | host_net |
|---|---|---|---|
| сервер шлюза, он же концентратор | 10.0.0.1 | 10.21.1.0/24 | 10.20.1.0/24 |
| сервер микросервиса | 10.0.0.10 | 10.21.2.0/24 | 10.20.2.0/24 |
Внутри моста последний октет закрепляется за ролью: .10 — контейнер клиента туннеля, .100 —
backend. Это соглашение, а не требование протокола; его смысл в том, что адрес в Compose-файле
читается без сверки с чужой конфигурацией.
Концентратором здесь работает сервер шлюза: у него уже есть публичный адрес и открытые порты, и
отдельная машина ради одного интерфейса не нужна. Когда шлюзов несколько, концентратор выносят на
отдельную машину — схема при этом не меняется, меняется только то, чей адрес стоит в Endpoint.
Адреса локальных мостов, которые встречаются в сетевом контуре
(10.0.0.100 для vpn_net и 10.0.1.100 для host_net), — это адреса backend на двух мостах
одного сервера; конкретные значения задаёт блок ipam его Compose-файла. Жёсткое требование одно:
подсети мостов не пересекаются ни между собой, ни с планом туннеля.
Почему на концентраторе у пира всегда /32. AllowedIPs в WireGuard работает в обе стороны: для
исходящих пакетов это таблица маршрутов, для входящих — фильтр допустимых адресов отправителя. Пир
с AllowedIPs = 10.0.0.0/24 получил бы право отправлять пакеты от имени любой машины контура.
Маска /32 оставляет за пиром ровно один адрес.
Шаг 1. Пакеты и ключи на концентраторе
sudo apt-get update
sudo apt-get install -y wireguard wireguard-tools
sudo install -d -m 700 /etc/wireguard
wg genkey | sudo tee /etc/wireguard/server_private.key >/dev/null
sudo chmod 600 /etc/wireguard/server_private.key
sudo sh -c 'wg pubkey < /etc/wireguard/server_private.key > /etc/wireguard/server_public.key'
sudo chmod 644 /etc/wireguard/server_public.keysudo ls -l /etc/wireguardОжидается: server_private.key с правами -rw-------, server_public.key с правами
-rw-r--r--. Права на закрытый ключ шире 600 — исправьте до продолжения: файл читают все
пользователи машины.
Закрытый ключ не покидает машину, на которой создан. Наружу отдаётся только содержимое
server_public.key.
Шаг 2. Конфигурация wg0.conf на концентраторе
sudo tee /etc/wireguard/wg0.conf >/dev/null <<'EOF'
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <содержимое /etc/wireguard/server_private.key>
PostUp = iptables -I FORWARD 1 -i wg0 -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -o wg0 -j ACCEPT
EOF
sudo chmod 600 /etc/wireguard/wg0.confЧто означает каждая строка:
| Строка | Смысл |
|---|---|
Address = 10.0.0.1/24 | адрес концентратора в туннеле; маска /24 задаёт, какой диапазон считается локальным для wg0 |
ListenPort = 51820 | UDP-порт, который слушает интерфейс; фиксируется явно, потому что на него ссылается правило экрана и Endpoint клиентов |
PrivateKey | значение ключа, а не путь к файлу |
PostUp / PostDown | разрешение пересылать пакеты между пирами и снятие этого разрешения при остановке |
Правило пересылки ставится первым в цепочке (-I FORWARD 1). В FORWARD ufw держит
собственные переходы; правило, добавленное в конец (-A), срабатывает или нет в зависимости от их
содержимого, а поставленное первым — не зависит.
Пересылка разрешается только между пирами (-i wg0 -o wg0). Выхода из туннеля в интернет здесь
нет и NAT не настраивается: это частная сеть между серверами, а не выходной узел.
SaveConfig не включается. С ним wg-quick down перезаписывает файл своим представлением
состояния и удаляет комментарии — вместе с журналом соответствия адресов машинам.
sudo grep -c '^PrivateKey = <' /etc/wireguard/wg0.conf # ожидается: 0Ожидается: 0 — заполнитель заменён на значение ключа. Единица означает, что в файле остался
текст <содержимое ...>, и интерфейс не поднимется.
Шаг 3. Пересылка пакетов
Без пересылки концентратор отвечает на обращения к своему адресу, но не передаёт пакеты между
двумя пирами: сервер шлюза увидит 10.0.0.1 и не увидит 10.0.0.10.
sudo tee /etc/sysctl.d/99-wireguard.conf >/dev/null <<'EOF'
net.ipv4.ip_forward = 1
EOF
sudo sysctl --systemsysctl net.ipv4.ip_forward # ожидается: net.ipv4.ip_forward = 1Отдельный файл в /etc/sysctl.d/ вместо правки /etc/sysctl.conf — чтобы настройку было видно
как принадлежащую этому документу и чтобы снятие сводилось к удалению одного файла.
Шаг 4. Открыть один порт UDP
sudo ufw allow 51820/udp
sudo ufw status verboseОжидается строка 51820/udp ALLOW IN Anywhere и отсутствие других новых разрешений. TCP на
этом порту не открывается: WireGuard работает только по UDP, разрешение TCP не даёт ничего, кроме
дополнительной открытой точки.
Шаг 5. Запуск и автозапуск
sudo systemctl enable --now wg-quick@wg0Команда wg-quick up wg0 делает то же самое разово, но не переживает перезагрузку, и после неё
systemctl start завершается ошибкой wg0 already exists. Используйте один способ — через
systemd.
sudo wg showinterface: wg0
public key: <открытый ключ концентратора>
private key: (hidden)
listening port: 51820Пиров пока нет — это состояние соответствует конфигурации.
sudo ss -ulpn | grep 51820Ожидается строка вида UNCONN 0 0 0.0.0.0:51820 0.0.0.0:* без имени процесса: сокет держит
модуль ядра, а не пользовательская программа. Пустая колонка процесса здесь — норма, а не признак
неисправности.
ip addr show wg0 # ожидается: inet 10.0.0.1/24, состояние UPШаг 6. Ключи и адрес для второго сервера
Выполняется на сервере микросервиса — закрытый ключ создаётся там, где будет использоваться, и по сети не передаётся:
sudo apt-get update
sudo apt-get install -y wireguard wireguard-tools
sudo install -d -m 700 /etc/wireguard
wg genkey | sudo tee /etc/wireguard/private.key >/dev/null
sudo chmod 600 /etc/wireguard/private.key
sudo sh -c 'wg pubkey < /etc/wireguard/private.key > /etc/wireguard/public.key'
sudo cat /etc/wireguard/public.keyСодержимое public.key переносится на концентратор. Обратно с концентратора берётся содержимое
server_public.key.
Дальше — на концентраторе. Блок пира дописывается в конец wg0.conf:
# сервер микросервиса
[Peer]
PublicKey = <открытый ключ сервера микросервиса>
AllowedIPs = 10.0.0.10/32Применение без разрыва уже установленных соединений:
sudo wg syncconf wg0 <(sudo wg-quick strip wg0)wg-quick strip убирает из файла директивы, которые понимает только wg-quick (Address,
PostUp, DNS), и отдаёт остальное wg syncconf. Тот приводит состояние интерфейса к файлу,
не пересоздавая его. Пара wg-quick down / wg-quick up дала бы тот же результат, но оборвала бы
все остальные пиры.
sudo wg show wg0 allowed-ipsОжидается строка с открытым ключом сервера микросервиса и 10.0.0.10/32. Обращений от пира
ещё нет — рукопожатие появится после шага 7.
Шаг 7. Клиент на сервере микросервиса
sudo tee /etc/wireguard/wg0.conf >/dev/null <<'EOF'
[Interface]
Address = 10.0.0.10/24
PrivateKey = <содержимое /etc/wireguard/private.key>
[Peer]
PublicKey = <содержимое server_public.key концентратора>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
EOF
sudo chmod 600 /etc/wireguard/wg0.conf
sudo systemctl enable --now wg-quick@wg0| Параметр | Значение | Почему так |
|---|---|---|
AllowedIPs = 10.0.0.0/24 | только подсеть туннеля | в туннель уходит трафик к машинам контура; весь остальной — обычным маршрутом |
AllowedIPs = 0.0.0.0/0 | весь трафик | превращает концентратор в единственный выход машины в интернет; для связи между серверами не требуется |
PersistentKeepalive = 25 | пакет каждые 25 секунд | удерживает запись в таблице NAT провайдера, иначе входящие пакеты перестают доходить через несколько минут тишины |
ListenPort не задан | порт выбирается ядром | клиент не принимает входящие соединения, фиксировать порт незачем |
Значение AllowedIPs определяет и маршруты: wg-quick добавляет маршрут на каждую подсеть из
этого списка. Указание 0.0.0.0/0 увело бы в туннель в том числе SSH-сессию, по которой идёт
настройка.
sudo wg showinterface: wg0
public key: <открытый ключ этого сервера>
private key: (hidden)
listening port: <выбран ядром>
peer: <открытый ключ концентратора>
endpoint: <публичный адрес концентратора>:51820
allowed ips: 10.0.0.0/24
latest handshake: 12 seconds ago
transfer: 1.98 KiB received, 3.12 KiB sentКлючевая строка — latest handshake. Её отсутствие означает, что рукопожатие не состоялось, и
дальше идти нельзя.
ip route get 10.0.0.1Ожидается: 10.0.0.1 dev wg0 src 10.0.0.10. Вывод с другим интерфейсом означает пересечение
подсетей — см. отказы.
ping -c 3 10.0.0.1 # ожидается: 3 packets transmitted, 3 received
ping -M do -s 1392 -c 3 10.0.0.1 # ожидается: 3 received, без "Frag needed"Второй ping проверяет проходимость пакета полного размера: 1392 байта данных плюс заголовки дают
1420 — это MTU интерфейса wg0 по умолчанию. Первая команда прошла, вторая нет — раздел про MTU в
отказах.
sudo wg show wg0 latest-handshakes # ожидается: у пира не 0
ping -c 3 10.0.0.10 # ожидается: 3 receivedШаг 8. Клиент внутри контейнера рядом с сервисом
Клиент из шага 7 поднимает туннель на самой машине. Это работает, но привязывает микросервис к настройке хоста: перенос на другой сервер требует повторить её вручную. Вариант, при котором вся сетевая часть микросервиса описана в его же Compose-файле, — клиент в контейнере.
vpn/Dockerfile:
FROM linuxserver/wireguard:latest
ENV PUID=1000 \
PGID=1000 \
TZ=UTC
# образ обращается к этому каталогу при старте, проверяя наличие модуля ядра
RUN mkdir -p /lib/modules && chmod 755 /lib/modulesКонфигурация в образ не копируется, а монтируется: слой образа с закрытым ключом попадает в кэш
сборки и в реестр, если образ туда отправляют. Файл vpn/wg0.conf — тот же, что в шаге 7, с
адресом этого сервера; в системе контроля версий он не хранится.
Compose-файл микросервиса:
networks:
host_net:
driver: bridge
ipam: { config: [{ subnet: 10.20.2.0/24 }] }
vpn_net:
driver: bridge
ipam: { config: [{ subnet: 10.21.2.0/24 }] }
services:
vpn_client:
build: { context: ., dockerfile: vpn/Dockerfile }
networks:
vpn_net: { ipv4_address: 10.21.2.10 }
cap_add: ["NET_ADMIN"]
devices: ["/dev/net/tun:/dev/net/tun"]
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- ./vpn/wg0.conf:/config/wg0.conf:ro
restart: unless-stopped
command: >
sh -c "iptables -t nat -A POSTROUTING -s 10.21.2.0/24 -o wg0 -j MASQUERADE
&& tail -f /dev/null"
backend:
build: { context: ., dockerfile: source/Dockerfile }
networks:
host_net: { ipv4_address: 10.20.2.100 }
vpn_net: { ipv4_address: 10.21.2.100 }
expose: ["8000"]
depends_on: [vpn_client]Вход в микросервис со стороны туннеля — отдельный nginx, подключённый к сетевому пространству
клиента (network_mode: "service:vpn_client"). Он описан в
сетевом контуре и здесь не повторяется.
Что делает command
Правило MASQUERADE переписывает адрес отправителя у пакетов, уходящих из моста vpn_net в
туннель. Без него сервер на другом конце получил бы пакет с адресом 10.21.2.100 — адресом
локального моста чужой машины, маршрута к которому у него нет, и ответ никуда бы не ушёл. С
правилом обращения приходят с адреса 10.0.0.10, то есть с адреса этого сервера в туннеле.
Правило ссылается на wg0, поэтому может быть добавлено только после подъёма интерфейса.
Инициализация образа поднимает wg0 из /config/wg0.conf до запуска команды, отдельный вызов
wg-quick не нужен. tail -f /dev/null удерживает команду запущенной: её завершение остановило бы
контейнер, а вместе с ним и сетевое пространство, в котором живёт nginx.
Права: почему NET_ADMIN, а не privileged
| Что нужно клиенту | Чем выдаётся | Зачем |
|---|---|---|
создать интерфейс, задать адрес и маршруты, править iptables в своём пространстве | cap_add: ["NET_ADMIN"] | это и есть работа wg-quick |
устройство /dev/net/tun | devices | нужно резервной реализации в пространстве пользователя, когда модуль ядра недоступен |
параметр net.ipv4.conf.all.src_valid_mark | sysctls в Compose | wg-quick устанавливает его сам, когда туннель забирает маршрут по умолчанию; контейнер без полных привилегий сделать этого не может, поэтому значение задаётся снаружи |
privileged: true выдаёт все возможности сразу, доступ ко всем устройствам хоста и ослабляет
профили seccomp и AppArmor. Из этого набора клиенту нужны две позиции из таблицы выше.
Возможность SYS_MODULE в список не входит намеренно: она нужна только для загрузки модуля ядра
изнутри контейнера и равносильна праву загрузить в ядро хоста произвольный код. Модуль
wireguard входит в ядро с версии 5.6; если он не загружен, загрузите его на хосте:
sudo modprobe wireguard
echo wireguard | sudo tee /etc/modules-load.d/wireguard.confИсходящие обращения от backend
Если микросервис только отвечает на запросы, шаг закончен: входящие приходят через nginx, который уже находится в сетевом пространстве клиента. Если backend сам обращается к другим машинам контура, ему нужен маршрут в туннель через контейнер клиента:
backend:
cap_add: ["NET_ADMIN"]
command: >
sh -c "ip route replace 10.0.0.0/24 via 10.21.2.10
&& exec python -m source.app.main"Возможность NET_ADMIN здесь выдаётся ради одной команды ip route. Микросервису, который никуда
не обращается сам, ни маршрут, ни возможность не нужны — не добавляйте их «на всякий случай».
docker compose up -d
docker compose exec vpn_client wg showОжидается тот же вывод, что в шаге 7: интерфейс, пир концентратора и непустой
latest handshake.
docker compose exec vpn_client ip route # ожидается строка: 10.0.0.0/24 dev wg0
docker compose exec vpn_client iptables -t nat -L POSTROUTING -n
docker compose exec vpn_client ping -c 3 10.0.0.1Ожидается во второй команде правило MASQUERADE ... 10.21.2.0/24 ... на интерфейсе wg0, в
третьей — три полученных пакета. Ответ ping: not found означает только, что в образе нет этой
программы: тогда признак связности — растущие счётчики transfer в wg show при повторном вызове.
Если настраивали маршрут для backend:
docker compose exec backend ip route get 10.0.0.1 # ожидается: via 10.21.2.10Клиент из шага 7 и клиент в контейнере — два способа сделать одно и то же на одной машине. Работать
должен один: два интерфейса wg0 с одним и тем же адресом пира дадут отбрасывание пакетов на
концентраторе. Переходя на контейнер, снимите хостовый клиент: sudo systemctl disable --now wg-quick@wg0.
Проверка всей цепочки
Снизу вверх, с сервера шлюза:
# 1. пир поднят и обменивается данными
sudo wg show # ожидается: latest handshake, ненулевой transfer
# 2. адрес второй машины отвечает
ping -c 3 10.0.0.10 # ожидается: 3 received
# 3. вход в микросервис отвечает по адресу туннеля
curl -sf http://10.0.0.10:8000/health && echo OK
# 4. адрес микросервиса недоступен снаружи контура — с машины вне туннеля
curl -m 5 -I http://10.0.0.10:8000/health # ожидается: таймаут| Первая неудавшаяся проверка | Где искать |
|---|---|
| 1 | рукопожатие: порт, Endpoint, ключи — шаги 4–7 |
| 2 | адресация и маршруты: AllowedIPs, пересечение подсетей, пересылка на концентраторе |
| 3 | nginx микросервиса не запущен или не в сетевом пространстве клиента — сетевой контур |
| 4 | адрес частной сети маршрутизируется извне — план адресов пересекается с реальной сетью провайдера |
Типичные отказы
| Признак | Причина | Что делать |
|---|---|---|
в wg show у пира нет строки latest handshake, transfer: 0 B received | пакеты не доходят до концентратора | на концентраторе: sudo ss -ulpn | grep 51820 и sudo ufw status; проверить облачный экран провайдера; сверить Endpoint в конфигурации клиента |
latest handshake есть, но 0 B sent или 0 B received | несовпадение AllowedIPs: на концентраторе не тот адрес пира либо на клиенте подсеть не покрывает адресата | sudo wg show wg0 allowed-ips на обоих концах; на концентраторе у пира — /32 его адреса, на клиенте — подсеть туннеля |
| концентратор пингуется, второй пир — нет | не включена пересылка или нет правила FORWARD | sysctl net.ipv4.ip_forward (ожидается 1), sudo iptables -L FORWARD -n -v --line-numbers — правило wg0 → wg0 должно быть выше правил ufw |
ping проходит, curl отдаёт заголовки и виснет; большие ответы обрываются, маленькие проходят | MTU: внешний пакет 1500 байт не проходит по пути, а признак «не фрагментировать» запрещает его разрезать | задать MTU = 1380 в [Interface] на обоих концах, перезапустить интерфейс, проверить ip link show wg0 и ping -M do -s 1352; при отказе снижать до 1280 |
ip route get <адрес туннеля> показывает не dev wg0 | подсеть туннеля пересекается с локальным мостом Docker или с сетью провайдера | сменить план адресов или подсети в ipam, пересоздать сети: docker compose down && docker network prune |
wg-quick up wg0 завершается с RTNETLINK answers: File exists | интерфейс уже поднят — вручную или предыдущим запуском службы | sudo wg-quick down wg0, затем sudo systemctl start wg-quick@wg0; пользоваться одним способом запуска |
| после добавления пира оборвались соединения остальных | интерфейс перезапускали целиком | применять sudo wg syncconf wg0 <(sudo wg-quick strip wg0) |
| туннеля нет после перезагрузки | служба не включена в автозапуск | sudo systemctl enable wg-quick@wg0 |
контейнер клиента запускается, но wg show внутри пуст | нет NET_ADMIN или /dev/net/tun | добавить cap_add и devices, смотреть docker compose logs vpn_client |
обращения приходят с адреса 10.21.N.100, а не с адреса туннеля | нет правила MASQUERADE в контейнере клиента | проверить iptables -t nat -L POSTROUTING -n внутри контейнера; правило добавляется после подъёма wg0 |
| backend не может обратиться к другой машине контура, хотя туннель поднят | у backend нет маршрута в подсеть туннеля | ip route replace 10.0.0.0/24 via 10.21.N.10 в контейнере backend с cap_add: ["NET_ADMIN"] |
| рукопожатия нет, ключи и порт верны | расхождение часов: метка времени в рукопожатии не принимается | timedatectl на обеих машинах, включить синхронизацию |
| закрытый ключ попал в репозиторий, слой образа или переписку | ключ считается скомпрометированным независимо от того, воспользовался ли им кто-то | см. «Отзыв ключа» ниже |
Отзыв ключа
Смена ключа затрагивает обе стороны, порядок — от концентратора к пиру, чтобы старый ключ перестал приниматься до того, как начнётся возня с конфигурацией сервера.
# на концентраторе: убрать блок [Peer] со старым ключом из /etc/wireguard/wg0.conf
sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
sudo wg show wg0 allowed-ips # ожидается: старого ключа в выводе нет
# на пире: новая пара
wg genkey | sudo tee /etc/wireguard/private.key >/dev/null
sudo chmod 600 /etc/wireguard/private.key
sudo sh -c 'wg pubkey < /etc/wireguard/private.key > /etc/wireguard/public.key'
# на концентраторе: новый блок [Peer] с тем же адресом, снова syncconfЧто нужно сделать дополнительно, если ключ утёк через образ: пересобрать образ без старого файла и
удалить прежние слои (docker image prune -a на машинах, где образ есть). Слой с ключом остаётся в
кэше сборки и в реестре после того, как из репозитория файл убран.
Откат и снятие
Снять клиента в контейнере:
docker compose stop vpn_client
docker compose rm -f vpn_clientСервисы, у которых network_mode: "service:vpn_client", останавливаются вместе с ним: их сетевое
пространство принадлежит удалённому контейнеру. Уберите эти сервисы из Compose-файла или переключите
их на локальный вход — в сетевом контуре для этого рядом с vpn_nginx
лежит закомментированный host_nginx.
Снять клиента на хосте:
sudo systemctl disable --now wg-quick@wg0
ip link show wg0 # ожидается: Device "wg0" does not existОтозвать ключ на концентраторе — по разделу «Отзыв ключа»: блок [Peer] удаляется, wg syncconf
применяет; после этого машина с этим ключом в туннель не входит.
Полностью снять концентратор:
sudo systemctl disable --now wg-quick@wg0
sudo ufw delete allow 51820/udp
sudo rm /etc/sysctl.d/99-wireguard.conf && sudo sysctl --systemВернуть прямой доступ к микросервису, пока частной сети нет: опубликовать порт его входа на loopback и обращаться к нему с этой же машины либо через SSH-туннель. Публикация на всех интерфейсах открывает сервис интернету — если она понадобилась, снимайте её сразу после проверки.
Что не удаляется перечисленными командами:
| Остаётся | Где | Что с этим делать |
|---|---|---|
/etc/wireguard/wg0.conf и файлы ключей | на каждой машине | закрытые ключи удалять отдельно: sudo sh -c 'shred -u /etc/wireguard/*.key /etc/wireguard/wg0.conf' — раскрытие маски выполняет sh под sudo, потому что каталог закрыт для чтения обычному пользователю |
правила iptables из PostUp | таблица filter, цепочка FORWARD | снимаются PostDown при остановке службы; после ручного запуска правил — удалить командой из PostDown |
| сети Compose | Docker | docker network prune после остановки проектов |
| образ клиента с вкомпилированной конфигурацией | локальный кэш образов | docker image prune -a |
Пакеты wireguard и wireguard-tools удалять не нужно: без интерфейса и конфигурации они ничего
не делают, а их наличие не открывает портов.
Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.