MAATRIX / Блог / cloud-init: автонастройка VPN при создании сервера

cloud-init: автонастройка VPN при создании сервера

MAATRIX

Обычный сценарий: вы создаёте VPS, ждёте загрузки, заходите по SSH, ставите WireGuard или OpenVPN, генерируете ключи, правите конфиг, включаете форвардинг — и только через 10-15 минут ручной работы сервер готов раздавать VPN. Если серверы нужны регулярно (тестовые окружения, серверы под конкретную поездку, замена заблокированного IP), эта рутина утомляет и допускает ошибки от невнимательности. cloud-init убирает этот шаг: вы описываете конфигурацию один раз в user-data, и VPN поднимается сам в первые секунды жизни сервера — ещё до того, как вы успеете открыть терминал.

Что такое cloud-init и что происходит при первом старте VM

cloud-init — это сервис, который есть почти во всех облачных образах Ubuntu, Debian, AlmaLinux и других дистрибутивов. При создании сервера через панель управления или API провайдер передаёт в VM текстовый блок — user-data. cloud-init читает его на самом первом старте и выполняет описанные там действия: устанавливает пакеты, создаёт файлы, запускает команды.

Важная деталь: user-data выполняется один раз — при первой загрузке инстанса, определяемой по уникальному instance-id. Перезагрузка сервера cloud-init заново не запускает. Этим он отличается от systemd-юнита или крон-задачи: не про постоянную работу, а про начальную инициализацию — ровно то, что нужно для разового поднятия VPN при создании сервера.

Формат user-data определяется первой строкой файла. Самый частый и удобный для VPN-задач — #cloud-config, YAML-подобный декларативный формат с модулями packages, write_files, runcmd. Альтернатива — обычный shell-скрипт с шебангом #!/bin/bash, который просто выполняется как есть. Для сложных сценариев их можно объединить через multi-part MIME, но для одного VPN-сервера хватает одного #cloud-config.

Какой VPS выбрать для собственного VPN

Для личного VPN на телефоне, компьютере и домашних устройствах начните с Lite: 1 vCPU, 2 ГБ памяти и 40 ГБ NVMe. Это стартовый вариант для небольшого числа одновременно подключённых устройств. Если VPN будет обслуживать команду и другие приложения, рассмотрите Старт с 2 vCPU и 4 ГБ памяти.

Для личного VPN

Lite · 1 vCPU · 2 ГБ RAM
40 ГБ NVMe
Начальный тариф для своего VPN-сервера.

Заказать Lite

Для VPN и дополнительных сервисов

Старт · 2 vCPU · 4 ГБ RAM
80 ГБ NVMe
Больше памяти для нескольких приложений на одном сервере.

Заказать Старт

Аренда VPS для VPN: после заказа переходите к настройке по этой инструкции. Скорость зависит также от канала и маршрута до сервера.

В кнопках заказа выбрана локация в Великобритании. Другую страну можно выбрать при оформлении.

Оплата VPS картой РФ или по СБП доступна в личном кабинете. Для безналичной оплаты от юридического лица или ИП запросите счёт у support@maatrix.io, указав тариф и реквизиты.

Как передать user-data серверу

У большинства провайдеров VPS есть поле User Data прямо в форме создания сервера в панели — обычно на шаге выбора образа или в разделе «Advanced options». Туда вставляется содержимое файла целиком.

Через CLI или API это тоже одна опция:

# пример для облака с cloud-init API (синтаксис условный,
# у каждого провайдера свои названия параметров)
curl -X POST https://api.example-cloud.com/v1/servers \
  -H "Authorization: Bearer $API_TOKEN" \
  -d image=ubuntu-24-04 \
  -d size=2vcpu-4gb \
  -d region=fra1 \
  --data-urlencode "user_data@cloud-init.yaml"

Если вы уже используете Terraform для создания VPS, user-data передаётся через параметр ресурса (обычно называется user_data) с функцией templatefile(), которая подставляет в шаблон реальные значения — например, публичный ключ клиента — перед отправкой:

resource "cloud_server" "vpn" {
  name      = "vpn-01"
  image     = "ubuntu-24-04"
  size      = "1vcpu-1gb"
  region    = "fra1"
  user_data = templatefile("${path.module}/cloud-init-wg.yaml.tpl", {
    server_private_key = var.wg_server_private_key
    client_public_key  = var.wg_client_public_key
  })
}

Так весь процесс — от terraform apply до готового поднятого туннеля — укладывается в одну команду без единого ручного захода на сервер.

Готовый user-data для WireGuard

Ключи WireGuard лучше сгенерировать заранее на своей машине (wg genkey, wg pubkey), а не пытаться делать это внутри cloud-init — так проще контролировать, какой ключ куда попадает, и не завязываться на порядок выполнения модулей. Вот рабочий #cloud-config, который ставит WireGuard и поднимает интерфейс сразу после установки пакетов:

