Automate direct domain enrollment across Windows versions
This commit is contained in:
@@ -181,6 +181,18 @@ function Test-IPv4AddressesSharePrefix {
|
||||
return $true
|
||||
}
|
||||
|
||||
function Test-PrivateIPv4Address {
|
||||
param([Parameter(Mandatory)][ipaddress]$Address)
|
||||
|
||||
if ($Address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
|
||||
return $false
|
||||
}
|
||||
$bytes = $Address.GetAddressBytes()
|
||||
return $bytes[0] -eq 10 -or
|
||||
($bytes[0] -eq 172 -and $bytes[1] -ge 16 -and $bytes[1] -le 31) -or
|
||||
($bytes[0] -eq 192 -and $bytes[1] -eq 168)
|
||||
}
|
||||
|
||||
function Wait-ClientInterface {
|
||||
param(
|
||||
[string]$RequestedAlias,
|
||||
@@ -355,6 +367,95 @@ function Set-ClientDomainDns {
|
||||
Clear-DnsClientCache
|
||||
}
|
||||
|
||||
function Test-ClientDomainDns {
|
||||
param([Parameter(Mandatory)][string]$DnsDomain)
|
||||
|
||||
try {
|
||||
$records = @(Resolve-DnsName -Type SRV "_ldap._tcp.dc._msdcs.$DnsDomain" `
|
||||
-DnsOnly -ErrorAction Stop)
|
||||
return @($records | Where-Object {
|
||||
$_.Type -eq 'SRV' -and -not [string]::IsNullOrWhiteSpace([string]$_.NameTarget)
|
||||
}).Count -gt 0
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Set-ClientHostMappings {
|
||||
param(
|
||||
[Parameter(Mandatory)][ipaddress]$ServerAddress,
|
||||
[Parameter(Mandatory)][string[]]$HostNames
|
||||
)
|
||||
|
||||
$hostsPath = Join-Path $env:SystemRoot 'System32\drivers\etc\hosts'
|
||||
$managedNames = @($HostNames |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
ForEach-Object { $_.Trim().ToLowerInvariant() } |
|
||||
Select-Object -Unique)
|
||||
$preservedLines = foreach ($line in [IO.File]::ReadAllLines($hostsPath)) {
|
||||
$data = ($line -split '#', 2)[0].Trim()
|
||||
$tokens = @($data -split '\s+' | Where-Object { $_ })
|
||||
$lineNames = if ($tokens.Count -gt 1) {
|
||||
@($tokens[1..($tokens.Count - 1)] | ForEach-Object { $_.ToLowerInvariant() })
|
||||
}
|
||||
else { @() }
|
||||
if (@($lineNames | Where-Object { $managedNames -contains $_ }).Count -eq 0) {
|
||||
$line
|
||||
}
|
||||
}
|
||||
$mapping = '{0} {1} # SGU managed direct enrollment' -f
|
||||
$ServerAddress.IPAddressToString,($managedNames -join ' ')
|
||||
[IO.File]::WriteAllLines($hostsPath, @($preservedLines) + $mapping,
|
||||
[Text.UTF8Encoding]::new($false))
|
||||
Clear-DnsClientCache
|
||||
}
|
||||
|
||||
function Enable-ClientDnsOverHttps {
|
||||
param(
|
||||
[Parameter(Mandatory)][ipaddress]$ServerAddress,
|
||||
[Parameter(Mandatory)][string]$DohTemplate,
|
||||
[Parameter(Mandatory)][string]$CertificateBase64
|
||||
)
|
||||
|
||||
if (-not (Get-Command Add-DnsClientDohServerAddress -ErrorAction SilentlyContinue)) {
|
||||
throw 'This Windows build cannot configure DNS over HTTPS. Permit traditional DNS to the supplied server or update Windows, then retry.'
|
||||
}
|
||||
|
||||
$certificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
|
||||
[Convert]::FromBase64String($CertificateBase64))
|
||||
$store = [Security.Cryptography.X509Certificates.X509Store]::new(
|
||||
[Security.Cryptography.X509Certificates.StoreName]::Root,
|
||||
[Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine)
|
||||
try {
|
||||
$store.Open([Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
|
||||
if (-not @($store.Certificates | Where-Object Thumbprint -eq $certificate.Thumbprint).Count) {
|
||||
$store.Add($certificate)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$store.Close()
|
||||
$certificate.Dispose()
|
||||
}
|
||||
|
||||
$existing = Get-DnsClientDohServerAddress -ErrorAction SilentlyContinue |
|
||||
Where-Object ServerAddress -eq $ServerAddress.IPAddressToString |
|
||||
Select-Object -First 1
|
||||
if ($existing) {
|
||||
Set-DnsClientDohServerAddress -ServerAddress $ServerAddress.IPAddressToString `
|
||||
-DohTemplate $DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true | Out-Null
|
||||
}
|
||||
else {
|
||||
Add-DnsClientDohServerAddress -ServerAddress $ServerAddress.IPAddressToString `
|
||||
-DohTemplate $DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true | Out-Null
|
||||
}
|
||||
& "$env:SystemRoot\System32\netsh.exe" dnsclient set global doh=yes | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Windows did not enable its global DNS over HTTPS client setting.'
|
||||
}
|
||||
Clear-DnsClientCache
|
||||
}
|
||||
|
||||
function Assert-ClientOperatingSystem {
|
||||
param(
|
||||
[Parameter(Mandatory)]$OperatingSystem,
|
||||
@@ -429,6 +530,11 @@ if ($DomainControllerIPv4Address.AddressFamily -ne [Net.Sockets.AddressFamily]::
|
||||
$DomainControllerIPv4Address.IPAddressToString -match '^(0\.|127\.|169\.254\.|22[4-9]\.|23\d\.|24\d\.|25[0-5]\.)') {
|
||||
throw 'Enter a reachable unicast IPv4 address for the domain controller.'
|
||||
}
|
||||
$publicDirectEnrollment = $ConnectivityMode -eq 'Direct' -and
|
||||
-not (Test-PrivateIPv4Address -Address $DomainControllerIPv4Address)
|
||||
if ($publicDirectEnrollment) {
|
||||
Write-Host 'Public domain-controller address detected. Direct DNS and domain discovery will be configured automatically.'
|
||||
}
|
||||
|
||||
$packageRoot = $PSScriptRoot
|
||||
$packageManifest = Assert-PackageManifest -PackageRoot $packageRoot
|
||||
@@ -595,7 +701,141 @@ try {
|
||||
$serverIdentity.RustDeskHbbrTask -ne 'Running') {
|
||||
throw "The RustDesk server is not ready on $($serverIdentity.ComputerName). Run the current server bootstrap first."
|
||||
}
|
||||
|
||||
$targetComputerName = if ($NewComputerName) { $NewComputerName } else { $env:COMPUTERNAME }
|
||||
Invoke-Command -Session $session -ScriptBlock {
|
||||
param($ComputerName, $ComputerPath)
|
||||
Import-Module ActiveDirectory -ErrorAction Stop
|
||||
$samAccountName = "$ComputerName`$"
|
||||
$account = Get-ADComputer -Filter "SamAccountName -eq '$samAccountName'" |
|
||||
Select-Object -First 1
|
||||
if (-not $account) {
|
||||
New-ADComputer -Name $ComputerName -SamAccountName $samAccountName `
|
||||
-Path $ComputerPath -Enabled $true -ErrorAction Stop
|
||||
}
|
||||
} -ArgumentList $targetComputerName,$ComputerOuDn
|
||||
|
||||
if ($publicDirectEnrollment) {
|
||||
$directDns = Invoke-Command -Session $session -ScriptBlock {
|
||||
param($DnsDomain, $DomainControllerComputerName)
|
||||
|
||||
$domainControllerFqdn = "$DomainControllerComputerName.$DnsDomain".ToLowerInvariant()
|
||||
$dohTemplate = "https://${domainControllerFqdn}:443/dns-query"
|
||||
$dohCommand = Get-Command Set-DnsServerEncryptionProtocol -ErrorAction SilentlyContinue
|
||||
if (-not $dohCommand) {
|
||||
return [pscustomobject]@{
|
||||
DohSupported = $false
|
||||
DomainControllerFqdn = $domainControllerFqdn
|
||||
}
|
||||
}
|
||||
|
||||
$certificate = Get-ChildItem Cert:\LocalMachine\My |
|
||||
Where-Object {
|
||||
$_.Subject -eq "CN=$domainControllerFqdn" -and
|
||||
$_.HasPrivateKey -and
|
||||
$_.NotAfter -gt (Get-Date).AddDays(30)
|
||||
} |
|
||||
Sort-Object NotAfter -Descending |
|
||||
Select-Object -First 1
|
||||
if (-not $certificate) {
|
||||
$certificate = New-SelfSignedCertificate `
|
||||
-DnsName $domainControllerFqdn `
|
||||
-CertStoreLocation Cert:\LocalMachine\My `
|
||||
-FriendlyName 'SGU Direct Enrollment DoH' `
|
||||
-Type SSLServerAuthentication `
|
||||
-KeyAlgorithm RSA `
|
||||
-KeyLength 2048 `
|
||||
-HashAlgorithm SHA256 `
|
||||
-KeyExportPolicy NonExportable `
|
||||
-NotAfter (Get-Date).AddYears(2)
|
||||
}
|
||||
|
||||
$bindingOutput = @(& "$env:SystemRoot\System32\netsh.exe" http show sslcert ipport=0.0.0.0:443 2>&1)
|
||||
$bindingExists = $LASTEXITCODE -eq 0
|
||||
$normalizedBinding = (($bindingOutput -join '') -replace '[^0-9A-Fa-f]', '').ToUpperInvariant()
|
||||
$normalizedThumbprint = ($certificate.Thumbprint -replace ' ', '').ToUpperInvariant()
|
||||
if ($bindingExists -and -not $normalizedBinding.Contains($normalizedThumbprint)) {
|
||||
throw 'TCP 443 already has an HTTPS certificate binding that is not managed by SGU. Free that port or configure SGU DoH before enrolling this client.'
|
||||
}
|
||||
if (-not $bindingExists) {
|
||||
& "$env:SystemRoot\System32\netsh.exe" http add sslcert `
|
||||
ipport=0.0.0.0:443 "certhash=$($certificate.Thumbprint)" `
|
||||
"appid={47E9CF26-79B7-4C9D-A0AE-ADFA22447A41}" certstorename=MY | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not bind the SGU DoH certificate to TCP 443.' }
|
||||
}
|
||||
|
||||
$dnsChanged = $false
|
||||
$encryption = Get-DnsServerEncryptionProtocol
|
||||
if (-not $encryption.EnableDoh -or $encryption.UriTemplate -ne $dohTemplate) {
|
||||
Set-DnsServerEncryptionProtocol -EnableDoh $true -UriTemplate $dohTemplate
|
||||
$dnsChanged = $true
|
||||
}
|
||||
|
||||
Import-Module ActiveDirectory -ErrorAction Stop
|
||||
$domainController = Get-ADComputer -Identity $DomainControllerComputerName `
|
||||
-Properties ServicePrincipalName
|
||||
if (@($domainController.ServicePrincipalName) -notcontains "cifs/$DnsDomain") {
|
||||
& "$env:SystemRoot\System32\setspn.exe" -S "cifs/$DnsDomain" $DomainControllerComputerName | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Could not register cifs/$DnsDomain on $DomainControllerComputerName." }
|
||||
}
|
||||
|
||||
$lanmanPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters'
|
||||
$optionalNames = @((Get-ItemProperty $lanmanPath -Name OptionalNames `
|
||||
-ErrorAction SilentlyContinue).OptionalNames | Where-Object { $_ })
|
||||
$serverChanged = $false
|
||||
if ($optionalNames -notcontains $DnsDomain) {
|
||||
New-ItemProperty -Path $lanmanPath -Name OptionalNames -PropertyType MultiString `
|
||||
-Value (@($optionalNames) + $DnsDomain) -Force | Out-Null
|
||||
$serverChanged = $true
|
||||
}
|
||||
New-ItemProperty -Path $lanmanPath -Name DisableStrictNameChecking `
|
||||
-PropertyType DWord -Value 1 -Force | Out-Null
|
||||
|
||||
if ($serverChanged) {
|
||||
Restart-Service LanmanServer -Force
|
||||
Start-Service Netlogon
|
||||
}
|
||||
if ($dnsChanged) {
|
||||
Restart-Service DNS -Force
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
DohSupported = $true
|
||||
DohTemplate = $dohTemplate
|
||||
DohCertificateBase64 = [Convert]::ToBase64String($certificate.RawData)
|
||||
DomainControllerFqdn = $domainControllerFqdn
|
||||
}
|
||||
} -ArgumentList $DomainName,$serverIdentity.ComputerName
|
||||
|
||||
$directHostNames = @(
|
||||
$directDns.DomainControllerFqdn,
|
||||
$DomainName,
|
||||
$brokerDnsName
|
||||
)
|
||||
if ([string]$serverIdentity.RustDeskServerAddress -match '[A-Za-z]') {
|
||||
$directHostNames += [string]$serverIdentity.RustDeskServerAddress
|
||||
}
|
||||
Set-ClientHostMappings -ServerAddress $DomainControllerIPv4Address `
|
||||
-HostNames $directHostNames
|
||||
|
||||
if ($directDns.DohSupported -and
|
||||
(Get-Command Add-DnsClientDohServerAddress -ErrorAction SilentlyContinue)) {
|
||||
Enable-ClientDnsOverHttps -ServerAddress $DomainControllerIPv4Address `
|
||||
-DohTemplate $directDns.DohTemplate `
|
||||
-CertificateBase64 $directDns.DohCertificateBase64
|
||||
}
|
||||
elseif (-not $directDns.DohSupported) {
|
||||
Write-Warning 'The server does not support DNS over HTTPS; enrollment will use traditional DNS.'
|
||||
}
|
||||
else {
|
||||
Write-Warning 'This Windows build does not support DNS over HTTPS; enrollment will use traditional DNS.'
|
||||
}
|
||||
}
|
||||
Set-ClientDomainDns -DnsDomain $DomainName -ServerAddress $DomainControllerIPv4Address
|
||||
if (-not (Test-ClientDomainDns -DnsDomain $DomainName)) {
|
||||
throw "The domain DNS service at $DomainControllerIPv4Address did not return an Active Directory SRV record. For a public server, permit DNS over HTTPS on TCP 443 or traditional DNS from this client network."
|
||||
}
|
||||
foreach ($port in @(53, 88, 135, 389, 445, 8443)) {
|
||||
if (-not (Test-TcpPort -Address $DomainControllerIPv4Address -Port $port -TimeoutMilliseconds 2000)) {
|
||||
throw "Server $DomainControllerIPv4Address is reachable, but required TCP port $port is unavailable. Check AD/SGU services and the LAN/VPN firewall. Domain join has not started."
|
||||
@@ -680,6 +920,7 @@ try {
|
||||
DomainCredential = $DomainCredential
|
||||
DomainName = $DomainName
|
||||
DomainNetbios = $DomainNetbios
|
||||
DomainControllerDnsName = "$($serverIdentity.ComputerName).$DomainName"
|
||||
ComputerOuDn = $ComputerOuDn
|
||||
NetworkInterfaceAlias = $NetworkInterfaceAlias
|
||||
DomainDnsServerAddresses = @($DomainControllerIPv4Address.IPAddressToString)
|
||||
|
||||
Reference in New Issue
Block a user