gitaspen docs

Частная сеть между серверами

Инструкция соединяет несколько серверов частной сетью на WireGuard: у каждой машины появляется адрес, по которому её видят остальные машины контура и не видит интернет. После выполнения сервисы обращаются друг к другу по этим адресам, а наружу по-прежнему смотрят только порты 80 и 443 на сервере шлюза.

26 минут

Инструкция соединяет несколько серверов частной сетью на WireGuard: у каждой машины появляется адрес, по которому её видят остальные машины контура и не видит интернет. После выполнения сервисы обращаются друг к другу по этим адресам, а наружу по-прежнему смотрят только порты 80 и 443 на сервере шлюза.

Исходное состояние: две или более машины, у каждой есть публичный адрес и вход по SSH, между собой они связаны только через интернет. Конечное состояние: интерфейс wg0 на каждой машине, адреса из одного плана, рукопожатие между пирами, и контейнер-клиент рядом с сервисом, к сетевому пространству которого подключается его nginx.

Каждый шаг заканчивается проверкой с однозначным ожидаемым результатом. Результат другой — переходите к разделу «Типичные отказы», не выполняя следующий шаг.

Что нужно до начала:

  • две машины с Ubuntu 22.04/24.04 LTS или Debian 12, доведённые до состояния из базовой настройки сервера: рабочий пользователь с sudo, включённый ufw, синхронизированное время;
  • на машинах, где будут контейнеры, — установленный Docker;
  • у машины, которая станет узлом-концентратором, — постоянный публичный адрес или доменное имя, указывающее на неё;
  • аварийная консоль провайдера. Ошибка в правилах пересылки доступ по SSH не отнимает, но восстанавливать связность быстрее из консоли.

Проверки предусловий — на каждой машине:

bash
. /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 отрабатывает до установки каких-либо пакетов. Синхронное время нужно самому протоколу: рукопожатие несёт метку времени, и инициатор с отставшими часами отвергается стороной, которая уже видела более позднюю метку.

Проверка, что выбранный план адресов ни с чем не пересекается, — на каждой машине:

bash
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.

Порядок принципиален в трёх местах:

  1. Ключи создаются до конфигурации: в wg0.conf подставляется значение закрытого ключа, а не ссылка на файл.
  2. Блок [Peer] появляется на концентраторе до первого запуска клиента. Рукопожатие с неизвестным ключом отбрасывается молча — на клиенте это выглядит так же, как закрытый порт, и разбирать придётся две причины сразу.
  3. 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 концентратора
мост частной сети сервера N10.21.N.0/24контейнеры, которым нужен выход в туннельCompose, блок ipam
мост хоста сервера N10.20.N.0/24контейнеры, доступные с этой же машиныCompose, блок ipam

Пример на две машины:

РольАдрес в туннелеvpn_nethost_net
сервер шлюза, он же концентратор10.0.0.110.21.1.0/2410.20.1.0/24
сервер микросервиса10.0.0.1010.21.2.0/2410.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. Пакеты и ключи на концентраторе

bash
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.key
Проверка:
sudo ls -l /etc/wireguard

Ожидается: server_private.key с правами -rw-------, server_public.key с правами -rw-r--r--. Права на закрытый ключ шире 600 — исправьте до продолжения: файл читают все пользователи машины.

Закрытый ключ не покидает машину, на которой создан. Наружу отдаётся только содержимое server_public.key.


Шаг 2. Конфигурация wg0.conf на концентраторе

bash
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 = 51820UDP-порт, который слушает интерфейс; фиксируется явно, потому что на него ссылается правило экрана и 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.

bash
sudo tee /etc/sysctl.d/99-wireguard.conf >/dev/null <<'EOF'
net.ipv4.ip_forward = 1
EOF

sudo sysctl --system
Проверка:
sysctl net.ipv4.ip_forward     # ожидается: net.ipv4.ip_forward = 1

Отдельный файл в /etc/sysctl.d/ вместо правки /etc/sysctl.conf — чтобы настройку было видно как принадлежащую этому документу и чтобы снятие сводилось к удалению одного файла.


Шаг 4. Открыть один порт UDP

bash
sudo ufw allow 51820/udp
sudo ufw status verbose

Ожидается строка 51820/udp ALLOW IN Anywhere и отсутствие других новых разрешений. TCP на этом порту не открывается: WireGuard работает только по UDP, разрешение TCP не даёт ничего, кроме дополнительной открытой точки.


Шаг 5. Запуск и автозапуск

bash
sudo systemctl enable --now wg-quick@wg0

Команда wg-quick up wg0 делает то же самое разово, но не переживает перезагрузку, и после неё systemctl start завершается ошибкой wg0 already exists. Используйте один способ — через systemd.