#cloud-config
package_update: true
packages:
  - wireguard
  - wireguard-tools

write_files:
  - path: /etc/wireguard/wg0.conf
    permissions: '0600'
    owner: root:root
    content: |
      [Interface]
      PrivateKey = SERVER_PRIVATE_KEY_HERE
      Address = 10.10.0.1/24
      ListenPort = 51820
      PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
      PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

      [Peer]
      PublicKey = CLIENT_PUBLIC_KEY_HERE
      AllowedIPs = 10.10.0.2/32

runcmd:
  - sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
  - sysctl -p
  - systemctl enable wg-quick@wg0
  - systemctl start wg-quick@wg0

Пара нюансов, которые часто ломают этот сценарий на практике:

  • Имя внешнего интерфейса в PostUp/PostDown (eth0) нужно проверить под конкретный образ — у части провайдеров это ens3 или enp1s0. Если ошибиться, wg-quick не упадёт, но NAT работать не будет, а трафик из туннеля наружу не пойдёт.
  • Модуль runcmd выполняется после write_files и packages, порядок в файле cloud-config гарантирован — можно не бояться, что сервис попытается стартовать раньше, чем появится конфиг.
  • Если вместо sed вы предпочитаете системный способ, вариант чище — положить net.ipv4.ip_forward=1 отдельным файлом в /etc/sysctl.d/99-wireguard.conf через тот же write_files.

Это тот же сервер, который получился бы после ручной настройки по шагам из статьи про установку WireGuard на Ubuntu 24.04 — просто без вашего участия и без риска что-то забыть на середине.

Вариант для OpenVPN: осторожнее с PKI

С OpenVPN автоматизация первого старта сложнее, потому что классическая схема требует полноценного центра сертификации (easy-rsa): генерация CA, серверного и клиентских сертификатов. Генерировать CA прямо в cloud-init — плохая идея: приватный ключ CA окажется в user-data в открытом виде, а user-data часто остаётся доступным через API/метаданные сервера ещё долго после создания.

Практичнее два подхода. Первый — сгенерировать PKI заранее (например, локально или на отдельном защищённом сервере) и передать через cloud-init уже готовые серверный сертификат, ключ и dh.pem:

#cloud-config
package_update: true
packages:
  - openvpn

write_files:
  - path: /etc/openvpn/server.conf
    content: |
      port 1194
      proto udp
      dev tun
      ca /etc/openvpn/ca.crt
      cert /etc/openvpn/server.crt
      key /etc/openvpn/server.key
      dh /etc/openvpn/dh.pem
      server 10.20.0.0 255.255.255.0
      push "redirect-gateway def1 bypass-dhcp"
      push "dhcp-option DNS 1.1.1.1"
      keepalive 10 120
      persist-key
      persist-tun
  - path: /etc/openvpn/ca.crt
    content: |
      -----BEGIN CERTIFICATE-----
      ...ваш CA-сертификат...
      -----END CERTIFICATE-----
  - path: /etc/openvpn/server.key
    permissions: '0600'
    content: |
      -----BEGIN PRIVATE KEY-----
      ...приватный ключ сервера...
      -----END PRIVATE KEY-----

runcmd:
  - sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
  - sysctl -p
  - iptables -t nat -A POSTROUTING -s 10.20.0.0/24 -o eth0 -j MASQUERADE
  - systemctl enable openvpn@server
  - systemctl start openvpn@server

Блок server.crt в примере опущен для краткости — в реальном файле он идёт тем же write_files-блоком, что и ca.crt. Второй подход — вообще не класть сертификаты в user-data, а скачать их при загрузке с защищённого эндпоинта по одноразовому токену (об этом ниже). Для полноценной установки с нуля, если автоматизация не нужна с первой секунды, разумнее ориентироваться на пошаговую установку OpenVPN на Ubuntu 24.04 и переносить в cloud-init только те шаги, которые действительно повторяются от сервера к серверу.

Ключи и секреты: чего не стоит класть в user-data напрямую

user-data — не защищённое хранилище секретов. На большинстве провайдеров его содержимое доступно через API аккаунта, а изнутри самой VM — через сервис метаданных (curl http://169.254.169.254/...) без какой-либо авторизации, кроме факта, что запрос идёт с самого сервера. Для приватного ключа WireGuard одного личного VPN-сервера это приемлемый риск: утечка требует либо доступа к панели, либо root на самой VM, которые и так критичны. Но масштабировать подход на что-то чувствительнее — CA-ключ, сертификаты для десятков клиентов — не стоит.

Более безопасная схема — не embedding, а fetch-at-boot: user-data не содержит секрет, а только команду на его получение с одноразовым или короткоживущим токеном:

#cloud-config
packages:
  - wireguard

runcmd:
  - curl -sSf -H "Authorization: Bearer ${ONE_TIME_TOKEN}" \
      https://secrets.example.internal/vpn/wg0.conf \
      -o /etc/wireguard/wg0.conf
  - chmod 600 /etc/wireguard/wg0.conf
  - systemctl enable --now wg-quick@wg0

Токен в этом случае живёт ровно до первого использования (сервер секретов его отзывает), а сам приватный ключ никогда не сохраняется в логах API-запроса на создание сервера. Для одного домашнего или рабочего VPN-сервера это избыточно, но если вы автоматизируете создание серверов регулярно и через общий CI/CD — это уже разумная гигиена, а не паранойя.

Проверка и отладка, если VPN не поднялся

Первое, что стоит сделать после создания сервера — не пытаться сразу подключиться клиентом, а проверить, что сам cloud-init отработал без ошибок:

cloud-init status --long

Статус done без ошибок в поле errors означает, что все модули завершились штатно. Если что-то пошло не так, детальный лог выполнения — в двух местах:

cat /var/log/cloud-init-output.log   # stdout/stderr всех команд из runcmd
cat /var/log/cloud-init.log          # подробный лог самого cloud-init построчно

Типичные причины, по которым VPN не поднимается, несмотря на «зелёный» статус cloud-init:

  • Опечатка в имени внешнего интерфейса в PostUp/PostDown — сервис стартует, но трафик не маршрутизируется.
  • IP-forwarding не применился, потому что sysctl -p выполнился раньше, чем изменения записались в файл (редко, но случается при нестандартном порядке модулей) — проверяется через sysctl net.ipv4.ip_forward, должно быть 1.
  • Провайдер режет исходящий UDP на нестандартный порт по умолчанию — стоит явно проверить правила firewall/security group на стороне панели, cloud-init их не трогает.
  • В write_files конфиг записался с неверными правами доступа, и сервис отказался стартовать из-за слишком открытого файла с приватным ключом — WireGuard и OpenVPN оба чувствительны к правам 600 на файлы с ключами.

Для отладки шаблона на уже существующей машине можно принудительно прогнать модули заново без пересоздания сервера:

sudo cloud-init clean --logs
sudo cloud-init init
sudo cloud-init modules --mode=config
sudo cloud-init modules --mode=final

Это не полный аналог первого старта (часть вещей привязана к первой загрузке ядра), но для отладки write_files и runcmd достаточно — и заметно быстрее, чем пересоздавать VM на каждую правку YAML.

Частые вопросы

Все ли провайдеры VPS поддерживают user-data через cloud-init?

Практически все крупные площадки — да, для образов на базе Ubuntu, Debian, AlmaLinux, Rocky Linux. Поддержка обычно не заявлена явно как «фича VPN», а идёт как общая возможность облачных образов — ищите поле «User Data» или «Cloud-init» на шаге создания сервера или в разделе Advanced.

Что если нужен не Ubuntu/Debian, а другой дистрибутив?

cloud-init есть и в облачных образах CentOS/AlmaLinux/Rocky, но набор доступных модулей и путей к пакетным менеджерам отличается — например, вместо apt нужен dnf, и в некоторых сборках cloud-init предустановлен не по умолчанию. Перед переносом скрипта на другой дистрибутив стоит проверить cloud-init --version и доступность нужных модулей.

Безопасно ли класть приватный ключ WireGuard прямо в user-data, как в примере?

Для личного или рабочего VPN-сервера — приемлемый компромисс: риск ограничен доступом к панели провайдера и root-доступом на самой VM, которые и так критичны для безопасности. Для чувствительных сценариев (CA-ключи, десятки клиентов) лучше схема с получением секрета по токену при загрузке, описанная выше.

Можно ли использовать один и тот же user-data для нескольких серверов?

Технически да, но для WireGuard и OpenVPN так делать не стоит — одинаковый приватный ключ или общий CA без разделения по клиентским сертификатам ломает саму модель безопасности VPN. Используйте шаблонизацию (Terraform templatefile или подстановку переменных перед отправкой) даже для похожих серверов.

cloud-init выполнится повторно, если развернуть сервер из snapshot?

Зависит от того, как провайдер обрабатывает instance-id при клонировании: часто новый сервер получает новый ID и user-data выполнится заново, но иногда ID сохраняется, и cloud-init решает, что уже всё сделал. Стоит проверить один раз на конкретном провайдере, прежде чем полагаться на снапшоты как способ клонирования VPN-серверов.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Готовы перейти к настройке?

Закажите Lite с конфигурацией, предложенной для старта, или сравните все тарифы. Оплата картой РФ и через СБП — в личном кабинете.

Заказать Lite Все тарифы и цены