Защита RDP: как закрыть Windows Server от атак
Свежеустановленный Windows-сервер собирает тысячи неудачных попыток входа по RDP за считаные часы. Это руководство показывает, какие меры действительно работают, чего стоит каждая и как не отрезать доступ себе самому.
Windows-сервер с открытым портом 3389 атакуют не когда-нибудь, а в течение первых минут. Сканеры работают непрерывно, они знают каждый диапазон адресов IPv4 и тупо перебирают имена пользователей и пароли. Почти каждый случай заражения шифровальщиком на небольших серверах начинается ровно в этой точке: учётная запись с именем Administrator, пароль, который кто-то счёл достаточным, и отсутствие блокировки после тысячной неудачной попытки.
Это руководство разбирает меры в порядке их реальной эффективности, а не в том порядке, в каком они стоят в большинстве статей. Все команды выполняются в сеансе PowerShell с правами администратора на Windows Server версий с 2016 по 2025.
Почему RDP остаётся самой атакуемой дверью
RDP не плохой протокол. Проблема в том, что он выставляет полноценную форму входа в открытый интернет и что Windows по умолчанию разрешает заполнять эту форму неограниченное число раз. Атакующему не нужна уязвимость, ему нужны только терпение и список паролей.
Насколько сильно затронут ваш сервер, видно из одной строки. Посчитайте неудачные попытки входа за последние 24 часа:
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddDays(-1)} -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count
Внутренний сервер без доступа из интернета обычно даёт несколько десятков событий в сутки, в основном это забытые пароли и старые служебные учётные записи. Сервер с открытым 3389 быстро выходит на четырёх- и пятизначные числа. Это число и есть ваша точка отсчёта: после описанных ниже мер оно должно упасть на порядки. Если Get-WinEvent отвечает сообщением «Не найдено событий, соответствующих указанным условиям выбора», значит ваш сервер вообще не пишет журнал неудачных входов. Это мы исправим ниже.
До первого изменения: обеспечьте себе путь назад
Любая из следующих мер может отрезать вам доступ. На сервере, к которому вы подключаетесь только по RDP, это конец сеанса и начало долгого вечера. Три предосторожности стоят пяти минут и предотвращают ровно это.
Во-первых: проверьте, есть ли у вас консоль, работающая независимо от RDP. У KVM root-серверов KernelHost в личном кабинете есть VNC-консоль, которая смотрит прямо на экран виртуальной машины. Она продолжает работать даже тогда, когда файрвол, сеть и служба RDP сломаны одновременно. Откройте её один раз для пробы до того, как что-то менять, а не после.
Во-вторых: заведите вторую учётную запись администратора, чтобы заблокированный логин не означал потерянный сервер. Имена групп зависят от языка системы, поэтому мы работаем с фиксированными идентификаторами SID (S-1-5-32-544 — локальная группа администраторов, S-1-5-32-555 — пользователи удалённого рабочего стола):
New-LocalUser -Name 'kh-adm' -Password (Read-Host -AsSecureString -Prompt 'Пароль') -FullName 'Сервисная учётная запись' -PasswordNeverExpires
Add-LocalGroupMember -SID 'S-1-5-32-544' -Member 'kh-adm'
Add-LocalGroupMember -SID 'S-1-5-32-555' -Member 'kh-adm'
Третья строка, строго говоря, лишняя, потому что члены локальной группы администраторов и так заходят по RDP. Но вреда она не приносит и делает намерение явным на случай, если позже вы уберёте эту учётную запись из группы администраторов.
В-третьих: не закрывайте RDP-сеанс, в котором вы работаете, на всё время перенастройки и проверяйте каждое изменение вторым, новым подключением. Существующий сеанс новые правила файрвола сразу не разрывают, а новое подключение сорвётся немедленно. Так вы заметите ошибку, пока ещё можете её отменить.
Принудительная проверка подлинности на уровне сети
Без проверки подлинности на уровне сети (по-английски Network Level Authentication, сокращённо NLA) сервер сначала создаёт сеанс, рисует экран входа и только потом спрашивает учётные данные. Каждая анонимная попытка подключения стоит оперативной памяти и времени CPU, а весь код за экраном входа доступен ещё до какой-либо аутентификации. Именно там сидели тяжёлые уязвимости RDP прошлых лет.
С NLA сервер проверяет учётные данные через CredSSP до того, как сеанс вообще возникнет. Бот без действительных данных получает только отклонённое TLS-соединение.
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'UserAuthentication' -Value 1
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'SecurityLayer' -Value 2
SecurityLayer со значением 2 принуждает к TLS при согласовании. Значение 1 (согласование) позволяет клиенту в крайнем случае откатиться на старый уровень безопасности RDP, 0 это старый уровень без TLS. Проверка:
Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' | Select-Object UserAuthentication, SecurityLayer
Перезагрузка не нужна, значения действуют для каждого вновь устанавливаемого соединения. Существующие сеансы продолжают работать без изменений, что удобно, если вы как раз отрезали себе клиент.
Если после этого никто больше не заходит
После включения NLA регулярно всплывают два типа ошибок.
«Удалённому компьютеру требуется проверка подлинности на уровне сети, которую данный компьютер не поддерживает. Обратитесь за помощью к системному администратору или в службу технической поддержки.» Клиент слишком стар или не понимает CredSSP. Актуальные клиенты Windows умеют это уже много лет. В Linux FreeRDP требует ключ /sec:nla, Remmina требует выставить протокол безопасности на NLA, а очень старые клиенты macOS не подключатся в принципе. Решение — всегда более новый клиент, а не отключение NLA.
Вход не проходит с правильным паролем. Это ловушка, о которой умалчивает большинство руководств: если у учётной записи установлен флаг «Требовать смену пароля при следующем входе в систему» или срок действия пароля истёк, с включённым NLA эта учётная запись вообще не сможет войти. CredSSP не умеет менять пароль, так и задумано. Сервер в зависимости от версии сообщает об ошибке аутентификации или просто о неверных учётных данных. Выход ведёт через консоль в личном кабинете, либо вы исключаете сервисные учётные записи из срока действия пароля:
Set-LocalUser -Name 'kh-adm' -PasswordNeverExpires $true
Блокировка учётной записи: самый действенный отдельный переключатель
Сильный пароль защищает от угадывания, блокировка учётной записи защищает от неограниченного угадывания. Без неё атакующий вправе делать миллионы попыток в неделю, с ней их десять за четверть часа. Задайте порог, длительность блокировки и окно наблюдения одним вызовом, иначе Windows отклонит длительность, пока порог ещё равен 0:
net accounts /lockoutthreshold:10 /lockoutduration:15 /lockoutwindow:15
Длительность блокировки всегда должна быть больше окна наблюдения или равна ему. Проверка результата:
net accounts
Более новые версии Windows уже приносят значение такого порядка по умолчанию, старые установки и многие образы провайдеров нет. Проверка ничего не стоит.
Включить в блокировку встроенную учётную запись администратора
Исторически именно та учётная запись, которую атакующий пробует первой, была исключена из блокировки. Для этого есть политика «Разрешить блокировку учётной записи администратора». Правда, она требует более свежей версии, а именно Windows 11 22H2 или Windows Server 2025. На Windows Server 2022 (сборка 20348) экспорт политики безопасности вообще не содержит строки AllowAdministratorLockout, в разделе [System Access] там стоят только значения вроде LockoutBadCount и MinimumPasswordLength. Поэтому сначала проверьте, что именно экспортирует ваша система:
secedit /export /cfg C:\secpol.inf
Select-String -Path C:\secpol.inf -Pattern 'AllowAdministratorLockout'
Если совпадений нет, ваша сборка эту политику не знает, и раздел для вас на этом закрыт. Ровно здесь кроется ловушка, которую создаёт большинство руководств: обычный однострочник с -replace заменяет строку, которой в файле вообще нет, но об ошибке не сообщает, а последующий secedit /configure рапортует об успехе. После этого вы считаете, что блокировка администратора включена, хотя не изменилось ничего.
На сборке, которая эту политику знает, вы меняете строку, если она есть, а иначе вставляете её. Такой вариант покрывает оба случая:
$c = Get-Content C:\secpol.inf
if ($c -notmatch 'AllowAdministratorLockout') { $c = $c -replace '(LockoutBadCount = \d+)', "`$1`r`nAllowAdministratorLockout = 1" } else { $c = $c -replace 'AllowAdministratorLockout = 0','AllowAdministratorLockout = 1' }
$c | Set-Content C:\secpol.inf -Encoding Unicode
-Encoding Unicode обязателен, и это не деталь: файл INF должен быть в UTF-16 LE с меткой порядка байтов. Иначе Set-Content в Windows PowerShell 5.1 запишет ANSI, и secedit откажется принимать файл. Перед обратной записью файл проверяет secedit /validate C:\secpol.inf, затем:
secedit /configure /db C:\Windows\security\local.sdb /cfg C:\secpol.inf /areas SECURITYPOLICY
И после этого обязательно перечитайте результат, потому что успешный запуск secedit в этом месте ничего не доказывает. Экспортируйте политику ещё раз во второй файл и посмотрите, действительно ли значение стоит с 1.
На контроллере домена эти локальные настройки не работают. Там политика блокировки задаётся в Default Domain Policy: Конфигурация компьютера, Конфигурация Windows, Параметры безопасности, Политики учётных записей.
Обратная сторона и как вернуться обратно
Блокировка учётной записи работает и против вас: тот, кто знает ваше имя пользователя, может держать его заблокированным постоянно, отправляя каждые 15 минут по десять неверных паролей. Именно поэтому блокировка остаётся лишь второй линией обороны, а первой служит ограничение по IP-адресам из следующего раздела.
Если локальная учётная запись заблокирована, клиент сообщает примерно следующее: «Учётная запись заблокирована и не может быть использована для входа в систему». Простейший путь назад — подождать, потому что по истечении длительности блокировки Windows снимает её сам. Кто ждать не хочет, снимает блокировку через консоль:
$u = [ADSI]"WinNT://./kh-adm,user"; $u.IsAccountLocked = $false; $u.SetInfo()
В Active Directory это короче: Unlock-ADAccount -Identity kh-adm.
Пароли, которые придают блокировке смысл
Десять попыток за четверть часа становятся препятствием только тогда, когда пароль не стоит на третьем месте в любом списке. Минимальную длину и сложность задают так:
net accounts /minpwlen:14
Правило сложности снова лежит в политике безопасности, раздел [System Access], ключ PasswordComplexity = 1. Путь тот же, что и выше, через secedit.
Два замечания из практики. Во-первых, принудительная смена пароля каждые 30 дней доказуемо даёт мало и в сочетании с NLA порождает ровно ту проблему входа из предыдущего раздела. Длинные пароли, заданные один раз и сохранённые в менеджере паролей, лучше. Во-вторых, политика действует только для вновь задаваемых паролей. Существующий пароль из шести символов остаётся действительным, пока вы его не смените.
Ограничить доступ известными IP-адресами
Это та мера, которая действительно прекращает атакующий трафик, причём полностью. Все правила группы удалённого рабочего стола можно ограничить списком адресов. Используйте языконезависимый идентификатор группы, чтобы скрипт работал и на английских установках:
Get-NetFirewallRule -Group '@FirewallAPI.dll,-28752' | Select-Object Name, DisplayName, Enabled, Profile
Set-NetFirewallRule -Group '@FirewallAPI.dll,-28752' -RemoteAddress @('203.0.113.10','198.51.100.0/24')
Ожидайте при этом больше попаданий, чем правил, которые вы предполагаете. -Group затрагивает все правила группы, на тестовой системе это оказалось шесть штук, включая теневые правила и дополнительные копии с именами-GUID в профиле Public. Так и задумано, и это правильно, иначе одна из копий осталась бы открытой. Предшествующий вывод списка через Get-NetFirewallRule заранее показывает, что будет затронуто.
Проверка, какие адреса действительно сохранились:
Get-NetFirewallRule -Group '@FirewallAPI.dll,-28752' | Get-NetFirewallAddressFilter
Если вы сменили порт, встроенные правила больше не срабатывают, потому что они жёстко привязаны к 3389. Тогда нужно собственное правило:
New-NetFirewallRule -DisplayName 'RDP ограничен' -Direction Inbound -Protocol TCP -LocalPort 34567 -RemoteAddress '203.0.113.10' -Action Allow -Profile Any
Страховка от собственного правила файрвола
Кто ошибётся в своём IP-адресе или впишет динамический адрес, тот надёжно отрежет себе доступ. Поэтому заранее создайте задание, которое через десять минут само снимет ограничение:
Set-Content -Path C:\rdp-rettung.ps1 -Value "Set-NetFirewallRule -Group '@FirewallAPI.dll,-28752' -RemoteAddress Any"
Register-ScheduledTask -TaskName 'RDP-восстановление' -Action (New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-ExecutionPolicy Bypass -File C:\rdp-rettung.ps1') -Trigger (New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(10)) -User 'SYSTEM' -RunLevel Highest
Если новое подключение работает, задание удаляется снова:
Unregister-ScheduledTask -TaskName 'RDP-восстановление' -Confirm:$false
Если фиксированного IP-адреса у вас нет, чистое решение — не выставлять RDP в интернет вообще, а подключаться через VPN. Как это настроить, описано в нашем руководстве по серверу WireGuard VPN. Тогда сервер слушает только на VPN-адресе, а порт 3389 полностью исчезает из интернета.
Смена порта и что она реально даёт
Другой порт — не мера безопасности, а мера защиты от шума. Основная масса ботов сканирует исключительно 3389 и после смены вас уже не находит, что, как правило, резко снижает ваше число событий 4625 и снова делает журналы читаемыми. Кто ищет целенаправленно, тот найдёт службу всё равно: поисковые системы по открытым службам распознают RDP по отпечатку протокола независимо от порта, а полное сканирование 65535 портов занимает секунды.
Смена порта, таким образом, осмысленна как дополнение, но никогда как замена NLA, блокировки учётных записей и ограничения по адресам. Практическую реализацию, включая ту часть, которую почти все руководства делают неправильно, а именно без перезагрузки, мы описали отдельно: Смена порта RDP без перезагрузки. В любом случае не забудьте создать правило файрвола для нового порта, прежде чем переключать службу.
Разбор журналов входа
Сначала журналирование вообще должно быть включено. Названия подкатегорий в auditpol локализованы, английское название на русской системе завершается ошибкой «Произошла ошибка 0x00000057: Неверный параметр.» GUID же работает в любой языковой версии:
auditpol /set '/subcategory:{0CCE9215-69AE-11D9-BED3-505054503030}' /success:enable /failure:enable
auditpol /get '/subcategory:{0CCE9215-69AE-11D9-BED3-505054503030}'
Одинарные кавычки вокруг всего параметра — не косметика, а обязательное условие. Иначе PowerShell трактует фигурные скобки как блок скрипта и удаляет их, из одного аргумента получаются три, и auditpol прерывается с Произошла ошибка 0x00000057: Неверный параметр. и кодом выхода 87, а следом выводит текст справки. В cmd.exe запись без кавычек работает, в PowerShell нет. С кавычками оба вызова проходят с кодом выхода 0, а контрольный запрос отвечает Вход в систему Успех и отказ.
Под обстрелом журнал безопасности переполняется за считаные часы и перезаписывает ровно те записи, которые вам нужны. Дайте ему больше места:
wevtutil sl Security /ms:1073741824
Затем самые частые адреса источников за последнюю неделю, отсортированные по количеству. Вариант через структуру XML надёжнее, потому что он не зависит от порядка полей:
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddDays(-7)} -ErrorAction SilentlyContinue | ForEach-Object { ([xml]$_.ToXml()).Event.EventData.Data | Where-Object { $_.Name -eq 'IpAddress' } | Select-Object -ExpandProperty '#text' } | Group-Object | Sort-Object Count -Descending | Select-Object -First 15 Count, Name
-ErrorAction SilentlyContinue нужен в каждом таком вызове. Get-WinEvent обрывается красной ошибкой, как только за указанный период нет ни одного подходящего события: «Не найдено событий, соответствующих указанным условиям выбора.» На только что защищённом сервере это как раз нормальный случай, то есть команда падает именно при том успехе, к которому ведёт это руководство.
Решающий вопрос, однако, не в том, кто пытался, а в том, удалось ли кому-то. Успешные входы по удалённому рабочему столу несут идентификатор события 4624 с типом входа 10 (RemoteInteractive):
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624; StartTime=(Get-Date).AddDays(-7)} -ErrorAction SilentlyContinue | Where-Object { $_.Properties[8].Value -eq 10 } | Select-Object TimeCreated, @{n='Пользователь';e={$_.Properties[5].Value}}, @{n='Источник';e={$_.Properties[18].Value}} | Format-Table -AutoSize
Если там стоит имя пользователя или адрес источника, который вы не можете сопоставить ни с чем своим, значит кто-то нашёл действующий пароль. Тогда доводка политик уже не поможет, тогда сервер нужно устанавливать заново.
Дополнительно стоит заглянуть в собственный журнал RDP. Событие 1149 называет пользователя, домен и адрес источника каждого авторизованного подключения одной строкой:
Get-WinEvent -LogName 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational' -FilterXPath '*[System[EventID=1149]]' -MaxEvents 25 | Format-List TimeCreated, Message
Переименовать администратора, а лучше отключить
Встроенная учётная запись администратора — единственное имя пользователя, которое наверняка знает каждый атакующий. Переименование ничего не стоит:
Rename-LocalUser -Name 'Administrator' -NewName 'kh-svc'
Но отдавайте себе отчёт в границах этой меры: SID учётной записи по-прежнему заканчивается на -500, и любой аутентифицированный доступ может через него получить новое имя. Против массовых ботов переименование работает, против целенаправленного атакующего, который уже поставил ногу в дверь, нет. Вот как найти учётную запись, несмотря на новое имя:
Get-LocalUser | Where-Object { $_.SID.Value -like '*-500' } | Select-Object Name, Enabled
Заметно действеннее полностью отключить встроенную учётную запись после того, как ваша собственная учётная запись администратора из раздела «Обеспечьте себе путь назад» доказанно работает. Проверьте вход под новой учётной записью во втором сеансе, и только тогда:
Disable-LocalUser -Name 'kh-svc'
Дополнительно ограничьте доступ по RDP теми людьми, которым он нужен. По умолчанию по RDP заходит любой член локальной группы администраторов, в том числе служебные учётные записи, которым этого делать не следует. Кто имеет право сейчас, покажет:
Get-LocalGroupMember -SID 'S-1-5-32-555'
По чему видно, что всё действительно сработало
Отмечать галочки, а не надеяться. Эти пять проверок скажут вам, дошла ли перенастройка:
- NLA:
Get-ItemPropertyдляRDP-TcpвыдаётUserAuthentication : 1иSecurityLayer : 2. Новое подключение теперь спрашивает учётные данные до установления соединения, а не уже на экране входа внутри окна. - Блокировка:
net accountsпоказывает порог блокировки, отличный от «Никогда». Контрольная проверка одноразовой учётной записью: после одиннадцатого неверного пароля клиент должен выдать сообщение о блокировке, а не сообщение о неверных учётных данных. - Файрвол:
Get-NetFirewallAddressFilterпоказывает ваши адреса вместоAny. Проверка подключения с чужого адреса должна уходить в ошибку по тайм-ауту, а не в приглашение ко входу. Проверьте это снаружи командойTest-NetConnection -ComputerName ваш-сервер -Port 3389, результат должен бытьTcpTestSucceeded : False. - Журналы:
auditpol /getсообщает для подкатегории успех и отказ. - Число: посчитайте события 4625 ещё раз через 24 часа после перенастройки. Оно должно быть заметно ниже. Если оно остаётся высоким, одно из ваших правил не действует, чаще всего потому, что существует второе, более открытое правило файрвола для порта 3389, созданное образом провайдера или установкой какой-то программы. Эта строка его найдёт:
Get-NetFirewallPortFilter | Where-Object { $_.LocalPort -eq 3389 } | Get-NetFirewallRule | Select-Object DisplayName, Enabled, Profile, Action. Порядок в конвейере выбран намеренно. Если его развернуть и сначала прогнать все правила файрвола черезGet-NetFirewallPortFilter, вызов по замерам занимает около 12 секунд вместо одной, а главное, в выводе не будет имени правила: вы увидите четыре попадания, не узнав, что это за правила.
Если вы всё равно как раз настраиваете сервер с нуля, пройдите эти пункты сразу в начале, а не дооснащайте их потом. Для систем Linux действует та же схема, только с SSH вместо RDP, она описана в статье Защита SSH и вход по ключу, а порядок первых шагов вы найдёте в нашем чек-листе для новых root-серверов.
Чего защита RDP не покрывает
Меры выше защищают от попыток входа. Они не защищают от объёмных атак, цель которых сделать сервер недоступным через сетевое подключение. Против них помогает только фильтрация в сети перед сервером. Как работают такие атаки, описано в статье Что такое DDoS-атака, а какие меры остаются осмысленными на стороне сервера, в статье Защита сервера от DDoS-атак. Все серверы KernelHost стоят в дата-центре maincubes во Франкфурте-на-Майне за фильтрацией, которая перехватывает атакующий трафик ещё до сервера.
И они не заменяют резервное копирование. Сервер, на котором кто-то успешно вошёл, больше не заслуживает доверия, как бы быстро вы ни сменили пароль после этого. Единственный надёжный путь назад — резервная копия, сделанная до инцидента.
Частые вопросы
Достаточно ли сменить порт RDP с 3389 на другой?
Я сам себе закрыл доступ после установки правила файрвола. Что теперь?
Почему пользователь не может войти с правильным паролем с тех пор, как включён NLA?
Распространяется ли блокировка учётных записей на встроенную учётную запись администратора?
Может ли атакующий через блокировку учётных записей закрыть мне доступ навсегда?
По чему понять, что атака удалась?
2026 KernelHost GmbH. Все права защищены. Эта инструкция охраняется авторским правом. Публикация на других сайтах, в том числе частично или в изменённом виде, без нашего письменного согласия не разрешается. Цитирование с указанием источника и активной ссылкой мы приветствуем.