Проверка:
sudo wg show
Ожидается:
interface: wg0
  public key: <открытый ключ концентратора>
  private key: (hidden)
  listening port: 51820

Пиров пока нет — это состояние соответствует конфигурации.

bash
sudo ss -ulpn | grep 51820

Ожидается строка вида UNCONN 0 0 0.0.0.0:51820 0.0.0.0:* без имени процесса: сокет держит модуль ядра, а не пользовательская программа. Пустая колонка процесса здесь — норма, а не признак неисправности.

bash
ip addr show wg0        # ожидается: inet 10.0.0.1/24, состояние UP

Шаг 6. Ключи и адрес для второго сервера

Выполняется на сервере микросервиса — закрытый ключ создаётся там, где будет использоваться, и по сети не передаётся:

bash
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:

ini
# сервер микросервиса
[Peer]
PublicKey = <открытый ключ сервера микросервиса>
AllowedIPs = 10.0.0.10/32

Применение без разрыва уже установленных соединений:

bash
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. Клиент на сервере микросервиса

bash
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 show
Ожидается:
interface: 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. Её отсутствие означает, что рукопожатие не состоялось, и дальше идти нельзя.

bash
ip route get 10.0.0.1

Ожидается: 10.0.0.1 dev wg0 src 10.0.0.10. Вывод с другим интерфейсом означает пересечение подсетей — см. отказы.

bash
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:

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-файл микросервиса:

yaml
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/tundevicesнужно резервной реализации в пространстве пользователя, когда модуль ядра недоступен
параметр net.ipv4.conf.all.src_valid_marksysctls в Composewg-quick устанавливает его сам, когда туннель забирает маршрут по умолчанию; контейнер без полных привилегий сделать этого не может, поэтому значение задаётся снаружи

privileged: true выдаёт все возможности сразу, доступ ко всем устройствам хоста и ослабляет профили seccomp и AppArmor. Из этого набора клиенту нужны две позиции из таблицы выше.

Возможность SYS_MODULE в список не входит намеренно: она нужна только для загрузки модуля ядра изнутри контейнера и равносильна праву загрузить в ядро хоста произвольный код. Модуль wireguard входит в ядро с версии 5.6; если он не загружен, загрузите его на хосте:

bash
sudo modprobe wireguard
echo wireguard | sudo tee /etc/modules-load.d/wireguard.conf

Исходящие обращения от backend

Если микросервис только отвечает на запросы, шаг закончен: входящие приходят через nginx, который уже находится в сетевом пространстве клиента. Если backend сам обращается к другим машинам контура, ему нужен маршрут в туннель через контейнер клиента:

yaml
  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.

bash
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:

bash
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.


Проверка всей цепочки

Снизу вверх, с сервера шлюза:

bash
# 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, пересечение подсетей, пересылка на концентраторе
3nginx микросервиса не запущен или не в сетевом пространстве клиента — сетевой контур
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 его адреса, на клиенте — подсеть туннеля
концентратор пингуется, второй пир — нетне включена пересылка или нет правила FORWARDsysctl 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 на обеих машинах, включить синхронизацию
закрытый ключ попал в репозиторий, слой образа или перепискуключ считается скомпрометированным независимо от того, воспользовался ли им кто-тосм. «Отзыв ключа» ниже

Отзыв ключа

Смена ключа затрагивает обе стороны, порядок — от концентратора к пиру, чтобы старый ключ перестал приниматься до того, как начнётся возня с конфигурацией сервера.

bash
# на концентраторе: убрать блок [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 на машинах, где образ есть). Слой с ключом остаётся в кэше сборки и в реестре после того, как из репозитория файл убран.


Откат и снятие

Снять клиента в контейнере:

bash
docker compose stop vpn_client
docker compose rm -f vpn_client

Сервисы, у которых network_mode: "service:vpn_client", останавливаются вместе с ним: их сетевое пространство принадлежит удалённому контейнеру. Уберите эти сервисы из Compose-файла или переключите их на локальный вход — в сетевом контуре для этого рядом с vpn_nginx лежит закомментированный host_nginx.

Снять клиента на хосте:

bash
sudo systemctl disable --now wg-quick@wg0
ip link show wg0        # ожидается: Device "wg0" does not exist

Отозвать ключ на концентраторе — по разделу «Отзыв ключа»: блок [Peer] удаляется, wg syncconf применяет; после этого машина с этим ключом в туннель не входит.

Полностью снять концентратор:

bash
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
сети ComposeDockerdocker network prune после остановки проектов
образ клиента с вкомпилированной конфигурациейлокальный кэш образовdocker image prune -a

Пакеты wireguard и wireguard-tools удалять не нужно: без интерфейса и конфигурации они ничего не делают, а их наличие не открывает портов.

Инструкция не помогла?

Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.