MAATRIX / Блог / PowerShell-автоматизация установки VPN-сервера на Windows Server

PowerShell-автоматизация установки VPN-сервера на Windows Server

MAATRIX

Если вы разворачивали RRAS-роль через мастер в Server Manager хотя бы пару раз, вы знаете это ощущение: десяток экранов, чекбоксы, которые легко перепутать местами, и никакой уверенности, что второй сервер настроен точно так же, как первый. Когда серверов не один, а три-пять — в разных локациях, с разными диапазонами адресов — ручная настройка превращается в источник расхождений. Решение простое: перенести процесс в PowerShell-скрипт, который устанавливает роль, включает VPN-функциональность и настраивает параметры туннеля одной командой, одинаково на любом сервере.

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

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

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

Почему GUI-мастер не годится для повторяемого деплоя

Мастер добавления ролей в Server Manager хорош для разовой настройки одного сервера, когда вы сидите за консолью. У него есть несколько системных проблем, которые вылезают, как только серверов становится больше одного:

  • Нет журнала действий. Через полгода никто не вспомнит, какие галочки стояли при установке — только итоговый Get-VpnServerConfiguration, по которому сложно восстановить логику выбора.
  • Расхождение конфигураций. Второй сервер в другой локации почти неизбежно настраивается чуть иначе — другой порядок кликов, забытая галочка, другой диапазон адресов «на глаз».
  • Не автоматизируется. Мастер требует GUI-сессии, поэтому его нельзя вызвать из Server Core, из Ansible/DSC-плейбука или из пайплайна первичной настройки образа.
  • Долго на масштабе. Настройка одного сервера руками — 15-20 минут. При деплое пяти серверов это уже час монотонных кликов с риском ошибиться на четвёртом от усталости.

PowerShell-модуль RemoteAccess даёт все те же возможности, что и мастер, но в виде командлетов, которые можно собрать в один идемпотентный скрипт: запустили — получили одинаковый результат на любом сервере, хоть в России, хоть в США, хоть в Британии.

Подготовка окружения и переменных скрипта

Скрипту нужны права локального администратора и Windows Server 2019 или 2022 (на 2016 модуль тоже есть, но часть параметров вроде Set-VpnIPAddressAssignment появилась позже — проверяйте на конкретной сборке). Сервер должен иметь статический внутренний IP — DHCP на сетевом адаптере, который будет обслуживать VPN-роль, приведёт к проблемам при перезапуске службы.

Вынесите все специфичные для сервера значения в блок параметров вверху скрипта — это то, что реально отличает один сервер от другого:

[CmdletBinding()]
param(
    [ValidateSet('IKEv2','SSTP','L2TP','PPTP')]
    [string[]]$TunnelTypes = @('IKEv2','SSTP'),

    [string]$AddressPoolStart = '10.10.10.2',
    [string]$AddressPoolEnd   = '10.10.10.254',

    [string]$DnsServer = '10.10.10.1',

    [ValidateSet('MachineCertificates','EAP')]
    [string]$IkeAuth = 'MachineCertificates',

    [string]$CertSubject = 'CN=vpn.example.com'
)

$ErrorActionPreference = 'Stop'
Import-Module RemoteAccess -ErrorAction Stop

$ErrorActionPreference = 'Stop' важен: без него часть командлетов RemoteAccess молча проглатывает ошибки и продолжает выполнение, оставляя сервер в промежуточном состоянии — роль вроде установлена, а VPN не поднялся.

Арендуйте сервер под свои задачи!

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Установка роли через Install-WindowsFeature

Первый шаг — установка компонентов DirectAccess-VPN и Routing (последний нужен, если планируете NAT или маршрутизацию между сетями, а не только терминацию клиентских туннелей):

$feature = Get-WindowsFeature -Name DirectAccess-VPN
if (-not $feature.Installed) {
    Write-Verbose 'Устанавливаю роль RemoteAccess (VPN)...'
    $result = Install-WindowsFeature -Name DirectAccess-VPN, Routing `
        -IncludeManagementTools

    if ($result.RestartNeeded -eq 'Yes') {
        Write-Warning 'Требуется перезагрузка. Запустите скрипт повторно после reboot.'
        exit 3010
    }
} else {
    Write-Verbose 'Роль DirectAccess-VPN уже установлена, пропускаю.'
}

Проверка if (-not $feature.Installed) — не формальность, а основа идемпотентности: повторный запуск скрипта на уже настроенном сервере не должен падать с ошибкой «роль уже установлена» и не должен пытаться переустановить то, что работает. Код возврата 3010 — стандартный признак «нужен перезапуск» (ERROR_SUCCESS_REBOOT_REQUIRED), его удобно ловить во внешнем оркестраторе — Ansible или Task Scheduler-задаче, которая перезапускает сервер и повторяет вызов скрипта.

Активация VPN через Install-RemoteAccess

Установка feature ещё не включает саму службу маршрутизации и удалённого доступа — для этого нужен отдельный командлет:

$ra = Get-RemoteAccess -ErrorAction SilentlyContinue
if (-not $ra -or $ra.VpnStatus -ne 'Installed') {
    Write-Verbose 'Включаю VPN-функциональность RemoteAccess...'
    Install-RemoteAccess -VpnType Vpn
} else {
    Write-Verbose 'RemoteAccess VPN уже включён.'
}

Параметр -VpnType принимает три значения: Vpn — обычный сервер удалённого доступа для клиентов (наш случай), VpnS2S — туннели сайт-сайт между офисами, VpnS2SWithClustering — то же самое, но с поддержкой отказоустойчивого кластера RRAS. Если позже понадобится совместить клиентский доступ и связь между площадками, Install-RemoteAccess можно вызвать повторно с другим значением — командлет умеет докручивать конфигурацию, а не только ставить с нуля.

После этого шага стоит явно перезапустить службу, чтобы конфигурация точно применилась, прежде чем переходить к настройке туннелей:

Restart-Service RemoteAccess -Force
Start-Sleep -Seconds 5

Настройка туннелей и пула адресов

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

Set-VpnServerConfiguration `
    -TunnelType $TunnelTypes `
    -AuthenticationMethod $IkeAuth `
    -DeviceComplianceEnabled $false `
    -Force

PPTP в списке ValidateSet оставлен только для совместимости со старыми клиентами — для новых развёртываний его использовать не стоит: протокол считается устаревшим и небезопасным, о деталях мы писали в статье про WireGuard и OpenVPN. Для продакшена разумный набор — IKEv2 и SSTP: первый быстро восстанавливает соединение при смене сети (мобильные клиенты), второй проходит почти через любой файрвол, потому что использует порт 443.

Пул адресов для клиентов задаётся отдельным командлетом:

Set-VpnIPAddressAssignment `
    -StaticAddressPoolUsage Full `
    -StaticAddressPool "$AddressPoolStart-$AddressPoolEnd"

Диапазон должен быть отдельным от адресов, которые сервер раздаёт внутренней сети по DHCP — пересечение приведёт к конфликтам IP у клиентов и обычных устройств в локальной сети.

DNS для VPN-клиентов в чистом модуле RemoteAccess напрямую не выставляется — проще прописать его через параметры сетевого адаптера, обслуживающего VPN, либо через NPS-политику при аутентификации через RADIUS. Если клиенты подключаются, но не резолвят внутренние имена, чаще всего дело именно в DNS, а не в самом туннеле.

Сертификат и правила файрвола

IKEv2 и SSTP требуют сертификата на сервере — без него служба поднимется, но клиенты не смогут подключиться с ошибкой проверки сертификата. Для лабораторной проверки самоподписанный сертификат создаётся прямо в скрипте:

$existingCert = Get-ChildItem Cert:\LocalMachine\My |
    Where-Object { $_.Subject -eq $CertSubject } |
    Select-Object -First 1

if (-not $existingCert) {
    New-SelfSignedCertificate `
        -DnsName ($CertSubject -replace '^CN=','') `
        -CertStoreLocation Cert:\LocalMachine\My `
        -KeyExportPolicy Exportable `
        -NotAfter (Get-Date).AddYears(2) | Out-Null
}

Для продакшена самоподписанный сертификат подойдёт только если вы вручную ставите его в доверенные на каждом клиенте — иначе разумнее сертификат от публичного CA или от собственного корневого центра, выпущенного через Windows CA (мы разбирали этот процесс отдельно — сертификаты для IKEv2 и SSTP через Windows CA).

Дальше — правила файрвола. По умолчанию установка роли открывает часть портов автоматически, но полагаться на это не стоит — явные правила надёжнее и понятнее при аудите:

$rules = @(
    @{ Name='VPN-IKEv2-UDP500';  Proto='UDP'; Port=500  }
    @{ Name='VPN-IKEv2-UDP4500'; Proto='UDP'; Port=4500 }
    @{ Name='VPN-SSTP-TCP443';   Proto='TCP'; Port=443  }
    @{ Name='VPN-L2TP-UDP1701';  Proto='UDP'; Port=1701 }
)

foreach ($r in $rules) {
    if (-not (Get-NetFirewallRule -DisplayName $r.Name -ErrorAction SilentlyContinue)) {
        New-NetFirewallRule -DisplayName $r.Name -Direction Inbound `
            -Protocol $r.Proto -LocalPort $r.Port -Action Allow | Out-Null
    }
}

