Ротация ключей WireGuard: автоматизация
У WireGuard нет встроенного механизма ротации ключей — конфиг, который вы сгенерировали при установке, будет работать годами, если его не трогать. Это и плюс, и проблема: ключ, который никогда не менялся, — это ключ, компрометацию которого вы не заметите даже спустя месяцы после утечки. Ниже — рабочая схема, как менять приватные и публичные ключи WireGuard по расписанию, не разрывая туннель для тех, кто им пользуется прямо сейчас.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Почему ключи WireGuard вообще нужно ротировать
WireGuard использует пару Curve25519-ключей (приватный на клиенте и сервере, публичный — то, что уходит в конфиг собеседника) для установления сессии через Noise Protocol Framework. Сама сессия защищена forward secrecy — даже если украдут текущий сессионный ключ, старый трафик не расшифровать. Но это не относится к долгоживущей паре PrivateKey/PublicKey: она статична, пока вы её не смените, и именно она открывает доступ к туннелю.
Риски накапливаются со временем так же, как с любым другим секретом:
- конфиг клиента разошёлся дальше, чем планировалось.
.conf-файл переслали в мессенджере, он остался в бэкапе старого телефона, скопирован на флешку "на всякий случай"; - сотрудник ушёл, а его peer остался в
wg0.conf. Секция[Peer]без записи "кто это" через полгода уже не расшифровать — оставляют, потому что страшно сломать; - приватный ключ сервера утёк вместе с образом диска — снапшот, бэкап, скомпрометированный хостинг-провайдер;
- ключ сгенерирован слабым
wg genkeyна системе с плохим источником энтропии — актуально для старых embedded-роутеров и контейнеров без нормально инициализированного/dev/urandom.
Если у вас уже есть общий подход к секретам, ротация ключей WireGuard — просто ещё одна строчка в практике ротации секретов, а не отдельная дисциплина. Специфика тут одна: WireGuard не умеет "принять два ключа одновременно" на уровне интерфейса, поэтому переход всегда идёт через параллельный peer, а не через замену на месте.
Ключевая идея: новый peer до того, как убит старый
Главная ошибка при ручной ротации — сгенерировать новый ключ, тут же вписать его вместо старого в wg0.conf и перезапустить интерфейс. У клиента на руках всё ещё старый конфиг — рукопожатие не пройдёт, пока вы физически не доставите новый файл. Для одного человека это пять минут простоя, для парка из полусотни клиентов — поток тикетов "не работает VPN".
Правильный порядок:
- Генерируете новую пару ключей для peer'а, не трогая старую.
- Добавляете на сервере вторую секцию
[Peer]с новымPublicKeyдля того же клиента — временно у него два действующих peer'а. - Раздаёте клиенту новый конфиг (файл, QR-код — как удобнее).
- Клиент подтверждает, что подключился именно новым ключом — проверяете
wg showпо последнему handshake. - Только после этого удаляете старую секцию
[Peer]с сервера.
Тот же принцип, но для ключа самого сервера, требует ещё одного слоя: серверный PublicKey прописан у всех клиентов, так что смена серверного ключа — это рассылка обновлённых конфигов всем разом до того, как старый ключ выключат. Проще всего это делается через второй интерфейс wg1 с новым ключом сервера, поднятый параллельно wg0, — клиенты переезжают по расписанию, а не одномоментно.
Арендуйте сервер под свои задачи!
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверРучная ротация ключа одного peer'а
Разберём на примере одного клиента laptop-ivan. Текущая секция на сервере:
# /etc/wireguard/wg0.conf
[Peer]
# laptop-ivan, добавлен 2026-01-14
PublicKey = old_XjW9k3QoZ2yF...=
AllowedIPs = 10.8.0.5/32
Генерируем новую пару на клиенте (или на сервере, если ключи выпускает админ):
wg genkey | tee laptop-ivan-new.key | wg pubkey > laptop-ivan-new.pub
chmod 600 laptop-ivan-new.key
Добавляем вторую секцию на сервере с тем же AllowedIPs, но новым ключом и новым IP из туннельной подсети — двум peer'ам нельзя отдать один и тот же AllowedIPs, WireGuard просто откажется маршрутизировать однозначно:
[Peer]
# laptop-ivan, ротация ключа, добавлен 2026-08-20
PublicKey = new_Qp8Ldm4Vn7Rt...=
AllowedIPs = 10.8.0.55/32
Применяем без разрыва существующих сессий — wg syncconf не рвёт активные peer'ы, в отличие от wg-quick down/up:
wg syncconf wg0 <(wg-quick strip wg0)
Собираем новый клиентский конфиг с адресом 10.8.0.55/32, тем же Endpoint и тем же публичным ключом сервера, доставляем клиенту. После того как wg show wg0 latest-handshakes покажет свежее время у нового peer'а, старую секцию убираем и повторяем wg syncconf.
Автоматизация: скрипт ротации по расписанию
Ручной процесс нормально масштабируется до пары клиентов, дальше нужен скрипт. Ниже — рабочий вариант для ротации ключа одного peer'а с уведомлением и логом, который можно поставить на systemd-таймер.
#!/usr/bin/env bash
# /usr/local/sbin/wg-rotate-peer.sh
set -euo pipefail
PEER_NAME="$1" # например laptop-ivan
WG_IFACE="wg0"
WG_DIR="/etc/wireguard"
PEERS_DB="$WG_DIR/peers.tsv" # name pubkey ip added_at
LOG="/var/log/wg-rotation.log"
log() { echo "$(date -Iseconds) $*" | tee -a "$LOG"; }
# новая пара ключей
priv=$(wg genkey)
pub=$(echo "$priv" | wg pubkey)
# следующий свободный IP в подсети 10.8.0.0/24
last_octet=$(awk -F. '{print $4}' <(cut -f3 "$PEERS_DB" | cut -d/ -f1) | sort -n | tail -1)
new_ip="10.8.0.$((last_octet + 1))/32"
# добавляем временную вторую секцию peer'а
cat >> "$WG_DIR/$WG_IFACE.conf" <<EOF
[Peer]
# $PEER_NAME, ротация $(date -Iseconds)
PublicKey = $pub
AllowedIPs = $new_ip
EOF
wg syncconf "$WG_IFACE" <(wg-quick strip "$WG_IFACE")
log "Добавлен новый ключ для $PEER_NAME, IP $new_ip, ожидание handshake"
# собираем клиентский конфиг рядом, для последующей раздачи
server_pub=$(cat "$WG_DIR/server_public.key")
endpoint=$(cat "$WG_DIR/endpoint.txt")
cat > "/root/wg-clients/${PEER_NAME}-new.conf" <<EOF
[Interface]
PrivateKey = $priv
Address = $new_ip
DNS = 1.1.1.1
[Peer]
PublicKey = $server_pub
Endpoint = $endpoint
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
EOF
echo -e "$PEER_NAME\t$pub\t$new_ip\t$(date -Iseconds)\tpending" >> "$PEERS_DB"
log "Конфиг сохранён: /root/wg-clients/${PEER_NAME}-new.conf — доставьте клиенту и запустите wg-confirm-rotation.sh $PEER_NAME"
Скрипт сознательно не удаляет старый ключ автоматически — это отдельный шаг подтверждения, потому что автоматическое удаление вслепую способно отрезать клиента, который просто не успел получить новый файл.
Второй скрипт закрывает цикл после того, как вы вручную (или через чат-бота, если раздача конфигов у вас автоматизирована) убедились, что клиент переехал:
#!/usr/bin/env bash
# /usr/local/sbin/wg-confirm-rotation.sh
set -euo pipefail
PEER_NAME="$1"
WG_IFACE="wg0"
WG_DIR="/etc/wireguard"
new_pub=$(grep "^$PEER_NAME" "$WG_DIR/peers.tsv" | tail -1 | cut -f2)
handshake_age=$(wg show "$WG_IFACE" latest-handshakes | grep "$new_pub" | awk '{print $2}')
now=$(date +%s)
if [ -n "$handshake_age" ] && [ $((now - handshake_age)) -lt 300 ]; then
echo "Handshake свежий — удаляем старый ключ $PEER_NAME"
# поиск и удаление старой секции [Peer] по имени в комментарии
else
echo "Свежего handshake нет — клиент ещё не переключился, старый ключ не трогаем"
exit 1
fi
Саму правку wg0.conf лучше делать не через sed, а небольшим Python-скриптом с честным парсером INI-подобного формата — на боевом конфиге ломать структуру регулярками рискованно. Если у вас уже есть Ansible для массового развёртывания WireGuard, логику ротации выгоднее оформить отдельной ролью, а не bash-обвязкой поверх — плейбук проще тестировать на staging.
systemd-таймер вместо cron
Для регулярного запуска (например, ротация всех ключей раз в 90 дней) systemd-таймер даёт нормальные логи через journalctl и не требует отдельного демона cron, если он у вас и так не используется:
# /etc/systemd/system/wg-rotate.service
[Unit]
Description=WireGuard key rotation batch
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/wg-rotate-batch.sh
# /etc/systemd/system/wg-rotate.timer
[Unit]
Description=Запуск ротации ключей WireGuard раз в 90 дней
[Timer]
OnCalendar=*-01,04,07,10-01 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now wg-rotate.timer
systemctl list-timers wg-rotate.timer
wg-rotate-batch.sh — обёртка, которая проходит по peers.tsv, для каждого peer'а старше 90 дней запускает wg-rotate-peer.sh и шлёт уведомление со списком тех, кому нужно забрать новый конфиг. Важно не рвать всех разом: при 50 клиентах разумнее растянуть партиями по 5-10 в день, а не получить одновременно полсотни тикетов "не могу подключиться".
Ротация ключа сервера — отдельный случай
Смена приватного ключа сервера сложнее ротации peer'а: новый PublicKey сервера нужно раздать *всем* клиентам одновременно — старый и новый серверные ключи не работают в одном интерфейсе параллельно так же гладко, как несколько peer-ключей.
Практичная схема — второй интерфейс:
# генерируем новую пару для сервера
wg genkey | tee /etc/wireguard/server_new.key | wg pubkey > /etc/wireguard/server_new.pub
# поднимаем wg1 с новым серверным ключом на соседнем порту
cat > /etc/wireguard/wg1.conf <<EOF
[Interface]
PrivateKey = $(cat /etc/wireguard/server_new.key)
Address = 10.9.0.1/24
ListenPort = 51821
[Peer]
# сюда copy-paste всех текущих клиентов с новыми AllowedIPs в подсети 10.9.0.0/24
EOF
wg-quick up wg1
Дальше — та же логика поэтапного переезда, только массово: клиентам раздаются конфиги с новым Endpoint (тот же IP сервера, порт wg1) и новым PublicKey сервера. Когда wg show wg1 показывает свежие handshake у всех, старый интерфейс останавливают:
wg-quick down wg0
systemctl disable wg-quick@wg0
Если переезд идёт не за один день, а размазан по неделе — старый wg0 можно не выключать полностью, а просто перестать добавлять туда новых клиентов, дав старым доехать в своём темпе. Это ровно тот сценарий, что описан в материале про миграцию между VPN-протоколами без даунтайма — принцип параллельного запуска и поэтапного переноса тот же, просто здесь меняется не протокол, а ключевая пара внутри одного протокола.
Что учитывать при выборе периодичности
Универсального "правильного" интервала нет — это компромисс между окном уязвимости и нагрузкой на команду. Ориентиры:
| Сценарий | Разумный интервал | Почему |
|---|---|---|
| Личный VPN, 1-2 устройства | Раз в 6-12 месяцев или по факту утечки | Риск компрометации низкий, автоматизация не окупается |
| Команда до 10 человек | Раз в 90 дней | Баланс между нагрузкой на онбординг и снижением риска |
| VPN с доступом к продовой инфраструктуре | Раз в 30 дней или после увольнения любого сотрудника | Высокая цена компрометации оправдывает частую ротацию |
| IoT / встраиваемые устройства | По событию (замена устройства, инцидент) | Плановая ротация часто физически неудобна — нет UI для смены конфига |
| Peer с истёкшим доступом (подрядчик, стажёр) | Немедленно при завершении контракта | Не ротация, а отзыв — секция [Peer] удаляется, а не переносится |
Если у вас настроен аудит подключений VPN, логи последних handshake — хороший сигнал для приоритизации: ключ, которым не пользовались три месяца, скорее кандидат на отзыв, чем на ротацию.
Арендуйте сервер под свои задачи!
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Обязательно ли менять IP при ротации ключа peer'а?
Нет, но если оставить старый AllowedIPs, старую секцию [Peer] придётся удалить перед добавлением новой — а значит, пропадает окно параллельной работы обоих ключей. Новый IP на время ротации — цена за отсутствие даунтайма.
Можно ли ротировать ключи без перезапуска интерфейса wg0?
Да, для этого нужен wg syncconf вместо wg-quick down && wg-quick up — команда применяет диф конфигурации на лету, не разрывая сессии других peer'ов.
Что делать, если клиент не может забрать новый конфиг сразу?
Оставьте параллельные секции [Peer] дольше стандартного окна — старая продолжит работать, пока клиент не подтвердит переезд. Поэтому подтверждение handshake — отдельный шаг, а не слепая автоматика по таймеру.
Нужна ли ротация, если сервер за NAT и не торчит наружу напрямую?
NAT не защищает от компрометации самого конфига — если файл утёк из бэкапа или мессенджера, атакующему нужен только Endpoint, а не прямая видимость сервера. Ротация остаётся актуальной независимо от топологии сети.
Как быть с ключами на роутерах (MikroTik, Keenetic), где нет bash?
Логику "новый peer — подтвердить — удалить старый" ведут с управляющего сервера через SSH и CLI/API соответствующего вендора — сам скрипт остаётся на сервере, роутер только получает готовый конфиг.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →