Коротко о проблеме

Клиент RDP выдаёт «Произошла внутренняя ошибка». Сервер удалённый, физического доступа нет, связь только через VPN. Порт 3389 при этом отвечает.

Ситуация неприятна тем, что «внутренняя ошибка» — это заглушка, за которой скрывается десяток разных причин, а привычный канал администрирования как раз и лежит. Ниже — методика, которая доводит до диагноза по шагам, а не перебором наугад.

Шаг 0. Отсечь клиентскую сторону

Начинать нужно с себя — это бесплатно и не требует доступа к серверу. RDP 8 и новее пытается установить дополнительный UDP-транспорт, и поверх VPN он часто не работает, давая ровно эту ошибку.

reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services\Client" /v fClientDisableUDP /t REG_DWORD /d 1 /f

Заодно чистим кэш клиента — битые записи в нём дают тот же симптом:

del /f /q "%LOCALAPPDATA%\Microsoft\Terminal Server Client\Cache\*"

Если после этого подключение прошло — дальше можно не читать.

Шаг 1. Понять, где именно рвётся

Test-NetConnection 10.0.0.10 -Port 3389 -InformationLevel Detailed

Развилка здесь принципиальная:

  • TcpTestSucceeded : False — проблема не в RDP вообще. Это маршрутизация, файрвол или туннель. Копать надо там, всё остальное бессмысленно.
  • TcpTestSucceeded : True — служба жива и слушает, соединение принимается. Значит рвётся после установки TCP-сессии: на рукопожатии TLS или на аутентификации.

Второй вариант и есть наш случай. Он сразу отсекает половину советов из интернета: если порт слушается, то «служба не запущена» и «RDP отключён в системе» — не про нас.

Шаг 2. Найти запасной канал управления

Чтобы чинить, нужно попасть на сервер хоть как-то. Проверяем, что доступно через туннель:

Test-NetConnection 10.0.0.10 -Port 5985   # WinRM
Test-NetConnection 10.0.0.10 -Port 445    # SMB / RPC

Если WinRM отвечает, но Enter-PSSession ругается на TrustedHosts — это не «порт закрыт», а недоверие клиента к хосту при NTLM-аутентификации. Лечится на своей машине:

Set-Item WSMan:\localhost\Client\TrustedHosts -Value 10.0.0.10 -Force

Учётку при подключении указывать явно, с префиксом машины: 10.0.0.10\Administrator. Иначе NTLM снова споткнётся.

Отдельно стоит помнить: правило файрвола для WinRM в публичном профиле по умолчанию разрешает доступ только из той же локальной подсети. Клиент за VPN почти всегда в другой подсети — и режется этим правилом, даже когда служба работает.

Шаг 3. Ловушка, на которой всё встаёт: UAC Remote Restrictions

Это ключевой момент статьи, и именно на нём теряется больше всего времени.

Картина выглядит так: SMB (445) открыт, пароль локального администратора верный, но всё подряд отбивается отказом в доступе.

psexec \\10.0.0.10 -u 10.0.0.10\admin -p *** powershell -c "Restart-Service TermService -Force"
# Couldn't access 10.0.0.10: не найден сетевой путь
# Make sure that the default admin$ share is enabled

sc.exe \\10.0.0.10 stop TermService
# [SC] OpenService: ошибка 5: Отказано в доступе

Get-WmiObject -ComputerName 10.0.0.10 -Credential $cred -Class Win32_Service
# Отказано в доступе. (0x80070005 E_ACCESSDENIED)

Три разных инструмента, три разных ошибки — и одна общая причина. Начиная с Windows Vista действует механизм UAC Remote Restrictions: при удалённом входе локальная учётная запись администратора получает урезанный токен. Аутентификация проходит, а административных прав у сессии нет. Поэтому отваливаются и административные шары, и SCM, и WMI одновременно.

Важно: на доменные учётные записи это не распространяется. Если сервер в домене и есть доменный админ — проблемы нет. Ловушка бьёт именно по standalone-серверам с локальными учётками.

Снимается ограничение одним ключом реестра:

HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
LocalAccountTokenFilterPolicy = 1 (DWORD)

И тут возникает то, ради чего стоит дочитать до конца: чтобы записать этот ключ удалённо, нужны ровно те права, которые он и разблокирует. Замкнутый круг.

Единственный шанс разорвать его снаружи — служба удалённого реестра. Она работает под системным токеном, и UAC-фильтрация её иногда не задевает:

reg query "\\10.0.0.10\HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v LocalAccountTokenFilterPolicy

Если куст читается — пишем ключ, и все остальные каналы оживают без перезагрузки:

reg add "\\10.0.0.10\HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f