Если планируете L2TP/IPsec, учтите отдельную грабли: протокол ESP (IP protocol 50) не имеет номера порта в привычном смысле и часто блокируется провайдерским или облачным файрволом на уровне выше Windows — стоит проверить правила у хостера отдельно от локального New-NetFirewallRule. Подробнее про типичные ошибки файрвола при настройке туннеля — в статье про настройку файрвола Windows Server для туннеля.

Полный скрипт и повторяемость

Собранный вместе, скрипт — последовательность идемпотентных шагов: каждый сначала проверяет текущее состояние и только потом что-то меняет. Это ключевое свойство для повторяемого деплоя: deploy-vpn.ps1 можно запускать через Invoke-Command -ComputerName на список серверов, через Ansible-модуль win_shell, или включить в сценарий первичной настройки образа — результат будет одинаковым независимо от того, запускали вы его на чистой машине или второй раз на уже настроенной.

Пара практических моментов:

  • Не храните shared secret или пароли сертификата в открытом виде в скрипте. Передавайте их как SecureString-параметр или тяните из хранилища секретов (Azure Key Vault, HashiCorp Vault, зашифрованная CI-переменная) — исходник скрипта обычно лежит в git, а git не место для паролей.
  • Логируйте каждый шаг. Start-Transcript -Path "C:\Logs\vpn-deploy-$(Get-Date -Format yyyyMMdd-HHmmss).log" в начале скрипта экономит часы при разборе, почему на одном из пяти серверов конфигурация не применилась.
  • Разделяйте установку роли и настройку. Если Install-WindowsFeature требует перезагрузки, скрипт должен корректно остановиться и дать оркестратору перезапустить его после reboot — не обходите это костылями вроде продолжения без рестарта, RRAS такое не прощает.
  • Проверяйте автозапуск службы после деплоя — установка роли обычно включает автозапуск RemoteAccess, но при клонировании образов настройка иногда «слетает». Подробнее — в статье про автозапуск служб Windows Server после перезагрузки.

Проверка после установки

После выполнения скрипта убедитесь, что конфигурация действительно применилась, а не просто отработала без ошибок — это разные вещи:

Get-RemoteAccess | Format-List VpnStatus, VpnType
Get-VpnServerConfiguration | Format-List TunnelType, AuthenticationMethod
Get-VpnAuthProtocol
Get-VpnIPAddressAssignment 2>$null

VpnStatus должен показывать Installed, а TunnelType — тот список протоколов, что задан в параметрах. Если что-то не совпадает, посмотрите журнал событий:

Get-WinEvent -LogName 'Microsoft-Windows-RasAgileVpn/Operational' -MaxEvents 20
Get-WinEvent -LogName System -MaxEvents 50 |
    Where-Object { $_.ProviderName -like '*RemoteAccess*' -or $_.ProviderName -like '*RRAS*' }

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

Арендуйте сервер под свои задачи!

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

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

Можно ли запускать скрипт повторно на уже настроенном сервере?

Да, если все шаги идемпотентны — с проверкой Get-WindowsFeature, Get-RemoteAccess и существующих правил файрвола перед изменением, как показано выше. Без этих проверок повторный запуск упадёт на командлетах, которые не любят применяться дважды.

Обязательна ли перезагрузка после Install-WindowsFeature?

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

Работает ли это на Server Core, без GUI?

Да, весь модуль RemoteAccess доступен на Server Core — в этом и смысл: PowerShell не завязан на графическую консоль, в отличие от мастера в Server Manager.

Как передать пароль сертификата или shared secret безопасно?

Через параметр типа SecureString, зашифрованную CI/CD-переменную или диспетчер секретов вроде Azure Key Vault — но не как обычную строку в скрипте, попадающем в систему контроля версий.

Можно ли этим же скриптом настроить IKEv2, SSTP и L2TP одновременно?

Да, -TunnelType принимает массив значений — сервер слушает все указанные протоколы параллельно, клиент сам выбирает, каким подключиться.

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

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

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