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
+27 -11
View File
@@ -7,7 +7,8 @@ param(
[string]$DeploymentPrefix = 'sgu-lab',
[Parameter(Mandatory)][string]$AdministratorUsername,
[securestring]$AdministratorPassword,
[Parameter(Mandatory)][string]$P2sRootCertificatePath,
[string]$P2sRootCertificatePath,
[bool]$DeployVpnGateway = $true,
[string]$ComputerName = 'SGU-DC01',
[string]$VmSize = 'Standard_D2s_v5',
[string]$VirtualNetworkAddressPrefix = '10.77.0.0/16',
@@ -15,6 +16,7 @@ param(
[ipaddress]$DomainControllerPrivateIp = '10.77.0.4',
[string]$GatewaySubnetPrefix = '10.77.255.0/27',
[string]$VpnClientAddressPoolPrefix = '172.30.0.0/24',
[string[]]$PublicEnrollmentSourceAddressPrefixes = @(),
[string]$AdministratorSourceAddressPrefix = '',
[string]$TemplateFile = (Join-Path $PSScriptRoot '..\infra\azure\main.bicep')
)
@@ -28,20 +30,24 @@ if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
if (-not (Test-Path -LiteralPath $TemplateFile -PathType Leaf)) {
throw "Azure Bicep template not found: $TemplateFile"
}
if (-not (Test-Path -LiteralPath $P2sRootCertificatePath -PathType Leaf)) {
throw "P2S root certificate not found: $P2sRootCertificatePath"
}
if (-not $AdministratorPassword) {
$AdministratorPassword = Read-Host 'Password for the local Azure VM administrator' -AsSecureString
}
$rootCertificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
(Resolve-Path -LiteralPath $P2sRootCertificatePath).Path)
if (-not ($rootCertificate.Extensions | Where-Object {
$_.Oid -and $_.Oid.Value -eq '2.5.29.19' -and $_.Format($false) -match 'CA' })) {
throw 'P2sRootCertificatePath must contain a certificate-authority certificate.'
$rootCertificateData = ''
if ($DeployVpnGateway) {
if (-not $P2sRootCertificatePath -or
-not (Test-Path -LiteralPath $P2sRootCertificatePath -PathType Leaf)) {
throw 'P2sRootCertificatePath is required when DeployVpnGateway is true.'
}
$rootCertificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
(Resolve-Path -LiteralPath $P2sRootCertificatePath).Path)
if (-not ($rootCertificate.Extensions | Where-Object {
$_.Oid -and $_.Oid.Value -eq '2.5.29.19' -and $_.Format($false) -match 'CA' })) {
throw 'P2sRootCertificatePath must contain a certificate-authority certificate.'
}
$rootCertificateData = [Convert]::ToBase64String($rootCertificate.RawData)
}
$rootCertificateData = [Convert]::ToBase64String($rootCertificate.RawData)
$account = & az account show --output json 2>$null
if ($LASTEXITCODE -ne 0) {
@@ -52,7 +58,13 @@ if ($LASTEXITCODE -ne 0) {
throw "Could not select Azure subscription $SubscriptionId."
}
if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", 'Create Azure VNet, Windows Server 2025 VM, public IP, and P2S VPN Gateway')) {
$deploymentDescription = if ($DeployVpnGateway) {
'Create Azure VNet, Windows Server 2025 VM, public IP, and P2S VPN Gateway'
}
else {
'Create Azure VNet, Windows Server 2025 VM, and public IP for direct enrollment'
}
if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", $deploymentDescription)) {
& az group create --name $ResourceGroupName --location $Location --only-show-errors --output none
if ($LASTEXITCODE -ne 0) {
throw "Could not create or update resource group $ResourceGroupName."
@@ -89,7 +101,9 @@ if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", 'Create Azure VNe
gatewaySubnetPrefix = @{ value = $GatewaySubnetPrefix }
domainControllerPrivateIp = @{ value = $DomainControllerPrivateIp.IPAddressToString }
vpnClientAddressPoolPrefix = @{ value = $VpnClientAddressPoolPrefix }
deployVpnGateway = @{ value = $DeployVpnGateway }
p2sRootCertificateData = @{ value = $rootCertificateData }
publicEnrollmentSourceAddressPrefixes = @{ value = @($PublicEnrollmentSourceAddressPrefixes) }
administratorSourceAddressPrefix = @{ value = $AdministratorSourceAddressPrefix }
}
}
@@ -135,6 +149,8 @@ if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", 'Create Azure VNe
DomainControllerPublicIp = $values.domainControllerPublicIp
VpnGatewayName = $values.vpnGatewayName
VpnClientAddressPoolPrefix = $values.vpnClientAddressPoolPrefix
DeployVpnGateway = $DeployVpnGateway
PublicEnrollmentSourceAddressPrefixes = @($PublicEnrollmentSourceAddressPrefixes)
ServerBootstrapArguments = $values.serverBootstrapArguments
}
}
+23 -5
View File
@@ -18,6 +18,24 @@ if (-not $computer.PartOfDomain) {
$remoteDesktopUsersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-555')
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
$remoteDesktopPrincipalSid = ([Security.Principal.NTAccount]::new($RemoteDesktopPrincipal)).Translate(
[Security.Principal.SecurityIdentifier])
function Get-LocalGroupMemberSid {
param([Parameter(Mandatory)][string]$Name)
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$Name,group")
foreach ($member in @($group.psbase.Invoke('Members'))) {
try {
$sidBytes = $member.GetType().InvokeMember('objectSid',
[Reflection.BindingFlags]::GetProperty, $null, $member, $null)
if ($sidBytes) {
([Security.Principal.SecurityIdentifier]::new([byte[]]$sidBytes, 0)).Value
}
}
catch { }
}
}
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesktopPrincipal access")) {
function Invoke-PowerCfgBestEffort {
@@ -57,9 +75,9 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesk
-ErrorAction SilentlyContinue |
Set-NetFirewallRule -Enabled True -Profile Domain
$existingMembers = @(Get-LocalGroupMember -Group $remoteDesktopUsersGroup -ErrorAction SilentlyContinue)
if ($existingMembers.Name -notcontains $RemoteDesktopPrincipal) {
Add-LocalGroupMember -Group $remoteDesktopUsersGroup -Member $RemoteDesktopPrincipal
$existingMembers = @(Get-LocalGroupMemberSid -Name $remoteDesktopUsersGroup)
if ($existingMembers -notcontains $remoteDesktopPrincipalSid.Value) {
Add-LocalGroupMember -Group $remoteDesktopUsersGroup -Member $remoteDesktopPrincipalSid.Value
}
# Use Windows PowerShell so both the inbox and compatible remoting endpoints
@@ -97,7 +115,7 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesk
}
}
$rdpMembers = @(Get-LocalGroupMember -Group $remoteDesktopUsersGroup -ErrorAction SilentlyContinue)
$rdpMembers = @(Get-LocalGroupMemberSid -Name $remoteDesktopUsersGroup)
[pscustomobject]@{
ComputerName = $env:COMPUTERNAME
Domain = $computer.Domain
@@ -108,7 +126,7 @@ $rdpMembers = @(Get-LocalGroupMember -Group $remoteDesktopUsersGroup -ErrorActio
'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' `
-Name UserAuthentication) -eq 1
RemoteDesktopPrincipal = $RemoteDesktopPrincipal
PrincipalIsAuthorized = $rdpMembers.Name -contains $RemoteDesktopPrincipal
PrincipalIsAuthorized = $rdpMembers -contains $remoteDesktopPrincipalSid.Value
TermService = (Get-Service TermService).Status
WinRM = (Get-Service WinRM).Status
FirewallProfile = 'Domain'
+21 -4
View File
@@ -3,6 +3,22 @@ param()
$ErrorActionPreference = 'Stop'
function Get-LocalGroupMemberSid {
param([Parameter(Mandatory)][string]$Name)
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$Name,group")
foreach ($member in @($group.psbase.Invoke('Members'))) {
try {
$sidBytes = $member.GetType().InvokeMember('objectSid',
[Reflection.BindingFlags]::GetProperty, $null, $member, $null)
if ($sidBytes) {
([Security.Principal.SecurityIdentifier]::new([byte[]]$sidBytes, 0)).Value
}
}
catch { }
}
}
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
@@ -30,11 +46,12 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable SGU session auditing and
# NETWORK SERVICE. Resolve both principals by SID for localized Windows.
$eventLogReadersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-573')
$networkServiceSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-20')
$members = @(Get-LocalGroupMember -SID $eventLogReadersSid -ErrorAction SilentlyContinue)
$eventLogReadersGroup = ($eventLogReadersSid.Translate(
[Security.Principal.NTAccount]).Value -split '\\', 2)[1]
$members = @(Get-LocalGroupMemberSid -Name $eventLogReadersGroup)
$eventLogReaderMembershipChanged = $false
if ($members.SID.Value -notcontains $networkServiceSid.Value) {
$networkServiceAccount = $networkServiceSid.Translate([Security.Principal.NTAccount]).Value
Add-LocalGroupMember -SID $eventLogReadersSid -Member $networkServiceAccount
if ($members -notcontains $networkServiceSid.Value) {
Add-LocalGroupMember -SID $eventLogReadersSid -Member $networkServiceSid.Value
$eventLogReaderMembershipChanged = $true
}
+27 -1
View File
@@ -18,6 +18,7 @@ param(
[PSCredential]$DomainCredential,
[string]$DomainName = 'lci.lasalle.mx',
[string]$DomainNetbios = 'LCI',
[string]$DomainControllerDnsName,
[string]$ComputerOuDn = 'OU=Laboratorio,DC=lci,DC=lasalle,DC=mx',
[string]$NewComputerName,
[string]$NetworkInterfaceAlias = 'Ethernet',
@@ -57,6 +58,31 @@ $computer = Get-CimInstance Win32_ComputerSystem
if ($computer.PartOfDomain -and $computer.Domain -ne $DomainName) {
throw "The computer is already joined to the unexpected domain $($computer.Domain)."
}
$domainMembershipHealthy = $false
if ($computer.PartOfDomain) {
try {
$domainMembershipHealthy = [bool](Test-ComputerSecureChannel -ErrorAction Stop)
}
catch {
$domainMembershipHealthy = $false
}
}
if ($computer.PartOfDomain -and -not $domainMembershipHealthy) {
if (-not $DomainCredential) {
$DomainCredential = Get-Credential `
-UserName "$DomainNetbios\Administrator" `
-Message "Credential permitted to repair this computer in $DomainName"
}
$repairServer = if ($DomainControllerDnsName) { $DomainControllerDnsName } else { $DomainName }
Write-Warning "The computer names $DomainName but its secure channel is broken. Repairing it against $repairServer."
Reset-ComputerMachinePassword -Server $repairServer -Credential $DomainCredential -ErrorAction Stop
Restart-Service Netlogon -Force
Start-Sleep -Seconds 2
$domainMembershipHealthy = [bool](Test-ComputerSecureChannel -ErrorAction Stop)
if (-not $domainMembershipHealthy) {
throw "The secure channel to $DomainName remained invalid after repair."
}
}
$installParams = @{
PublishPath = $PublishPath
@@ -133,7 +159,7 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before jo
throw "Domain join refused because SGU enrollment is invalid: $($preJoin.Issues -join ' ')"
}
if ($computer.PartOfDomain) {
if ($computer.PartOfDomain -and $domainMembershipHealthy) {
& (Join-Path $PSScriptRoot 'Enable-LabRemoteAccess.ps1') `
-RemoteDesktopPrincipal $RemoteDesktopPrincipal `
-EnableAdministrativeFirewallGroups | Out-Null
+80 -3
View File
@@ -9,6 +9,7 @@ param(
[ValidateSet('GuestStatic', 'PlatformManaged')]
[string]$NetworkConfigurationMode = 'GuestStatic',
[string[]]$TrustedClientNetworks = @(),
[string[]]$PublicEnrollmentNetworks = @(),
[ipaddress[]]$DnsForwarders = @(),
[string]$DomainName = 'lci.lasalle.mx',
[string]$DomainNetbios = 'LCI',
@@ -133,6 +134,74 @@ function ConvertTo-PrivateNetworkCidr {
return ConvertTo-NetworkCidr -Address $address -NetworkPrefixLength $networkPrefixLength
}
function ConvertTo-PublicNetworkCidr {
param([Parameter(Mandatory)][string]$Cidr)
if ($Cidr -notmatch '^([^/]+)/(\d{1,2})$') {
throw "Public enrollment network '$Cidr' must use IPv4 CIDR notation, for example 203.0.113.0/24."
}
$address = $null
if (-not [ipaddress]::TryParse($Matches[1], [ref]$address) -or
$address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
throw "Public enrollment network '$Cidr' is not a valid IPv4 network."
}
$networkPrefixLength = [int]$Matches[2]
if ($networkPrefixLength -lt 1 -or $networkPrefixLength -gt 32) {
throw "Public enrollment network '$Cidr' has an invalid prefix length."
}
if (Test-PrivateIPv4Address -Address $address) {
throw "Public enrollment network '$Cidr' is private RFC1918 space. Use -TrustedClientNetworks for LAN or VPN ranges."
}
$bytes = $address.GetAddressBytes()
if ($bytes[0] -in @(0, 127) -or
($bytes[0] -eq 169 -and $bytes[1] -eq 254) -or
$bytes[0] -ge 224) {
throw "Public enrollment network '$Cidr' is not usable unicast IPv4 space."
}
return ConvertTo-NetworkCidr -Address $address -NetworkPrefixLength $networkPrefixLength
}
function Set-SguPublicEnrollmentFirewall {
param(
[Parameter(Mandatory)][ipaddress]$LocalAddress,
[Parameter(Mandatory)][string[]]$RemoteAddress
)
if ($RemoteAddress.Count -eq 0) { return }
$definitions = @(
@{ Name = 'SGU Public Enrollment TCP'; Protocol = 'TCP';
Port = @('53','88','135','389','443','445','464','636','3268','3269','5985','8443','21115-21117','49152-65535') },
@{ Name = 'SGU Public Enrollment UDP'; Protocol = 'UDP';
Port = @('53','88','123','389','464','21116') }
)
foreach ($definition in $definitions) {
$rule = Get-NetFirewallRule -DisplayName $definition.Name -ErrorAction SilentlyContinue
if (-not $rule) {
New-NetFirewallRule -DisplayName $definition.Name -Direction Inbound -Action Allow `
-Protocol $definition.Protocol -LocalPort $definition.Port `
-LocalAddress $LocalAddress.IPAddressToString -RemoteAddress $RemoteAddress `
-Profile Any | Out-Null
}
else {
$rule | Set-NetFirewallRule -Enabled True -Action Allow -Profile Any | Out-Null
$rule | Get-NetFirewallPortFilter | Set-NetFirewallPortFilter `
-Protocol $definition.Protocol -LocalPort $definition.Port | Out-Null
$rule | Get-NetFirewallAddressFilter | Set-NetFirewallAddressFilter `
-LocalAddress $LocalAddress.IPAddressToString -RemoteAddress $RemoteAddress | Out-Null
}
}
}
function Get-ActiveIPv4Adapters {
# Accelerated Networking exposes an Up VF without an IP stack. Configure
# the synthetic adapter that owns IPv4, never the underlying VF.
Get-NetAdapter | Where-Object {
$_.Status -eq 'Up' -and
(Get-NetIPInterface -InterfaceIndex $_.ifIndex -AddressFamily IPv4 `
-ErrorAction SilentlyContinue | Where-Object ConnectionState -eq 'Connected')
}
}
function Resolve-PrivateInterfaceAlias {
param([string]$RequestedAlias)
@@ -141,7 +210,7 @@ function Resolve-PrivateInterfaceAlias {
return $RequestedAlias
}
$upAdapters = @(Get-NetAdapter | Where-Object Status -eq 'Up')
$upAdapters = @(Get-ActiveIPv4Adapters)
$withoutGateway = @($upAdapters | Where-Object {
-not (Get-NetIPConfiguration -InterfaceIndex $_.ifIndex).IPv4DefaultGateway
})
@@ -397,6 +466,7 @@ if ($Resume -or (-not $ServerIPv4Address -and $existingState)) {
$DefaultGateway = if ($existingState.DefaultGateway) { [ipaddress][string]$existingState.DefaultGateway } else { $null }
$NetworkConfigurationMode = if ($existingState.NetworkConfigurationMode) { [string]$existingState.NetworkConfigurationMode } else { 'GuestStatic' }
$TrustedClientNetworks = if ($existingState.TrustedClientNetworks) { @($existingState.TrustedClientNetworks | ForEach-Object { [string]$_ }) } else { @() }
$PublicEnrollmentNetworks = if ($existingState.PublicEnrollmentNetworks) { @($existingState.PublicEnrollmentNetworks | ForEach-Object { [string]$_ }) } else { @() }
$DnsForwarders = @($existingState.DnsForwarders | ForEach-Object { [ipaddress][string]$_ })
$DomainName = [string]$existingState.DomainName
$DomainNetbios = [string]$existingState.DomainNetbios
@@ -417,7 +487,10 @@ $TrustedClientNetworks = @($TrustedClientNetworks |
ForEach-Object { ConvertTo-PrivateNetworkCidr -Cidr $_ } |
Where-Object { $_ -ne $domainSubnet } |
Select-Object -Unique)
$allowedRemoteAddresses = @($domainSubnet) + $TrustedClientNetworks
$PublicEnrollmentNetworks = @($PublicEnrollmentNetworks |
ForEach-Object { ConvertTo-PublicNetworkCidr -Cidr $_ } |
Select-Object -Unique)
$allowedRemoteAddresses = @($domainSubnet) + $TrustedClientNetworks + $PublicEnrollmentNetworks
$sourceRoot = $PSScriptRoot
if (-not $Resume) {
@@ -482,6 +555,7 @@ if (-not $existingState) {
DefaultGateway = if ($DefaultGateway) { $DefaultGateway.IPAddressToString } else { $null }
NetworkConfigurationMode = $NetworkConfigurationMode
TrustedClientNetworks = $TrustedClientNetworks
PublicEnrollmentNetworks = $PublicEnrollmentNetworks
DnsForwarders = @($DnsForwarders | ForEach-Object IPAddressToString)
DomainName = $DomainName
DomainNetbios = $DomainNetbios
@@ -560,7 +634,7 @@ Write-BootstrapLog 'Finalizing Active Directory, DNS, policies, broker, shares,
# Once the machine is a DC, every active adapter must query the local DNS
# service. Only the private domain adapter may publish its address in the AD
# zone; otherwise clients can receive the DHCP/NAT address of the Internet NIC.
Get-NetAdapter | Where-Object Status -eq 'Up' | ForEach-Object {
Get-ActiveIPv4Adapters | ForEach-Object {
Set-DnsClientServerAddress -InterfaceIndex $_.ifIndex `
-ServerAddresses $ServerIPv4Address.IPAddressToString
Set-DnsClient -InterfaceIndex $_.ifIndex `
@@ -708,6 +782,8 @@ foreach ($hostRecord in $hostRecords) {
& (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1') `
-AllowedRemoteAddress $allowedRemoteAddresses | Out-Null
Set-SguPublicEnrollmentFirewall -LocalAddress $ServerIPv4Address `
-RemoteAddress $PublicEnrollmentNetworks
$contentPath = Join-Path $bootstrapRoot 'payload\server-content\Packages'
if (Test-Path -LiteralPath $contentPath -PathType Container) {
@@ -796,6 +872,7 @@ $validation = [ordered]@{
ServerIPv4Address = $ServerIPv4Address.IPAddressToString
NetworkConfigurationMode = $NetworkConfigurationMode
TrustedClientNetworks = $TrustedClientNetworks
PublicEnrollmentNetworks = $PublicEnrollmentNetworks
AllowedRemoteAddresses = $allowedRemoteAddresses
BrokerDnsName = $brokerDnsName
BrokerCertificateThumbprint = $serverCertificate.Thumbprint
+1 -1
View File
@@ -100,7 +100,7 @@ try {
if ($Connect) {
& "$env:SystemRoot\System32\rasdial.exe" $ConnectionName
if ($LASTEXITCODE -ne 0) {
throw "Windows could not connect $ConnectionName. Verify UDP 500/4500 (IKEv2) or use the Azure-generated SSTP profile when the local network blocks IKEv2."
throw "Windows could not connect $ConnectionName. Verify UDP 500/4500 (IKEv2), or configure the Azure-generated OpenVPN profile in Azure VPN Client when the local network blocks IKEv2."
}
}
+241
View File
@@ -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)
+4 -3
View File
@@ -113,10 +113,11 @@ Bootstrap reproducible para el laboratorio SGU.
- `sgu-server-bootstrap-$Version.zip`: crea el bosque AD/DNS, OUs, grupo RDP, GPO, recurso `Packages`, broker mTLS y administración remota; se reanuda solo después del reinicio.
- `sgu-windows-client-bootstrap-$Version.zip`: paquete único para Windows 10 1607+ y Windows 11 x64 Pro, Enterprise o Education, con LAN, VPN existente y Azure P2S opcional.
- Doble clic en `Start-SguClientEnrollment.cmd`, IP del servidor y credenciales: descubre el dominio autenticado, comprueba las interfaces y rutas disponibles y conserva DHCP, las IP del cliente y el DNS de Internet. Ya no solicita una IP del cliente.
- Conserva seguridad mTLS, Credential Provider, cuenta estándar, RustDesk, supervisión y autorreparación. El servidor debe estar preparado con SGU y ser accesible por LAN o VPN.
- Si la IP del DC es pública, configura automáticamente DoH autenticado, confianza del certificado, NRPT y nombres del bosque antes de unir el equipo; funciona con cualquier interfaz que pueda alcanzar el servidor.
- Conserva seguridad mTLS, Credential Provider, cuenta estándar, RustDesk, supervisión y autorreparación. El servidor puede ser accesible por LAN, una VPN ya conectada o un CIDR público autorizado.
- `sgu-linux-client-bootstrap-$Version.zip`: une clientes Debian/Ubuntu o RHEL/Fedora/Rocky/AlmaLinux con realmd, Kerberos y SSSD. Solicita interactivamente la contraseña de unión y no instala el Credential Provider de Windows.
- `sgu-azure-infrastructure-$Version.zip`: despliega mediante Bicep una VM Windows Server 2025, red privada, IP pública protegida por NSG y Azure VPN Gateway P2S; también genera certificados por equipo y descarga el perfil de cliente.
- El bootstrap Azure conserva la IP privada administrada por la NIC de Azure, autoriza el pool P2S en los firewalls SGU y nunca publica LDAP, Kerberos, SMB, RPC, WinRM ni el Auth Broker directamente a Internet.
- `sgu-azure-infrastructure-$Version.zip`: despliega mediante Bicep una VM Windows Server 2025, red privada e IP pública protegida por NSG; Azure VPN Gateway P2S es opcional.
- El modo directo recibe una lista explícita de CIDR públicos, la replica en NSG y Windows Firewall y deja cerrados los puertos de enrolamiento cuando la lista está vacía.
- Windows 10 y 11 pueden instalar el perfil IKEv2 de todos los usuarios con certificado de máquina y DNS dividido del dominio; la disponibilidad antes del inicio de sesión depende del perfil y de las políticas del equipo.
- El Auth Broker clasifica sin tareas programadas cada cuenta autenticada: `AL` se agrega a `SGU-Alumnos`, `AD` a `SGU-Administrativos` y `DO` a `SGU-Docentes`; el bootstrap crea cada grupo dentro de la OU de su rol y migra idempotentemente cualquier grupo heredado sin cambiar su SID.
- El Auth Broker resuelve la dirección guardada de administrativos y docentes mediante `GetDireccion`, `GetLocalidadListado` y `GetColoniasListado`, evitando conservar los valores transitorios `Seleccione...` de los controles dinámicos de SGU.
@@ -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
}
+10 -1
View File
@@ -32,7 +32,16 @@ if (-not $before.IsValid) {
}
$computer = Get-CimInstance Win32_ComputerSystem
$domainReady = $false
if ($computer.PartOfDomain) {
try {
$domainReady = [bool](Test-ComputerSecureChannel -ErrorAction Stop)
}
catch {
$domainReady = $false
}
}
if ($domainReady) {
& $remoteAccessScript `
-RemoteDesktopPrincipal ([string]$configuration.RemoteDesktopPrincipal) `
-EnableAdministrativeFirewallGroups | Out-Null
@@ -45,7 +54,7 @@ if ($configuration.RustDeskServerAddress -and $configuration.RustDeskServerPubli
}
$verificationParams = @{}
if ($computer.PartOfDomain) {
if ($domainReady) {
$verificationParams.RequireDomainJoined = $true
$verificationParams.RequireRemoteAccess = $true
$verificationParams.RemoteDesktopPrincipal = [string]$configuration.RemoteDesktopPrincipal
+35 -10
View File
@@ -15,6 +15,29 @@ function Get-LocalUserFlags {
return [int]$directoryEntry.InvokeGet('UserFlags')
}
function Get-LocalGroupMemberSid {
param([Parameter(Mandatory)][string]$Name)
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$Name,group")
foreach ($member in @($group.psbase.Invoke('Members'))) {
try {
$sidBytes = $member.GetType().InvokeMember(
'objectSid',
[Reflection.BindingFlags]::GetProperty,
$null,
$member,
$null)
if ($sidBytes) {
([Security.Principal.SecurityIdentifier]::new([byte[]]$sidBytes, 0)).Value
}
}
catch {
# An orphaned domain SID can no longer resolve after a forest is
# rebuilt. Other members must remain inspectable and unchanged.
}
}
}
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
@@ -64,14 +87,16 @@ try {
$usersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-545')
$administratorsGroup = Get-LocalGroup -SID $administratorsSid -ErrorAction Stop
$usersGroup = Get-LocalGroup -SID $usersSid -ErrorAction Stop
$administratorMembers = @(Get-LocalGroupMember -Group $administratorsGroup -ErrorAction Stop)
if ($administratorMembers.SID.Value -contains $user.SID.Value) {
Remove-LocalGroupMember -Group $administratorsGroup -Member $user -Confirm:$false
$administratorMembers = @(Get-LocalGroupMemberSid -Name $administratorsGroup.Name)
if ($administratorMembers -contains $user.SID.Value) {
([ADSI]("WinNT://$env:COMPUTERNAME/$($administratorsGroup.Name),group")).Remove(
"WinNT://$env:COMPUTERNAME/$userName,user")
}
$standardMembers = @(Get-LocalGroupMember -Group $usersGroup -ErrorAction Stop)
if ($standardMembers.SID.Value -notcontains $user.SID.Value) {
Add-LocalGroupMember -Group $usersGroup -Member $user
$standardMembers = @(Get-LocalGroupMemberSid -Name $usersGroup.Name)
if ($standardMembers -notcontains $user.SID.Value) {
([ADSI]("WinNT://$env:COMPUTERNAME/$($usersGroup.Name),group")).Add(
"WinNT://$env:COMPUTERNAME/$userName,user")
}
}
finally {
@@ -85,12 +110,12 @@ $verifiedAdministratorsGroup = Get-LocalGroup `
$verifiedUsersGroup = Get-LocalGroup `
-SID ([Security.Principal.SecurityIdentifier]::new('S-1-5-32-545')) `
-ErrorAction Stop
$verifiedAdministrators = @(Get-LocalGroupMember -Group $verifiedAdministratorsGroup -ErrorAction Stop)
$verifiedUsers = @(Get-LocalGroupMember -Group $verifiedUsersGroup -ErrorAction Stop)
if (@($verifiedAdministrators).SID.Value -contains $verifiedUser.SID.Value) {
$verifiedAdministrators = @(Get-LocalGroupMemberSid -Name $verifiedAdministratorsGroup.Name)
$verifiedUsers = @(Get-LocalGroupMemberSid -Name $verifiedUsersGroup.Name)
if ($verifiedAdministrators -contains $verifiedUser.SID.Value) {
throw "The local account '$userName' still belongs to the local Administrators group."
}
if ($verifiedUsers.SID.Value -notcontains $verifiedUser.SID.Value) {
if ($verifiedUsers -notcontains $verifiedUser.SID.Value) {
throw "The local account '$userName' does not belong to the local Users group."
}
$verifiedPasswordNeverExpires =
+3 -1
View File
@@ -316,7 +316,9 @@ if (-not $PSBoundParameters.ContainsKey('Location') -and $metadata) {
$Location = $metadata.Location
}
$genderWasProvided = $PSBoundParameters.ContainsKey('Gender')
if (-not $genderWasProvided -and $metadata) {
if (-not $genderWasProvided -and $metadata -and $metadata.Gender -in @('Male', 'Female')) {
# The parameter's ValidateSet also runs on assignments. Missing AD gender
# must leave the optional parameter unset so the neutral wording can render.
$Gender = $metadata.Gender
}
$welcomeHeading = Get-WelcomeHeading -Gender $Gender
+34 -6
View File
@@ -22,6 +22,28 @@ $issues = [Collections.Generic.List[string]]::new()
$standardLocalUserName = 'alumno'
$passwordNeverExpiresFlag = 0x10000
function Get-LocalGroupMemberSid {
param([Parameter(Mandatory)][string]$Name)
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$Name,group")
foreach ($member in @($group.psbase.Invoke('Members'))) {
try {
$sidBytes = $member.GetType().InvokeMember(
'objectSid',
[Reflection.BindingFlags]::GetProperty,
$null,
$member,
$null)
if ($sidBytes) {
([Security.Principal.SecurityIdentifier]::new([byte[]]$sidBytes, 0)).Value
}
}
catch {
# Keep validating known members when an old forest SID no longer resolves.
}
}
}
$computer = Get-CimInstance Win32_ComputerSystem
if ($RequireDomainJoined -and -not $computer.PartOfDomain) {
$issues.Add('The computer is not joined to a domain.')
@@ -100,12 +122,12 @@ if ($standardLocalUserPresent) {
$usersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-545')
$administratorsGroup = Get-LocalGroup -SID $administratorsSid -ErrorAction Stop
$usersGroup = Get-LocalGroup -SID $usersSid -ErrorAction Stop
$administratorMembers = @(Get-LocalGroupMember -Group $administratorsGroup -ErrorAction Stop)
$standardMembers = @(Get-LocalGroupMember -Group $usersGroup -ErrorAction Stop)
$administratorMembers = @(Get-LocalGroupMemberSid -Name $administratorsGroup.Name)
$standardMembers = @(Get-LocalGroupMemberSid -Name $usersGroup.Name)
$standardLocalUserIsAdministrator =
$administratorMembers.SID.Value -contains $standardLocalUser.SID.Value
$administratorMembers -contains $standardLocalUser.SID.Value
$standardLocalUserInUsersGroup =
$standardMembers.SID.Value -contains $standardLocalUser.SID.Value
$standardMembers -contains $standardLocalUser.SID.Value
try {
$directoryEntry = [ADSI]("WinNT://$env:COMPUTERNAME/$standardLocalUserName,user")
$userFlags = [int]$directoryEntry.InvokeGet('UserFlags')
@@ -195,12 +217,18 @@ $remoteAccessReady = $null
if ($RequireRemoteAccess) {
$remoteDesktopUsersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-555')
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
$rdpMembers = @(Get-LocalGroupMember -Group $remoteDesktopUsersGroup -ErrorAction SilentlyContinue)
$rdpMembers = @(Get-LocalGroupMemberSid -Name $remoteDesktopUsersGroup)
$remoteDesktopPrincipalSid = $null
try {
$remoteDesktopPrincipalSid = ([Security.Principal.NTAccount]::new($RemoteDesktopPrincipal)).Translate(
[Security.Principal.SecurityIdentifier]).Value
}
catch { }
$remoteAccessReady =
(Get-Service TermService).Status -eq 'Running' -and
(Get-Service WinRM).Status -eq 'Running' -and
(Get-ItemPropertyValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections) -eq 0 -and
$rdpMembers.Name -contains $RemoteDesktopPrincipal
$remoteDesktopPrincipalSid -and $rdpMembers -contains $remoteDesktopPrincipalSid
if (-not $remoteAccessReady) {
$issues.Add('RDP/WinRM or the authorized domain group is not fully configured.')
}