Если и удалённый реестр закрыт — программных рычагов не остаётся. Дальше только уровень ниже операционной системы: IPMI, iLO, iDRAC, KVM-консоль хостера или гипервизор. А если сервер стоит на своём железе без контроллера управления — увы, нужны чьи-то руки на площадке.

Маленькая, но обидная деталь для тех, кто работает в PowerShell: sc там является алиасом для Set-Content. Команда до сервера просто не долетает, выдавая невнятную ошибку про позиционный параметр. Писать нужно sc.exe.

Шаг 4. Диагностика с консоли

Когда доступ получен, первым делом снимаем общую картину:

Get-Service TermService,UmRdpService | Select Name,Status,StartType
Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server" -Name fDenyTSConnections
netstat -ano | findstr ":3389"
qwinsta

В нашем случае всё выглядело здоровым: службы Running, fDenyTSConnections = 0, порт слушается, слушатель RDP-Tcp в состоянии приёма. То есть типовые советы уже исчерпаны, а проблема осталась.

Дальше — журнал, и читать его нужно осмысленно:

Get-WinEvent -LogName "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational" -MaxEvents 20 |
  Select TimeCreated,Id,Message | Format-List

Ключевые события:

  • 258 — слушатель начал прослушивание. Служба поднялась нормально.
  • 261 — слушатель принял соединение. Ваши попытки долетают до сервера.
  • 1149 — аутентификация пользователя выполнена успешно.
  • 1057 / 1058 — не удалось создать или прочитать самоподписанный сертификат.

Самое информативное здесь — то, чего нет. Если события 261 идут, а 1149 не появляется ни разу, соединение рвётся между приёмом и аутентификацией. Это рукопожатие TLS, и круг подозреваемых резко сужается. Если при этом нет 1057 и 1058 — генерация сертификата исправна, и весь блок советов про повреждённый каталог MachineKeys можно смело пропустить.

Шаг 5. Изолировать переменную

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

Уровень безопасности слушателя и NLA — две независимые вещи, и проверять их надо по очереди:

$ts = Get-WmiObject -Namespace root\cimv2\TerminalServices -Class Win32_TSGeneralSetting -Filter "TerminalName='RDP-Tcp'"
$ts.SetSecurityLayer(0)              # 0 = RDP, 1 = Negotiate, 2 = SSL
$ts.SetUserAuthenticationRequired(0) # NLA
Restart-Service TermService -Force

Если подключение прошло — возвращаем только уровень безопасности, оставив NLA выключенным:

$ts.SetSecurityLayer(1)
Restart-Service TermService -Force

Заходит — значит виноват NLA и CredSSP. Не заходит — виноват TLS. Дальше вы чините конкретную вещь, а не гадаете.

Понятно, что режим без TLS и без NLA — это дыра, и оставлять сервер в нём нельзя. Это диагностический шаг на несколько минут, не решение.

Шаг 6. Разбираемся с TLS

Если виноват TLS, проверяем конфигурацию Schannel. Чаще всего её ломает хардненинг — вручную, групповой политикой или утилитами вроде IIS Crypto:

Get-ChildItem "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols" -Recurse |
  ForEach-Object { "$($_.Name -replace '.*Protocols\\','') Enabled=$((Get-ItemProperty $_.PSPath -EA SilentlyContinue).Enabled)" }

(Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy" -EA SilentlyContinue).Enabled

(Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Cryptography\Configuration\SSL\00010002" -EA SilentlyContinue).Functions

Последняя команда — недооценённая. Групповая политика умеет навязывать серверу собственный список наборов шифров, и если в нём не осталось ни одного, пригодного для RDP, вы получите ровно «внутреннюю ошибку» при полностью корректных настройках протоколов.

Если Schannel чист, а TLS не работает — смотрим сертификат и, что важнее, доступ к его закрытому ключу:

Get-ChildItem "Cert:\LocalMachine\Remote Desktop" | Select Thumbprint,Subject,NotAfter,NotBefore

Сертификат может быть валиден и виден, но если у службы нет прав на чтение закрытого ключа, рукопожатие рвётся. При этом нативное RDP-шифрование продолжает работать, потому что ключ ему не нужен — отсюда и обманчивая картина «без TLS всё хорошо».

$c = Get-ChildItem "Cert:\LocalMachine\Remote Desktop"
$kf = $c.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
icacls "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\$kf"

В списке должен присутствовать NT AUTHORITY\NETWORK SERVICE:(R). Если его нет — выдаём:

icacls "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\$kf" /grant "NT AUTHORITY\NETWORK SERVICE:(R)"
Restart-Service TermService -Force

Радикальный вариант — удалить сертификат и дать службе создать новый, права на ключ при генерации выставляются корректно:

Get-ChildItem "Cert:\LocalMachine\Remote Desktop" | Remove-Item -Force
Restart-Service TermService -Force

Здесь стоит быть готовым к тому, что Restart-Service зависнет в ожидании остановки: TermService часто не отпускает активные сеансы. Ожидание в консоли безобидно, Ctrl+C прерывает только цикл ожидания, а не саму операцию. Проверять результат нужно через Get-Service, а не по факту возврата приглашения — иначе легко решить, что сертификат не сгенерировался, хотя служба просто ещё не поднялась.

Шаг 7. Если роль RDSH

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

Get-WindowsFeature RDS-RD-Server | Select Name,InstallState

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

Чем закончилось в этом кейсе

Конечной причиной оказалась нерабочая пара «сертификат — закрытый ключ» у слушателя RDP-Tcp.

Коварство ситуации в том, что по всем внешним признакам сертификат был исправен: он присутствовал в хранилище, срок действия не истёк, привязка в реестре отсутствовала (то есть служба использовала свой самоподписанный), событий 1057 и 1058 в журнале не было — генерация не падала. Конфигурация Schannel тоже оказалась абсолютно чистой: TLS 1.2 для сервера включён, переопределений шифров, хешей и алгоритмов обмена ключами нет, FIPS выключен, список наборов шифров групповой политикой не навязан.

При этом рукопожатие TLS не проходило, а подключение с нативным RDP-шифрованием работало без нареканий. Объяснение простое: нативному шифрованию закрытый ключ не нужен, а Schannel без него сделать ничего не может. Отсюда и обманчивая картина «отключил TLS — всё заработало», которая уводит в сторону хардненинга и настроек протоколов, где искать нечего.

Лечение свелось к пересозданию сертификата — при генерации служба выставляет права на закрытый ключ корректно:

Get-ChildItem "Cert:\LocalMachine\Remote Desktop" | Remove-Item -Force
Restart-Service TermService -Force

После появления нового сертификата уровень безопасности возвращается на согласование, и подключение по TLS проходит штатно:

$ts = Get-WmiObject -Namespace root\cimv2\TerminalServices -Class Win32_TSGeneralSetting -Filter "TerminalName='RDP-Tcp'"
$ts.SetSecurityLayer(1)
$ts.SetUserAuthenticationRequired(1)
Restart-Service TermService -Force

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

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

Рецидив: когда автогенерация ломается насовсем

При повторном отказе картина сначала была знакомой: события 261 в журнале идут, служба Running, порт 3389 слушается — но хранилище Cert:\LocalMachine\Remote Desktop пустое. То есть служба снова осталась без сертификата, только теперь простой рестарт его не восстанавливал: после Restart-Service хранилище оставалось пустым. Автогенерация перестала отрабатывать в принципе.

Причину показал icacls по каталогу ключей. На самом каталоге права были в норме, но при рекурсивной проверке один конкретный файл контейнера отбил доступ — причём даже администратору:

icacls "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys" /T
# ...
# MachineKeys\01a5c6e4...aa632058-...: Отказано в доступе
# Успешно обработано 1 файлов; не удалось обработать 1 файлов

Когда takeown забрал владение этим файлом, вскрылась суть: владельцем контейнера оказалась учётная запись пользователя (в нашем случае — ДОМЕН\user), а не SYSTEM. Для MachineKeys это аномалия: там ключи должны принадлежать системе, иначе служба под NETWORK SERVICE до них не дотягивается. Один такой битый контейнер — и генерация нового самоподписанного сертификата молча спотыкается о него, ничего не создавая. Параллельно sfc /scannow нашёл повреждения системных файлов, которые не смог восстановить — второй независимый признак того, что повреждение лежит ниже уровня RDP.

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

Сначала убираем повреждённый контейнер (без takeown его не удалить — доступа нет даже у администратора):

$bad = "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\<имя_битого_файла>"
takeown /f $bad
icacls $bad /grant "BUILTIN\Администраторы:(F)"
Remove-Item $bad -Force

Создаём собственный сертификат с запасом по сроку и явно указанным криптопровайдером:

$c = New-SelfSignedCertificate -DnsName $env:COMPUTERNAME `
  -CertStoreLocation "Cert:\LocalMachine\My" -KeySpec KeyExchange `
  -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" `
  -NotAfter (Get-Date).AddYears(5)
$c.Thumbprint

Даём службе доступ к закрытому ключу — это тот самый шаг, отсутствие которого рвёт рукопожатие:

$kf = (Get-Item "Cert:\LocalMachine\My\$($c.Thumbprint)").PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
icacls "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\$kf" /grant "NT AUTHORITY\NETWORK SERVICE:(R)"

Привязываем отпечаток к слушателю RDP-Tcp. Отпечаток в реестре хранится как двоичный массив байтов, поэтому строку нужно превратить в byte[]:

$bytes = [byte[]] -split ($c.Thumbprint -replace '..', '0x$& ')
Set-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" `
  -Name SSLCertificateSHA1Hash -Value $bytes -Type Binary
Restart-Service TermService -Force

И обязательно проверяем, что привязка встала — именно на этом шаге легко промахнуться, пропустив Set-ItemProperty и решив, что всё готово:

-join ((Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" `
  -Name SSLCertificateSHA1Hash).SSLCertificateSHA1Hash | ForEach-Object { '{0:X2}' -f $_ })

Команда должна вернуть отпечаток вашего сертификата. Если вместо этого она сообщает, что свойство SSLCertificateSHA1Hash не существует — значит запись в реестр не выполнилась, и подключение по TLS работать не будет. У ручной привязки есть приятный побочный эффект: сертификат живёт пять лет и не подвержен ежегодной ротации самоподписанного, так что заодно снимается ещё один потенциальный повод для будущих сюрпризов.

Но и это — устранение симптома, пусть и устойчивое. Битый контейнер ключа с чужим владельцем и повреждения, которые не осилил sfc, редко берутся из ниоткуда. Обычно за ними стоит некорректное завершение работы — жёсткая перезагрузка, пропадание питания, сбой диска в момент записи. Поэтому корень надо добить отдельно.

Сначала дожимаем повреждения системных файлов через DISM — он берёт исправные компоненты из хранилища обслуживания, там где sfc опустил руки:

DISM /Online /Cleanup-Image /RestoreHealth

После него имеет смысл повторить sfc /scannow — теперь он должен пройти чисто. И проверяем диск, потому что повреждения на файловой системе вместе с битым контейнером слишком часто указывают именно на него:

Get-PhysicalDisk | Select FriendlyName,HealthStatus,OperationalStatus
chkdsk C: /scan

Ключ /scan работает в онлайне, не требует перезагрузки и не блокирует том — его безопасно запускать на боевом сервере. Если в системном журнале при этом попадаются события от disk, Ntfs или storahci с идентификаторами 7, 11, 51 или 153 — корень лежит в железе, и RDP тут лишь первый симптом, который отвалится снова, пока не разберётесь с накопителем.

Вывод из этого поворота простой: если «удалить сертификат и перезапустить службу» помогает лишь до следующего раза, не повторяйте фокус по кругу. Ручная привязка даёт устойчивый результат, а повторяющееся повреждение крипто-контейнеров — это сигнал проверять систему обслуживания образа и диск, а не сам RDP.

Реальная проблема в этом кейсе была не в RDP. Проблема была в том, что при отказе RDP не осталось ни одного запасного канала управления. Несколько часов ушло не на починку, а на попытки хоть как-то попасть на сервер.

Пока доступ есть, стоит потратить пять минут и подготовиться к тому дню, когда его не будет:

# снять UAC-ограничение для локальных администраторов
New-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" `
  -Name LocalAccountTokenFilterPolicy -Value 1 -PropertyType DWord -Force

# включить WinRM и разрешить доступ не только из локальной подсети
Enable-PSRemoting -Force
Set-NetFirewallRule -Name "WINRM-HTTP-In-TCP-PUBLIC" -RemoteAddress Any

# удалённый реестр
Set-Service RemoteRegistry -StartupType Automatic
Start-Service RemoteRegistry

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

И отдельно: если сервер стоит на своём железе, IPMI / iLO / iDRAC надо настраивать и проверять заранее. Контроллер управления, о котором вспомнили в момент аварии, обычно оказывается либо не подключённым к сети, либо с неизвестным паролем.

Итог

Порядок действий, который экономит время:

  1. Отсечь клиентскую сторону — UDP и кэш.
  2. Проверить порт: жив ли слушатель вообще.
  3. Найти запасной канал управления, помня про UAC Remote Restrictions на standalone-серверах.
  4. С консоли снять картину служб и прочитать журнал, обращая внимание на отсутствующие события, а не только на присутствующие.
  5. Изолировать одну переменную: TLS и NLA проверять по очереди.
  6. Если виноват TLS — сначала Schannel, затем сертификат и доступность его закрытого ключа.
  7. Если пересоздание сертификата помогает лишь до следующего раза — создать сертификат вручную и явно привязать к RDP-Tcp, а причину искать в MachineKeys, DISM и диске.
  8. Чинить конкретную причину, а не оставлять сервер с отключённой защитой.