Automate direct domain enrollment across Windows versions

This commit is contained in:
2026-09-11 17:34:19 -06:00
parent 7f8a9eed4e
commit 520b4be955
23 changed files with 1244 additions and 108 deletions
@@ -0,0 +1,99 @@
#Requires -Version 5.1
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[string]$ConnectionName = 'SGU Azure Device',
[ValidateRange(30,600)][int]$WaitSeconds = 180
)
$ErrorActionPreference = 'Stop'
$computer = Get-CimInstance Win32_ComputerSystem
if (-not $computer.PartOfDomain) { throw 'The device must already be joined to its domain.' }
$logPath = Join-Path $env:ProgramData 'SGU\Enrollment\azure-domain-connectivity.json'
$deadline = (Get-Date).AddSeconds($WaitSeconds)
$restarted = $false
$controller = $null
try {
do {
$vpn = Get-VpnConnection -Name $ConnectionName -AllUserConnection -ErrorAction SilentlyContinue
$reachable = $false
if ($vpn -and $vpn.ConnectionStatus -eq 'Connected') {
$record = Resolve-DnsName "_ldap._tcp.dc._msdcs.$($computer.Domain)" -Type SRV -ErrorAction SilentlyContinue |
Where-Object Type -eq 'SRV' | Select-Object -First 1
if ($record) {
$controller = $record.NameTarget.TrimEnd('.')
$socket = [Net.Sockets.TcpClient]::new()
try {
$connect = $socket.BeginConnect($controller, 389, $null, $null)
if ($connect.AsyncWaitHandle.WaitOne(2000)) {
$socket.EndConnect($connect)
$reachable = $socket.Connected
}
} catch { $reachable = $false }
finally { $socket.Dispose() }
}
}
if ($reachable) { break }
Start-Sleep -Seconds 5
} while ((Get-Date) -lt $deadline)
if (-not $reachable) { throw "The VPN and a domain controller were not reachable within $WaitSeconds seconds." }
# An early Netlogon attempt can remain failed after the device VPN connects.
# Refresh only that service, after confirming the domain is reachable.
if (-not (Test-ComputerSecureChannel -Server $controller)) {
Restart-Service -Name Netlogon
$restarted = $true
}
$secure = $false
for ($attempt = 0; $attempt -lt 6; $attempt++) {
$secure = Test-ComputerSecureChannel -Server $controller
if ($secure) { break }
Start-Sleep -Seconds 5
}
if (-not $secure) { throw 'The domain is reachable but the secure channel is still invalid. Administrative repair is required.' }
$guard = Get-ScheduledTask -TaskName 'SGU-CredentialProvider-EnrollmentGuard' -ErrorAction SilentlyContinue
$guardResult = $null
if ($guard) {
# Domain principal lookup can recover after the secure channel itself.
# Await the guard and retry a transient failure instead of reporting
# success while its asynchronous repair is still running or failed.
$guardDeadline = (Get-Date).AddMinutes(3)
do {
$guard = Get-ScheduledTask -TaskName $guard.TaskName
if ($guard.State -notin @('Running','Queued')) {
$previousRun = (Get-ScheduledTaskInfo -TaskName $guard.TaskName).LastRunTime
Start-ScheduledTask -InputObject $guard
do {
Start-Sleep -Seconds 2
$guard = Get-ScheduledTask -TaskName $guard.TaskName
$info = Get-ScheduledTaskInfo -TaskName $guard.TaskName
} while (($info.LastRunTime -le $previousRun -or $guard.State -in @('Running','Queued')) -and (Get-Date) -lt $guardDeadline)
if ($info.LastRunTime -gt $previousRun -and $guard.State -notin @('Running','Queued')) {
$guardResult = $info.LastTaskResult
if ($guardResult -eq 0) { break }
}
}
Start-Sleep -Seconds 10
} while ((Get-Date) -lt $guardDeadline)
if ($guardResult -ne 0) { throw "The secure channel recovered, but the enrollment guard did not succeed (result $guardResult)." }
}
[pscustomobject]@{
CheckedAt = (Get-Date).ToString('o')
ComputerName = $computer.Name
Domain = $computer.Domain
DomainController = $controller
ConnectionName = $ConnectionName
NetlogonRestarted = $restarted
SecureChannel = $secure
EnrollmentGuardResult = $guardResult
} | ConvertTo-Json | Set-Content -LiteralPath $logPath
} catch {
[pscustomobject]@{
CheckedAt = (Get-Date).ToString('o')
ConnectionName = $ConnectionName
NetlogonRestarted = $restarted
SecureChannel = $false
Error = $_.Exception.Message
} | ConvertTo-Json | Set-Content -LiteralPath $logPath
throw
}