feat: локальные скрипты починки RDP/TLS для Windows (fix-DAK-PC)
Fix-RdpTls.ps1 с фазами диагностики, cert, ACL, привязкой; самотесты; docs repair install и возврат TLS. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
*.log
|
||||||
|
report-*.txt
|
||||||
|
repair-*.log
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# fix-DAK-PC — локальная починка RDP/TLS на Windows 10/11
|
||||||
|
|
||||||
|
Набор скриптов для **локального** (с консоли проблемного ПК) восстановления RDP, когда клиент показывает «внутренняя ошибка», в журнале **1057** / **Schannel 36870**, TermService сбрасывает `SSLCertificateSHA1Hash`.
|
||||||
|
|
||||||
|
Сценарий проверен на **DAK-PC** (Win10 Pro 19045): корень — сломанная привязка TLS-сертификата RDP; обход `SecurityLayer=0`; полное лечение — repair install + возврат TLS.
|
||||||
|
|
||||||
|
## Быстрый старт (за компом DAK-PC)
|
||||||
|
|
||||||
|
1. Скопируйте репозиторий или папку `scripts` на ПК (флешка, `git clone`, общая шара).
|
||||||
|
2. **PowerShell от имени администратора**.
|
||||||
|
3. Разрешите выполнение (один раз в этой сессии):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
|
||||||
|
cd C:\path\to\fix-DAK-PC\scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Самотесты** (обязательно):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Invoke-RdpRepairSelfTest.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Полный интерактивный ремонт**:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Если TLS не удаётся починить — **экстренный доступ** (без TLS):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1 -EmergencyRdpOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
7. После **repair install** Windows и успешной привязки cert — **возврат TLS**:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1 -RestoreTls
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|------|------------|
|
||||||
|
| `scripts/Fix-RdpTls.ps1` | Главный сценарий: диагностика → очистка → cert → привязка → проверка |
|
||||||
|
| `scripts/Invoke-RdpRepairSelfTest.ps1` | Самотесты функций и синтаксиса |
|
||||||
|
| `scripts/lib/RdpRepair.Common.ps1` | Общие функции |
|
||||||
|
| `docs/REPAIR-INSTALL.md` | Repair install Windows (на месте) |
|
||||||
|
| `docs/RETURN-TO-TLS.md` | Возврат SecurityLayer=2 после починки cert |
|
||||||
|
| `docs/TROUBLESHOOTING.md` | События, типичные ошибки, GPO |
|
||||||
|
| `docs/LOCAL-RUNBOOK.md` | Пошаговый сценарий за консолью DAK-PC |
|
||||||
|
|
||||||
|
## Фазы `Fix-RdpTls.ps1`
|
||||||
|
|
||||||
|
| Фаза | Действие |
|
||||||
|
|------|----------|
|
||||||
|
| 0 | Самопроверка: админ, ОС, службы, модули |
|
||||||
|
| 1 | Диагностика → отчёт `%ProgramData%\RdpRepair\report-*.txt` |
|
||||||
|
| 2 | Очистка просроченных/битых cert в `Remote Desktop` и `My` |
|
||||||
|
| 3 | Нативный сброс (выкл/вкл RDP, очистка store) + опциональный reboot |
|
||||||
|
| 4 | Создание cert (цепочка провайдеров: Enhanced RSA → default) |
|
||||||
|
| 5 | ACL на приватный ключ (`NETWORK SERVICE`, `SYSTEM`) |
|
||||||
|
| 6 | Привязка (`reg` + WMI), проверка **Hash BEFORE/AFTER** |
|
||||||
|
| 7 | `-EmergencyRdpOnly`: SecurityLayer=0 |
|
||||||
|
| 8 | `-RestoreTls`: SecurityLayer=2 при успешной привязке |
|
||||||
|
|
||||||
|
## Критерий успеха TLS
|
||||||
|
|
||||||
|
После перезапуска `TermService`:
|
||||||
|
|
||||||
|
- `SSLCertificateSHA1Hash` в реестре **совпадает** с thumbprint cert в store.
|
||||||
|
- События **1057** / **36870** не появляются при тестовом подключении RDP.
|
||||||
|
- `SecurityLayer` = **2** (как на рабочих ПК в домене).
|
||||||
|
|
||||||
|
Если **Hash AFTER** пустой при любом cert — см. [docs/REPAIR-INSTALL.md](docs/REPAIR-INSTALL.md).
|
||||||
|
|
||||||
|
## Клонирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.kalinamall.ru/PapaTramp/fix-DAK-PC.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Внутреннее использование (PapaTramp / kalinamall).
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Пошаговый runbook — за компом DAK-PC (локально)
|
||||||
|
|
||||||
|
Выполняйте **на проблемном ПК** в PowerShell **от имени администратора**.
|
||||||
|
|
||||||
|
## 0. Подготовка
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Скопируйте репозиторий, например:
|
||||||
|
# git clone https://git.kalinamall.ru/PapaTramp/fix-DAK-PC.git C:\Tools\fix-DAK-PC
|
||||||
|
|
||||||
|
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
|
||||||
|
cd C:\Tools\fix-DAK-PC\scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Самотесты (обязательно)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Invoke-RdpRepairSelfTest.ps1 -IncludeAdminTests
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидание: `FAIL=0`. При ошибках — не продолжайте.
|
||||||
|
|
||||||
|
## 2. Диагностика
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1 -DiagnoseOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
Отчёт: `%ProgramData%\RdpRepair\report-*.txt`
|
||||||
|
Лог: `%ProgramData%\RdpRepair\repair-*.log`
|
||||||
|
|
||||||
|
Сохраните отчёт на флешку / в тикет.
|
||||||
|
|
||||||
|
## 3. Полный автоматический ремонт TLS
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт спросит подтверждение на каждой фазе:
|
||||||
|
|
||||||
|
1. Очистка cert + нативный сброс
|
||||||
|
2. Опционально **reboot** (если Windows должен сам создать cert)
|
||||||
|
3. Создание локального cert + ACL + привязка
|
||||||
|
4. Проверка **Hash AFTER**
|
||||||
|
5. Предложение `SecurityLayer=2`
|
||||||
|
|
||||||
|
### Если после reboot (шаг 3.2)
|
||||||
|
|
||||||
|
Снова с консоли:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Tools\fix-DAK-PC\scripts
|
||||||
|
.\Invoke-RdpRepairSelfTest.ps1 -IncludeAdminTests
|
||||||
|
.\Fix-RdpTls.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Если Hash AFTER пустой (TermService отклоняет cert)
|
||||||
|
|
||||||
|
### 4a. Срочный RDP (без TLS)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1 -EmergencyRdpOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверьте RDP с другого ПК. На клиенте:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Remove-Item "HKCU:\Software\Microsoft\Terminal Server Client\Servers\DAK-PC" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4b. Repair install Windows
|
||||||
|
|
||||||
|
См. [REPAIR-INSTALL.md](REPAIR-INSTALL.md) — `setup.exe` → обновление с сохранением файлов.
|
||||||
|
|
||||||
|
После repair:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Возврат нормального TLS
|
||||||
|
|
||||||
|
**Только** когда `Hash AFTER` совпадает с thumbprint и RDP с TLS работает:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1 -RestoreTls
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробно: [RETURN-TO-TLS.md](RETURN-TO-TLS.md).
|
||||||
|
|
||||||
|
## 6. DISM / SFC (до или после repair)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
DISM /Online /Cleanup-Image /RestoreHealth
|
||||||
|
sfc /scannow
|
||||||
|
```
|
||||||
|
|
||||||
|
Перезагрузка → снова `.\Fix-RdpTls.ps1`.
|
||||||
|
|
||||||
|
## Критерии «готово»
|
||||||
|
|
||||||
|
- [ ] `Cert store` — cert с `NotAfter` в будущем
|
||||||
|
- [ ] `SSLCertificateSHA1Hash` после старта TermService **не пустой**
|
||||||
|
- [ ] `SecurityLayer` = **2**
|
||||||
|
- [ ] События 1057 / 36870 не появляются при подключении RDP
|
||||||
|
- [ ] RDP с клиента без `-EmergencyRdpOnly`
|
||||||
|
|
||||||
|
## Текущее состояние DAK-PC (на момент создания репо)
|
||||||
|
|
||||||
|
- Работал обход: `SecurityLayer=0`
|
||||||
|
- Перед `RestoreTls` обязательно починить привязку cert или сделать repair install
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Repair install Windows — когда скрипты TLS не помогают
|
||||||
|
|
||||||
|
## Когда нужен repair install
|
||||||
|
|
||||||
|
Выполняйте, если **все** условия выполнены:
|
||||||
|
|
||||||
|
1. `Fix-RdpTls.ps1` прошёл фазы 2–6, но **Hash AFTER** пустой после старта `TermService`.
|
||||||
|
2. В журнале **System** событие **1057**: «не удалось **создать** самозаверяющий сертификат» / «неправильный алгоритм».
|
||||||
|
3. `DISM /RestoreHealth` и `sfc /scannow` завершились без нарушений, но проблема осталась.
|
||||||
|
4. Импорт cert с другого ПК (PFX) тоже сбрасывается службой.
|
||||||
|
5. **Emergency** `SecurityLayer=0` даёт RDP, **SecurityLayer=2** — нет.
|
||||||
|
|
||||||
|
Это указывает на повреждение компонентов **TermService / Schannel / cert enrollment**, а не на один просроченный файл.
|
||||||
|
|
||||||
|
## Подготовка
|
||||||
|
|
||||||
|
1. Сохраните отчёты: `%ProgramData%\RdpRepair\report-*.txt`.
|
||||||
|
2. Убедитесь в свободном месте на диске C: (**≥ 10 GB**).
|
||||||
|
3. Имейте установочный образ **той же редакции** Win10 (19045) или Media Creation Tool.
|
||||||
|
4. Доступ к ПК: физически или через RDP с `SecurityLayer=0` / WinRM.
|
||||||
|
|
||||||
|
## Порядок (на месте, сохранение файлов)
|
||||||
|
|
||||||
|
### 1. Скачать образ
|
||||||
|
|
||||||
|
- [Media Creation Tool](https://www.microsoft.com/software-download/windows10) на флешку **или**
|
||||||
|
- ISO той же разрядности (x64), что и установленная система.
|
||||||
|
|
||||||
|
### 2. Запуск repair
|
||||||
|
|
||||||
|
1. Смонтировать ISO / вставить флешку.
|
||||||
|
2. Запустить `setup.exe` **от администратора**.
|
||||||
|
3. Выбрать **«Обновление»** (Upgrade) — **не** «Выборочная установка».
|
||||||
|
4. Сохранить личные файлы и приложения.
|
||||||
|
5. Дождаться завершения (перезагрузки).
|
||||||
|
|
||||||
|
### 3. После repair
|
||||||
|
|
||||||
|
В PowerShell **от администратора**:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\path\to\fix-DAK-PC\scripts
|
||||||
|
.\Invoke-RdpRepairSelfTest.ps1
|
||||||
|
.\Fix-RdpTls.ps1 -SkipEmergency
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверьте:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1 -DiagnoseOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидание:
|
||||||
|
|
||||||
|
- В store `Remote Desktop` — cert с `NotAfter` в будущем (часто нативный).
|
||||||
|
- После фазы 6: **Hash AFTER** = thumbprint.
|
||||||
|
- RDP с другого ПК **без** `SecurityLayer=0`.
|
||||||
|
|
||||||
|
### 4. Возврат TLS
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1 -RestoreTls
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробнее: [RETURN-TO-TLS.md](RETURN-TO-TLS.md).
|
||||||
|
|
||||||
|
## DISM / SFC до repair (если ещё не делали)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
DISM /Online /Cleanup-Image /RestoreHealth
|
||||||
|
sfc /scannow
|
||||||
|
```
|
||||||
|
|
||||||
|
Перезагрузка, затем снова `Fix-RdpTls.ps1`.
|
||||||
|
|
||||||
|
## Если repair install не помог
|
||||||
|
|
||||||
|
- Чистая переустановка Windows с бэкапом данных.
|
||||||
|
- Проверка железа (диск, RAM).
|
||||||
|
- Эскалация в Microsoft Support с логами `%ProgramData%\RdpRepair`.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Возврат к нормальному TLS (SecurityLayer=2)
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
На DAK-PC временно работал RDP с **SecurityLayer=0** (шифрование RDP без TLS). На рабочих ПК домена (FVG-PC, NEW-ADMIN-PC) используется **SecurityLayer=2** (SSL/TLS) при пустом `SSLCertificateSHA1Hash` и нативном cert в store `Remote Desktop`.
|
||||||
|
|
||||||
|
**Не включайте SecurityLayer=2**, пока не выполнены критерии ниже.
|
||||||
|
|
||||||
|
## Критерии готовности
|
||||||
|
|
||||||
|
| Проверка | Ожидание |
|
||||||
|
|----------|----------|
|
||||||
|
| Cert в `LocalMachine\Remote Desktop` | есть, `NotAfter` > сегодня |
|
||||||
|
| `SSLCertificateSHA1Hash` после старта TermService | **не пустой**, совпадает с thumbprint |
|
||||||
|
| События 1057 за 10 мин после теста RDP | **0** |
|
||||||
|
| Schannel 36870 | **0** |
|
||||||
|
|
||||||
|
Проверка одной командой (локально на DAK-PC):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\path\to\fix-DAK-PC\scripts
|
||||||
|
.\Fix-RdpTls.ps1 -DiagnoseOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
## Возврат TLS
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1 -RestoreTls
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
|
||||||
|
1. Убедится, что cert и hash на месте (или попытается привязать).
|
||||||
|
2. Установит `SecurityLayer = 2`.
|
||||||
|
3. Перезапустит `TermService`.
|
||||||
|
4. Проверит hash **после** старта.
|
||||||
|
5. Выведет инструкцию сброса кэша RDP на клиенте.
|
||||||
|
|
||||||
|
## На клиенте RDP (ваш ПК)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Remove-Item "HKCU:\Software\Microsoft\Terminal Server Client\Servers\DAK-PC" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
# или имя FQDN, если подключаетесь по FQDN
|
||||||
|
Remove-Item "HKCU:\Software\Microsoft\Terminal Server Client\Servers\DAK-PC.b26.kalinamall.ru" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
```
|
||||||
|
|
||||||
|
Подключитесь снова по RDP.
|
||||||
|
|
||||||
|
## Если после RestoreTls RDP снова «внутренняя ошибка»
|
||||||
|
|
||||||
|
1. Срочный доступ:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Fix-RdpTls.ps1 -EmergencyRdpOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Повторите **repair install** ([REPAIR-INSTALL.md](REPAIR-INSTALL.md)).
|
||||||
|
3. Не оставляйте SecurityLayer=2 без рабочего cert.
|
||||||
|
|
||||||
|
## GPO
|
||||||
|
|
||||||
|
`LicenseServers` / `LicensingMode` в домене **не были** причиной на DAK-PC (одинаковы на всех ПК).
|
||||||
|
Если админ меняет политики — после `gpupdate /force` снова запустите `-DiagnoseOnly`.
|
||||||
|
|
||||||
|
## Значения SecurityLayer (справка)
|
||||||
|
|
||||||
|
| Значение | Режим |
|
||||||
|
|----------|--------|
|
||||||
|
| 0 | Только RDP Security Layer (legacy) |
|
||||||
|
| 1 | Negotiate |
|
||||||
|
| 2 | SSL (TLS) — **норма для вашей сети** |
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Troubleshooting — RDP / TLS / сертификаты
|
||||||
|
|
||||||
|
## События
|
||||||
|
|
||||||
|
| ID | Источник | Значение |
|
||||||
|
|----|----------|----------|
|
||||||
|
| **1057** | RemoteConnectionManager / System | Ошибка SSL/TLS RDP; часто «неправильный алгоритм» или «не удалось создать самозаверяющий сертификат» |
|
||||||
|
| **36870** | Schannel | `SEC_E_ALGORITHM_MISMATCH` (0x8009030d) — нет общего алгоритма с клиентом или нет валидного cert |
|
||||||
|
| **258** | Operational | Прослушиватель RDP-Tcp запущен (норма) |
|
||||||
|
| **261** | Operational | Входящее соединение на listener (норма) |
|
||||||
|
|
||||||
|
## Hash BEFORE есть, Hash AFTER пустой
|
||||||
|
|
||||||
|
TermService **отверг** сертификат при старте. Возможные причины:
|
||||||
|
|
||||||
|
- Просроченный cert (лечится заменой).
|
||||||
|
- Нет доступа к приватному ключу → фаза ACL в `Fix-RdpTls.ps1`.
|
||||||
|
- CNG vs CSP — скрипт пробует `RSA\MachineKeys`.
|
||||||
|
- Повреждение Schannel → repair install.
|
||||||
|
|
||||||
|
## Нативный cert не создаётся после reboot
|
||||||
|
|
||||||
|
Store `Remote Desktop` пустой после цикла «выкл RDP → очистка → вкл RDP → reboot»:
|
||||||
|
|
||||||
|
- Запустите `Fix-RdpTls.ps1` фазы 4–6 локально.
|
||||||
|
- Если не помогло — repair install.
|
||||||
|
|
||||||
|
## GPO Terminal Services
|
||||||
|
|
||||||
|
Путь: `HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services`
|
||||||
|
|
||||||
|
На DAK-PC и рабочих ПК может быть:
|
||||||
|
|
||||||
|
- `LicenseServers` = 192.168.160.150
|
||||||
|
- `LicensingMode` = 4
|
||||||
|
|
||||||
|
RDP при этом работал на других ПК — **не единственная причина** отказа DAK-PC. Убирать из GPO для Win10 Pro всё равно желательно (задача админа AD).
|
||||||
|
|
||||||
|
Локальное удаление **откатывается** `gpupdate /force`.
|
||||||
|
|
||||||
|
## Клиент «внутренняя ошибка»
|
||||||
|
|
||||||
|
1. Сброс кэша cert на клиенте (см. RETURN-TO-TLS.md).
|
||||||
|
2. Проверка 1057/36870 на **сервере** в момент подключения.
|
||||||
|
3. Сравнение `SecurityLayer` с рабочим ПК.
|
||||||
|
|
||||||
|
## WinRM vs RDP
|
||||||
|
|
||||||
|
WinRM может работать при сломанном RDP TLS — это нормально. Починка RDP не требует локальной консоли, но **локальный** запуск скриптов надёжнее для cert/ACL.
|
||||||
|
|
||||||
|
## Логи скриптов
|
||||||
|
|
||||||
|
- `%ProgramData%\RdpRepair\report-*.txt` — диагностика
|
||||||
|
- `%ProgramData%\RdpRepair\repair-*.log` — ход ремонта
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
#Requires -RunAsAdministrator
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Локальная починка RDP TLS / сертификата TermService.
|
||||||
|
|
||||||
|
.PARAMETER DiagnoseOnly
|
||||||
|
Только диагностика и отчёт.
|
||||||
|
|
||||||
|
.PARAMETER EmergencyRdpOnly
|
||||||
|
SecurityLayer=0 (без TLS) для срочного доступа.
|
||||||
|
|
||||||
|
.PARAMETER RestoreTls
|
||||||
|
Вернуть SecurityLayer=2 после успешной привязки cert.
|
||||||
|
|
||||||
|
.PARAMETER SkipReboot
|
||||||
|
Не предлагать reboot при нативном сбросе.
|
||||||
|
|
||||||
|
.PARAMETER NonInteractive
|
||||||
|
Без вопросов (для автоматизации). $env:RDP_REPAIR_NONINTERACTIVE=1
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$DiagnoseOnly,
|
||||||
|
[switch]$EmergencyRdpOnly,
|
||||||
|
[switch]$RestoreTls,
|
||||||
|
[switch]$SkipReboot,
|
||||||
|
[switch]$NonInteractive
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
if ($NonInteractive) { $env:RDP_REPAIR_NONINTERACTIVE = '1' }
|
||||||
|
|
||||||
|
$libDir = Join-Path $PSScriptRoot 'lib'
|
||||||
|
. (Join-Path $libDir 'RdpRepair.Common.ps1')
|
||||||
|
|
||||||
|
$logPath = Join-Path (Initialize-RdpRepairDataRoot) ("repair-{0:yyyyMMdd-HHmmss}.log" -f (Get-Date))
|
||||||
|
|
||||||
|
function Show-RdpRepairClientCacheHint {
|
||||||
|
Write-RdpRepairLog 'На клиенте RDP выполните:' 'INFO' $logPath
|
||||||
|
Write-RdpRepairLog ' Remove-Item "HKCU:\Software\Microsoft\Terminal Server Client\Servers\<имя-сервера>" -Recurse -Force' 'INFO' $logPath
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Assert-RdpRepairAdministrator
|
||||||
|
Write-RdpRepairLog "=== Fix-RdpTls start $(Get-Date -Format o) ===" 'STEP' $logPath
|
||||||
|
Write-RdpRepairLog "Компьютер: $env:COMPUTERNAME" 'INFO' $logPath
|
||||||
|
|
||||||
|
$report = Export-RdpRepairDiagnosticReport -LogPath $logPath
|
||||||
|
Write-RdpRepairLog "Диагностика сохранена: $report" 'OK' $logPath
|
||||||
|
|
||||||
|
if ($DiagnoseOnly) {
|
||||||
|
Get-RdpRepairDiagnosticSnapshot | Format-List
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($EmergencyRdpOnly) {
|
||||||
|
Write-RdpRepairLog 'ЭКСТРЕННЫЙ РЕЖИМ: SecurityLayer=0 (RDP без TLS)' 'WARN' $logPath
|
||||||
|
Set-RdpRepairSecurityLayer -Layer 0 -LogPath $logPath
|
||||||
|
Show-RdpRepairClientCacheHint
|
||||||
|
Write-RdpRepairLog 'Проверьте RDP. Для TLS после repair install: .\Fix-RdpTls.ps1 -RestoreTls' 'OK' $logPath
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RestoreTls) {
|
||||||
|
Write-RdpRepairLog 'Возврат SecurityLayer=2 (TLS)' 'STEP' $logPath
|
||||||
|
$certs = Get-RdpRepairRemoteDesktopCertificates
|
||||||
|
if ($certs.Count -eq 0) {
|
||||||
|
Write-RdpRepairLog 'No cert in Remote Desktop store - run full repair or repair install first' 'ERROR' $logPath
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
$valid = $certs | Where-Object { $_.NotAfter -gt (Get-Date) } | Select-Object -First 1
|
||||||
|
if (-not $valid) {
|
||||||
|
Write-RdpRepairLog 'Все cert просрочены' 'ERROR' $logPath
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
$thumb = $valid.Thumbprint
|
||||||
|
if (-not (Get-RdpRepairSslCertificateHash)) {
|
||||||
|
Write-RdpRepairLog "Привязка $($valid.Thumbprint)..." 'INFO' $logPath
|
||||||
|
Set-RdpRepairCertificateBinding -Thumbprint $thumb -LogPath $logPath | Out-Null
|
||||||
|
}
|
||||||
|
$bind = Test-RdpRepairCertificateBindingPersisted -ExpectedThumbprint $thumb -LogPath $logPath
|
||||||
|
if (-not $bind.Persisted) {
|
||||||
|
Write-RdpRepairLog 'TermService does not keep hash - cannot restore TLS. See docs\REPAIR-INSTALL.md' 'ERROR' $logPath
|
||||||
|
exit 3
|
||||||
|
}
|
||||||
|
Set-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SecurityLayer -Value 2
|
||||||
|
Restart-RdpRepairTermService
|
||||||
|
Write-RdpRepairLog 'SecurityLayer=2, TermService перезапущен' 'OK' $logPath
|
||||||
|
Show-RdpRepairClientCacheHint
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Полный интерактивный ремонт ---
|
||||||
|
Write-RdpRepairLog 'ФАЗА 1: текущее состояние' 'STEP' $logPath
|
||||||
|
$snap = Get-RdpRepairDiagnosticSnapshot
|
||||||
|
$snap | Format-List ComputerName, OsCaption, SecurityLayer, SslHash, CertCount, Events1057_24h, GpoLicenseSrv
|
||||||
|
|
||||||
|
$expired = @($snap.Certificates | Where-Object { $_.NotAfter -lt (Get-Date) })
|
||||||
|
if ($expired.Count -gt 0) {
|
||||||
|
Write-RdpRepairLog "Найдено просроченных cert: $($expired.Count)" 'WARN' $logPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Confirm-RdpRepairContinue 'Продолжить: очистка cert и нативный сброс?')) {
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-RdpRepairLog 'ФАЗА 2: очистка stores + нативный сброс' 'STEP' $logPath
|
||||||
|
$native = Invoke-RdpRepairNativeReset -LogPath $logPath
|
||||||
|
if ($native.CertCount -gt 0) {
|
||||||
|
Write-RdpRepairLog "После сброса без reboot: нативный cert найден ($($native.CertCount))" 'OK' $logPath
|
||||||
|
$nativeCert = $native.Certificates | Select-Object -First 1
|
||||||
|
$bindNative = Test-RdpRepairCertificateBindingPersisted -ExpectedThumbprint $nativeCert.Thumbprint -LogPath $logPath
|
||||||
|
if ($bindNative.Persisted) {
|
||||||
|
Write-RdpRepairLog 'Native cert accepted - you can use RestoreTls' 'OK' $logPath
|
||||||
|
if (Confirm-RdpRepairContinue 'Установить SecurityLayer=2 сейчас?') {
|
||||||
|
Set-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SecurityLayer -Value 2
|
||||||
|
Restart-RdpRepairTermService
|
||||||
|
}
|
||||||
|
Show-RdpRepairClientCacheHint
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SkipReboot -and (Confirm-RdpRepairContinue 'Нативный cert не прижился. Запланировать REBOOT через 60 с?')) {
|
||||||
|
Invoke-RdpRepairNativeReset -LogPath $logPath -Reboot | Out-Null
|
||||||
|
Write-RdpRepairLog 'После загрузки снова запустите: .\Fix-RdpTls.ps1' 'WARN' $logPath
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Confirm-RdpRepairContinue 'ФАЗА 3: создать локальный cert и привязать?')) {
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-RdpRepairLog 'ФАЗА 3: создание cert' 'STEP' $logPath
|
||||||
|
Clear-RdpRepairCertificateStores -LogPath $logPath
|
||||||
|
$dns = @(Get-RdpRepairComputerDnsNames)
|
||||||
|
Write-RdpRepairLog "DNS: $($dns -join ', ')" 'INFO' $logPath
|
||||||
|
$cert = New-RdpRepairSelfSignedCertificate -DnsNames $dns -LogPath $logPath
|
||||||
|
|
||||||
|
Write-RdpRepairLog 'ФАЗА 4: ACL + Remote Desktop store' 'STEP' $logPath
|
||||||
|
Set-RdpRepairPrivateKeyAcl -Certificate $cert -LogPath $logPath | Out-Null
|
||||||
|
Add-RdpRepairCertificateToRemoteDesktopStore -Certificate $cert
|
||||||
|
|
||||||
|
Write-RdpRepairLog 'ФАЗА 5: привязка и проверка AFTER' 'STEP' $logPath
|
||||||
|
Set-RdpRepairCertificateBinding -Thumbprint $cert.Thumbprint -LogPath $logPath | Out-Null
|
||||||
|
$bind = Test-RdpRepairCertificateBindingPersisted -ExpectedThumbprint $cert.Thumbprint -LogPath $logPath
|
||||||
|
|
||||||
|
if ($bind.Persisted) {
|
||||||
|
if (Confirm-RdpRepairContinue 'Привязка OK. Установить SecurityLayer=2?') {
|
||||||
|
Set-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SecurityLayer -Value 2
|
||||||
|
Restart-RdpRepairTermService
|
||||||
|
}
|
||||||
|
Show-RdpRepairClientCacheHint
|
||||||
|
Write-RdpRepairLog '=== Ремонт TLS завершён успешно ===' 'OK' $logPath
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-RdpRepairLog '=== TermService отклоняет cert ===' 'ERROR' $logPath
|
||||||
|
Write-RdpRepairLog 'Варианты:' 'WARN' $logPath
|
||||||
|
Write-RdpRepairLog ' 1) docs\REPAIR-INSTALL.md - repair install Windows' 'WARN' $logPath
|
||||||
|
Write-RdpRepairLog ' 2) .\Fix-RdpTls.ps1 -EmergencyRdpOnly - RDP without TLS' 'WARN' $logPath
|
||||||
|
if (Confirm-RdpRepairContinue 'Включить Emergency SecurityLayer=0 сейчас?') {
|
||||||
|
Set-RdpRepairSecurityLayer -Layer 0 -LogPath $logPath
|
||||||
|
Show-RdpRepairClientCacheHint
|
||||||
|
}
|
||||||
|
exit 4
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Write-RdpRepairLog "FATAL: $($_.Exception.Message)" 'ERROR' $logPath
|
||||||
|
Write-RdpRepairLog $_.ScriptStackTrace 'ERROR' $logPath
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Самотесты модуля RdpRepair (синтаксис, функции, dry-run логики).
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$IncludeAdminTests
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$script:SelfTestFailed = 0
|
||||||
|
$script:SelfTestPassed = 0
|
||||||
|
|
||||||
|
function Assert-Test {
|
||||||
|
param(
|
||||||
|
[string]$Name,
|
||||||
|
[scriptblock]$Test
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
& $Test
|
||||||
|
$script:SelfTestPassed++
|
||||||
|
Write-Host "[PASS] $Name" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
$script:SelfTestFailed++
|
||||||
|
Write-Host "[FAIL] $Name - $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$libPath = Join-Path $PSScriptRoot 'lib\RdpRepair.Common.ps1'
|
||||||
|
$mainPath = Join-Path $PSScriptRoot 'Fix-RdpTls.ps1'
|
||||||
|
. $libPath
|
||||||
|
|
||||||
|
Write-Host "`n=== RdpRepair SelfTest ===" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
Assert-Test 'RdpRepair.Common.ps1 exists' {
|
||||||
|
if (-not (Test-Path -LiteralPath $libPath)) { throw 'file missing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'Fix-RdpTls.ps1 exists' {
|
||||||
|
if (-not (Test-Path -LiteralPath $mainPath)) { throw 'file missing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'PowerShell parse RdpRepair.Common.ps1' {
|
||||||
|
$parseErrors = $null
|
||||||
|
$null = [System.Management.Automation.Language.Parser]::ParseFile($libPath, [ref]$null, [ref]$parseErrors)
|
||||||
|
if ($parseErrors.Count -gt 0) { throw (($parseErrors | ForEach-Object { $_.ToString() }) -join '; ') }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'PowerShell parse Fix-RdpTls.ps1' {
|
||||||
|
$parseErrors = $null
|
||||||
|
$null = [System.Management.Automation.Language.Parser]::ParseFile($mainPath, [ref]$null, [ref]$parseErrors)
|
||||||
|
if ($parseErrors.Count -gt 0) { throw (($parseErrors | ForEach-Object { $_.ToString() }) -join '; ') }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'Dot-source Common without throw' {
|
||||||
|
if (-not (Get-Command Initialize-RdpRepairDataRoot -ErrorAction SilentlyContinue)) {
|
||||||
|
throw 'Common module not loaded'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'Initialize-RdpRepairDataRoot returns path' {
|
||||||
|
$p = Initialize-RdpRepairDataRoot
|
||||||
|
if (-not (Test-Path -LiteralPath $p)) { throw 'dir not created' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'Get-RdpRepairComputerDnsNames non-empty' {
|
||||||
|
$n = Get-RdpRepairComputerDnsNames
|
||||||
|
if ($n.Count -lt 1) { throw 'no dns names' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'Get-RdpRepairDiagnosticSnapshot object shape' {
|
||||||
|
if (-not (Test-RdpRepairIsAdministrator)) {
|
||||||
|
Write-Host ' (skip snapshot fields - not admin)' -ForegroundColor DarkYellow
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$s = Get-RdpRepairDiagnosticSnapshot
|
||||||
|
foreach ($prop in @('ComputerName', 'SecurityLayer', 'CertCount', 'TermService')) {
|
||||||
|
if (-not $s.PSObject.Properties[$prop]) { throw "missing property $prop" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'Registry path constants well-formed' {
|
||||||
|
if ($script:RdpRepairRdpTcpKey -notmatch 'RDP-Tcp$') { throw 'bad rdp tcp key' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'Export-RdpRepairDiagnosticReport writes file' {
|
||||||
|
if (-not (Test-RdpRepairIsAdministrator)) {
|
||||||
|
Write-Host ' (skip report write - not admin)' -ForegroundColor DarkYellow
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$log = Join-Path $env:TEMP 'rdp-selftest.log'
|
||||||
|
$rp = Export-RdpRepairDiagnosticReport -LogPath $log
|
||||||
|
if (-not (Test-Path -LiteralPath $rp)) { throw 'report not written' }
|
||||||
|
if ((Get-Item $rp).Length -lt 50) { throw 'report too small' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'Confirm-RdpRepairContinue noninteractive' {
|
||||||
|
$env:RDP_REPAIR_NONINTERACTIVE = '1'
|
||||||
|
if (-not (Confirm-RdpRepairContinue 'test')) { throw 'expected true' }
|
||||||
|
Remove-Item Env:\RDP_REPAIR_NONINTERACTIVE -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'SecurityLayer values documented (0,1,2)' {
|
||||||
|
foreach ($v in 0, 1, 2) {
|
||||||
|
if ($v -lt 0 -or $v -gt 2) { throw 'invalid' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'docs REPAIR-INSTALL.md exists' {
|
||||||
|
$d = Join-Path (Split-Path $PSScriptRoot -Parent) 'docs\REPAIR-INSTALL.md'
|
||||||
|
if (-not (Test-Path -LiteralPath $d)) { throw 'missing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Test 'docs RETURN-TO-TLS.md exists' {
|
||||||
|
$d = Join-Path (Split-Path $PSScriptRoot -Parent) 'docs\RETURN-TO-TLS.md'
|
||||||
|
if (-not (Test-Path -LiteralPath $d)) { throw 'missing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($IncludeAdminTests -and (Test-RdpRepairIsAdministrator)) {
|
||||||
|
Write-Host "`n--- Admin-only tests ---" -ForegroundColor Cyan
|
||||||
|
Assert-Test 'TermService service exists' {
|
||||||
|
if (-not (Get-Service TermService -ErrorAction SilentlyContinue)) { throw 'TermService missing' }
|
||||||
|
}
|
||||||
|
Assert-Test 'Get-RdpRepairRemoteDesktopCertificates no throw' {
|
||||||
|
$null = Get-RdpRepairRemoteDesktopCertificates
|
||||||
|
}
|
||||||
|
} elseif ($IncludeAdminTests) {
|
||||||
|
Write-Host "`n(Warning: -IncludeAdminTests skipped - not running as Administrator)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n=== Results: PASS=$script:SelfTestPassed FAIL=$script:SelfTestFailed ===" -ForegroundColor Cyan
|
||||||
|
if ($script:SelfTestFailed -gt 0) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
|
$script:RdpRepairDataRoot = Join-Path $env:ProgramData 'RdpRepair'
|
||||||
|
$script:RdpRepairRdpTcpKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
|
||||||
|
$script:RdpRepairTsKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
|
||||||
|
|
||||||
|
function Initialize-RdpRepairDataRoot {
|
||||||
|
if (-not (Test-Path -LiteralPath $script:RdpRepairDataRoot)) {
|
||||||
|
New-Item -ItemType Directory -Path $script:RdpRepairDataRoot -Force | Out-Null
|
||||||
|
}
|
||||||
|
return $script:RdpRepairDataRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-RdpRepairLog {
|
||||||
|
param(
|
||||||
|
[string]$Message,
|
||||||
|
[ValidateSet('INFO', 'WARN', 'ERROR', 'OK', 'STEP')]
|
||||||
|
[string]$Level = 'INFO',
|
||||||
|
[string]$LogPath
|
||||||
|
)
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$line = "[$ts] [$Level] $Message"
|
||||||
|
if ($LogPath) {
|
||||||
|
Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8
|
||||||
|
}
|
||||||
|
switch ($Level) {
|
||||||
|
'ERROR' { Write-Host $line -ForegroundColor Red }
|
||||||
|
'WARN' { Write-Host $line -ForegroundColor Yellow }
|
||||||
|
'OK' { Write-Host $line -ForegroundColor Green }
|
||||||
|
'STEP' { Write-Host $line -ForegroundColor Cyan }
|
||||||
|
default { Write-Host $line }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-RdpRepairIsAdministrator {
|
||||||
|
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$p = New-Object Security.Principal.WindowsPrincipal($id)
|
||||||
|
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-RdpRepairAdministrator {
|
||||||
|
if (-not (Test-RdpRepairIsAdministrator)) {
|
||||||
|
throw 'Run PowerShell as Administrator.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RdpRepairComputerDnsNames {
|
||||||
|
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
|
||||||
|
$computer = $env:COMPUTERNAME
|
||||||
|
$fqdn = "$computer.$($cs.Domain)".ToLower()
|
||||||
|
$names = @($computer, $fqdn)
|
||||||
|
if ($fqdn -notmatch '\.') {
|
||||||
|
$names += "$computer.local"
|
||||||
|
}
|
||||||
|
return $names | Select-Object -Unique
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RdpRepairSslCertificateHash {
|
||||||
|
$p = Get-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SSLCertificateSHA1Hash -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $p -or [string]::IsNullOrWhiteSpace($p.SSLCertificateSHA1Hash)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
return $p.SSLCertificateSHA1Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RdpRepairSecurityLayer {
|
||||||
|
$p = Get-ItemProperty -Path $script:RdpRepairRdpTcpKey -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $p.SecurityLayer) { return '(default)' }
|
||||||
|
return [int]$p.SecurityLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RdpRepairRemoteDesktopCertificates {
|
||||||
|
$list = New-Object System.Collections.Generic.List[object]
|
||||||
|
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Remote Desktop', 'LocalMachine')
|
||||||
|
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
|
||||||
|
try {
|
||||||
|
foreach ($c in $store.Certificates) {
|
||||||
|
$list.Add([PSCustomObject]@{
|
||||||
|
Subject = $c.Subject
|
||||||
|
Thumbprint = $c.Thumbprint
|
||||||
|
NotAfter = $c.NotAfter
|
||||||
|
HasKey = $c.HasPrivateKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$store.Close()
|
||||||
|
}
|
||||||
|
return $list
|
||||||
|
}
|
||||||
|
|
||||||
|
function Clear-RdpRepairCertificateStores {
|
||||||
|
param([string]$LogPath)
|
||||||
|
foreach ($sn in @('Remote Desktop', 'My')) {
|
||||||
|
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($sn, 'LocalMachine')
|
||||||
|
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
|
||||||
|
try {
|
||||||
|
foreach ($c in @($store.Certificates)) {
|
||||||
|
Write-RdpRepairLog "Удаление cert из ${sn}: $($c.Thumbprint) ($($c.Subject))" 'INFO' $LogPath
|
||||||
|
$store.Remove($c)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$store.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Remove-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SSLCertificateSHA1Hash -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RdpRepairPrivateKeyPath {
|
||||||
|
param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate)
|
||||||
|
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
|
||||||
|
if (-not $rsa) { return $null }
|
||||||
|
$name = $rsa.Key.UniqueName
|
||||||
|
foreach ($base in @(
|
||||||
|
Join-Path $env:ProgramData 'Microsoft\Crypto\RSA\MachineKeys'
|
||||||
|
Join-Path $env:ProgramData 'Microsoft\Crypto\Keys'
|
||||||
|
)) {
|
||||||
|
$path = Join-Path $base $name
|
||||||
|
if (Test-Path -LiteralPath $path) { return $path }
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-RdpRepairPrivateKeyAcl {
|
||||||
|
param(
|
||||||
|
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
|
||||||
|
[string]$LogPath
|
||||||
|
)
|
||||||
|
$keyPath = Get-RdpRepairPrivateKeyPath -Certificate $Certificate
|
||||||
|
if (-not $keyPath) {
|
||||||
|
Write-RdpRepairLog 'Не найден файл приватного ключа' 'WARN' $LogPath
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
& icacls.exe $keyPath /grant 'NETWORK SERVICE:(F)' 'NT AUTHORITY\SYSTEM:(F)' 'BUILTIN\Administrators:(F)' 'NT AUTHORITY\LOCAL SERVICE:(R)' /c | Out-Null
|
||||||
|
Write-RdpRepairLog "ACL обновлён: $keyPath" 'OK' $LogPath
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-RdpRepairCertificateToRemoteDesktopStore {
|
||||||
|
param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate)
|
||||||
|
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Remote Desktop', 'LocalMachine')
|
||||||
|
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
|
||||||
|
try {
|
||||||
|
$store.Add($Certificate)
|
||||||
|
} finally {
|
||||||
|
$store.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-RdpRepairCertificateBinding {
|
||||||
|
param(
|
||||||
|
[string]$Thumbprint,
|
||||||
|
[string]$LogPath
|
||||||
|
)
|
||||||
|
$thumb = $Thumbprint.ToUpper()
|
||||||
|
$regPath = 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
|
||||||
|
& reg.exe add $regPath /v SSLCertificateSHA1Hash /t REG_SZ /d $thumb /f | Out-Null
|
||||||
|
try {
|
||||||
|
$ts = Get-CimInstance -Namespace root\cimv2\terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='RDP-Tcp'" -ErrorAction Stop
|
||||||
|
$ts | Set-CimInstance -Property @{ SSLCertificateSHA1Hash = $thumb } -ErrorAction Stop
|
||||||
|
Write-RdpRepairLog "WMI: SSLCertificateSHA1Hash = $thumb" 'INFO' $LogPath
|
||||||
|
} catch {
|
||||||
|
Write-RdpRepairLog "WMI Set-CimInstance: $($_.Exception.Message) (reg уже записан)" 'WARN' $LogPath
|
||||||
|
}
|
||||||
|
return (Get-RdpRepairSslCertificateHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restart-RdpRepairTermService {
|
||||||
|
param([int]$WaitSeconds = 8)
|
||||||
|
Stop-Service TermService -Force -ErrorAction Stop
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
Start-Service TermService -ErrorAction Stop
|
||||||
|
Start-Sleep -Seconds $WaitSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-RdpRepairCertificateBindingPersisted {
|
||||||
|
param(
|
||||||
|
[string]$ExpectedThumbprint,
|
||||||
|
[string]$LogPath
|
||||||
|
)
|
||||||
|
$before = Get-RdpRepairSslCertificateHash
|
||||||
|
Write-RdpRepairLog "Hash BEFORE TermService start: $(if ($before) { $before } else { '(empty)' })" 'INFO' $LogPath
|
||||||
|
Restart-RdpRepairTermService
|
||||||
|
$after = Get-RdpRepairSslCertificateHash
|
||||||
|
Write-RdpRepairLog "Hash AFTER TermService start: $(if ($after) { $after } else { '(empty)' })" 'INFO' $LogPath
|
||||||
|
$ok = ($after -eq $ExpectedThumbprint.ToUpper())
|
||||||
|
if ($ok) {
|
||||||
|
Write-RdpRepairLog 'Привязка cert принята TermService' 'OK' $LogPath
|
||||||
|
} else {
|
||||||
|
Write-RdpRepairLog 'TermService сбросил или отклонил привязку cert' 'ERROR' $LogPath
|
||||||
|
}
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Before = $before
|
||||||
|
After = $after
|
||||||
|
Persisted = $ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-RdpRepairSelfSignedCertificate {
|
||||||
|
param(
|
||||||
|
[string[]]$DnsNames,
|
||||||
|
[string]$LogPath
|
||||||
|
)
|
||||||
|
$providers = @(
|
||||||
|
@{ Name = 'Microsoft Enhanced RSA and AES Cryptographic Provider'; KeySpec = 'KeyExchange' }
|
||||||
|
@{ Name = $null; KeySpec = $null }
|
||||||
|
)
|
||||||
|
$notAfter = (Get-Date).AddYears(2)
|
||||||
|
$subject = "CN=$($DnsNames | Select-Object -First 1)"
|
||||||
|
foreach ($prov in $providers) {
|
||||||
|
try {
|
||||||
|
$params = @{
|
||||||
|
Subject = $subject
|
||||||
|
DnsName = $DnsNames
|
||||||
|
CertStoreLocation = 'Cert:\LocalMachine\My'
|
||||||
|
KeyAlgorithm = 'RSA'
|
||||||
|
KeyLength = 2048
|
||||||
|
KeyUsage = 'DigitalSignature', 'KeyEncipherment'
|
||||||
|
TextExtension = @('2.5.29.37={text}1.3.6.1.5.5.7.3.1')
|
||||||
|
NotAfter = $notAfter
|
||||||
|
}
|
||||||
|
if ($prov.Name) {
|
||||||
|
$params['Provider'] = $prov.Name
|
||||||
|
$params['KeySpec'] = $prov.KeySpec
|
||||||
|
}
|
||||||
|
$cert = New-SelfSignedCertificate @params
|
||||||
|
$keyPath = Get-RdpRepairPrivateKeyPath -Certificate $cert
|
||||||
|
$provLabel = if ($prov.Name) { $prov.Name } else { 'default' }
|
||||||
|
Write-RdpRepairLog "Создан cert $($cert.Thumbprint) провайдер=$provLabel key=$keyPath" 'OK' $LogPath
|
||||||
|
return $cert
|
||||||
|
} catch {
|
||||||
|
$provLabel = if ($prov.Name) { $prov.Name } else { 'default' }
|
||||||
|
Write-RdpRepairLog "New-SelfSignedCertificate ($provLabel): $($_.Exception.Message)" 'WARN' $LogPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw 'Не удалось создать самозаверяющий сертификат ни одним провайдером.'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RdpRepairNativeReset {
|
||||||
|
param(
|
||||||
|
[string]$LogPath,
|
||||||
|
[switch]$Reboot
|
||||||
|
)
|
||||||
|
Write-RdpRepairLog 'Нативный сброс RDP cert (без ручного cert)' 'STEP' $LogPath
|
||||||
|
Stop-Service TermService -Force -ErrorAction SilentlyContinue
|
||||||
|
Set-ItemProperty -Path $script:RdpRepairTsKey -Name fDenyTSConnections -Value 1
|
||||||
|
Clear-RdpRepairCertificateStores -LogPath $LogPath
|
||||||
|
Set-ItemProperty -Path $script:RdpRepairTsKey -Name fDenyTSConnections -Value 0
|
||||||
|
if ($Reboot) {
|
||||||
|
Write-RdpRepairLog 'Перезагрузка через 60 с для нативного создания cert...' 'WARN' $LogPath
|
||||||
|
shutdown.exe /r /t 60 /f /c 'RdpRepair: native certificate rebuild'
|
||||||
|
return [PSCustomObject]@{ RebootScheduled = $true }
|
||||||
|
}
|
||||||
|
Start-Service TermService -ErrorAction SilentlyContinue
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
$certs = Get-RdpRepairRemoteDesktopCertificates
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
RebootScheduled = $false
|
||||||
|
CertCount = $certs.Count
|
||||||
|
Certificates = $certs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RdpRepairDiagnosticSnapshot {
|
||||||
|
$polPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services'
|
||||||
|
$pol = if (Test-Path $polPath) { Get-ItemProperty $polPath -ErrorAction SilentlyContinue } else { $null }
|
||||||
|
$fips = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy' -Name Enabled -ErrorAction SilentlyContinue).Enabled
|
||||||
|
$certs = Get-RdpRepairRemoteDesktopCertificates
|
||||||
|
$ev1057 = @(Get-WinEvent -FilterHashtable @{
|
||||||
|
LogName = 'System'
|
||||||
|
Id = 1057, 36870
|
||||||
|
StartTime = (Get-Date).AddHours(-24)
|
||||||
|
} -ErrorAction SilentlyContinue).Count
|
||||||
|
$os = Get-CimInstance Win32_OperatingSystem
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
ComputerName = $env:COMPUTERNAME
|
||||||
|
OsCaption = $os.Caption
|
||||||
|
OsBuild = $os.BuildNumber
|
||||||
|
FipsEnabled = $fips
|
||||||
|
SecurityLayer = Get-RdpRepairSecurityLayer
|
||||||
|
SslHash = Get-RdpRepairSslCertificateHash
|
||||||
|
TermService = (Get-Service TermService).Status
|
||||||
|
RdpAllowed = -not (Get-ItemProperty $script:RdpRepairTsKey).fDenyTSConnections
|
||||||
|
CertCount = $certs.Count
|
||||||
|
Certificates = $certs
|
||||||
|
GpoLicenseSrv = $pol.LicenseServers
|
||||||
|
GpoLicMode = $pol.LicensingMode
|
||||||
|
Events1057_24h = $ev1057
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Export-RdpRepairDiagnosticReport {
|
||||||
|
param([string]$LogPath)
|
||||||
|
$snap = Get-RdpRepairDiagnosticSnapshot
|
||||||
|
$reportPath = Join-Path (Initialize-RdpRepairDataRoot) ("report-{0:yyyyMMdd-HHmmss}.txt" -f (Get-Date))
|
||||||
|
$lines = @(
|
||||||
|
"=== RDP/TLS Diagnostic Report ==="
|
||||||
|
"Generated: $(Get-Date -Format o)"
|
||||||
|
""
|
||||||
|
)
|
||||||
|
$snap.PSObject.Properties | ForEach-Object {
|
||||||
|
if ($_.Name -eq 'Certificates') {
|
||||||
|
$lines += 'Certificates:'
|
||||||
|
foreach ($c in $_.Value) {
|
||||||
|
$lines += (' - {0}; Thumb={1}; NotAfter={2}; Key={3}' -f $c.Subject, $c.Thumbprint, $c.NotAfter, $c.HasKey)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$lines += ('{0}: {1}' -f $_.Name, $_.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$text = $lines -join "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($reportPath, $text, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
Write-RdpRepairLog "Отчёт: $reportPath" 'OK' $LogPath
|
||||||
|
return $reportPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-RdpRepairSecurityLayer {
|
||||||
|
param(
|
||||||
|
[int]$Layer,
|
||||||
|
[string]$LogPath
|
||||||
|
)
|
||||||
|
Stop-Service TermService -Force -ErrorAction Stop
|
||||||
|
Set-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SecurityLayer -Value $Layer
|
||||||
|
if ($Layer -eq 0 -or $Layer -eq 1) {
|
||||||
|
Remove-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SSLCertificateSHA1Hash -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
Start-Service TermService -ErrorAction Stop
|
||||||
|
Write-RdpRepairLog "SecurityLayer установлен в $Layer" 'OK' $LogPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function Confirm-RdpRepairContinue {
|
||||||
|
param([string]$Prompt)
|
||||||
|
if ($env:RDP_REPAIR_NONINTERACTIVE -eq '1') { return $true }
|
||||||
|
$r = Read-Host "$Prompt [Y/n]"
|
||||||
|
return ($r -eq '' -or $r -match '^[yY]$')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user