#Requires -RunAsAdministrator #Requires -Version 5 #Requires -Modules Microsoft.PowerShell.LocalAccounts [CmdletBinding()] Param ( # Whether or not to enable debugging messages [bool]$EnableDebug = $true, # Username for remote administration account [String]$ServiceUser = "{{ winrm_remote_user }}", # Default password for this account [String]$ServicePassword = "{{ winrm_bootstrap_password }}" ) Set-StrictMode -Version 2 # don't force v3 [bool]$HasSSL = $false $ServiceUserDescription = "Service user for remote administration" function Log($Message, [String]$Color = $null, $NoNewline = $false) { if ($Color) { $ExtraParms = @{"ForegroundColor" = $Color} } else { $ExtraParms = @{} } Write-Host $Message @ExtraParms -NoNewline:$NoNewline } function Debug($Message) { if ($EnableDebug) { Log $Message -Color Cyan } } function Change($Message) { Log "! $Message" -Color Yellow } function Error($Message) { Log "ERROR: $Message" -Color Red Exit } function Assert($Condition, $Message) { if (!$Condition) { Error -Message $Message } } function Count($E) { return ($E | Measure).Count } function New-Credential($User, $Password) { return New-Object System.Management.Automation.PSCredential($User, (ConvertTo-SecureString $Password -AsPlainText -Force)) } # Checks service account for compliance function Verify-LocalUser($User) { $Name = $User.Name Debug "Verifying account `"$Name`"" # Is it enabled? if (!$User.Enabled) { Change "Enabling account `"$Name`"" $User | Enable-LocalUser -ErrorAction Stop } # Check if account is set to never expire if ($User.AccountExpires -ne $null) { Change "Changing account expiration policy for `"$Name`"" $User | Set-LocalUser -AccountNeverExpires -ErrorAction Stop } # Do the same with its password if ($User.PasswordExpires -ne $null) { Change "Changing password expiration policy for `"$Name`"" $User | Set-LocalUser -PasswordNeverExpires -ErrorAction Stop } # Account description is not really important but it doesn't hurt to check if ($User.Description -ne $ServiceUserDescription) { Change "Changing description for `"$Name`"" $User | Set-LocalUser -Description $ServiceUserDescription -ErrorAction Stop } # Validate group membership for Administrators group if ((Get-LocalGroupMember -SID S-1-5-32-544 | select -ExpandProperty SID) -notcontains $User.SID.Value) { Change "Changing group membership for `"$Name`" - adding account to `"Administrators`" local group" Add-LocalGroupMember -SID S-1-5-32-544 -Member $User -ErrorAction Stop } # Do the same for RMS group, if it exists if ((Get-LocalGroupMember -SID S-1-5-32-580 | select -ExpandProperty SID) -notcontains $User.SID.Value) { if (Get-LocalGroup -SID S-1-5-32-580 -ErrorAction SilentlyContinue) { Change "Changing group membership for `"$Name`" - adding account to `"Remote Management Users`" local group" Add-LocalGroupMember -SID S-1-5-32-580 -Member $User -ErrorAction Stop } } Debug "Verification complete" } # PROCESS: # Find, create, edit group membership of service account and validate it # DESIRED STATE: an active service account on local machine function Process-ServiceAccount { Debug "* Processing: Service account" # Verify if there is already an user $User = Get-LocalUser -Name $ServiceUser -ErrorAction SilentlyContinue if ($User) { Log "Found existing service account: `"$ServiceUser`"" } else { # Create new user and verify it Debug "No service account found, will create one" # Handle passwordless user (may be useful for pure cert auth, not really the case now) if ($ServicePassword -eq $null -or $ServicePassword.Length -eq 0) { $PasswordSplat = @{ "NoPassword" = $true } Log "Using passwordless login" } else { $PasswordSplat = @{ "Password" = ConvertTo-SecureString $ServicePassword -AsPlainText -Force "PasswordNeverExpires" = $true } } # Create an user try { $User = New-LocalUser -Name $ServiceUser ` -Description $ServiceUserDescription ` -AccountNeverExpires ` @PasswordSplat } catch { Error "Caught an exception while creating service account `"$ServiceUser`"" } Assert $User "Failed to create service account `"$ServiceUser`"" Change "Created service account `"$ServiceUser`"" # Add this user to Administrators local group try { Add-LocalGroupMember -SID S-1-5-32-544 -Member $User -ErrorAction Stop } catch { Error "Caught an exception while adding service account `"$ServiceUser`" to local group `"Administrators`"" } Change "Added account `"$ServiceUser`" to local group `"Administrators`"" # Check if RMU group exists if (!(Get-LocalGroup -SID S-1-5-32-580 -ErrorAction SilentlyContinue)) { Log "`"Remote Management Users`" group is missing from this system - will not add user to this group" -Color Yellow } else { # Also add user to RMU local group try { Add-LocalGroupMember -SID S-1-5-32-580 -Member $User -ErrorAction Stop } catch { Error "Caught an exception while adding service account `"$ServiceUser`" to local group `"Remote Management Users`"" } Change "Added account `"$ServiceUser`" to local group `"Remote Management Users`"" } } # Verify user, no matter whether it was found or created Verify-LocalUser -User $User } # PROCESS: # Ensure that local network connection profile category is set to Private, so firewall rules and Enable-PSRemoting should work correctly # Network interface are selected by their DNS suffix # DESIRED STATE: category of local network interface is set to Private function Process-NetworkProfile { Debug "* Processing: Network Profiles" $Interfaces = gwmi -Class Win32_NetworkAdapterConfiguration -Filter IPEnabled=TRUE -ComputerName . {% if old_corp_tld is defined -%} # workaround for old corp tld $Interfaces = $Interfaces | ? { ($_.DNSDomain -match ".*{{ int_tld }}$") -or ($_.DNSDomain -match ".*{{ old_int_tld }}$") } {%- else -%} $Interfaces = $Interfaces | ? { $_.DNSDomain -match ".*{{ int_tld }}$" } {%- endif %} if (!$Interfaces) { # early return if there is no compatible network interface Log -Color Yellow "Failed to find local network interface with corp DNS suffix - skipping network profile check" } else { Debug "Found $(Count $Interfaces) compatible network interfaces" $Interfaces.InterfaceIndex | % { if ((Get-NetConnectionProfile -InterfaceIndex $_).NetworkCategory -eq "Public") { Change "Setting network category of interface #$_ to Private" try { Set-NetConnectionProfile -InterfaceIndex $_ -NetworkCategory "Private" -ErrorAction Stop } catch { Error "Caught an exception when setting network profile category" } } } } } # PROCESS: # Start up WinRM service and ensure that it has automatic start type # DESIRED STATE: WinRM service is running and is set to auto-start on next boot function Process-WinRM { Debug "* Processing: WinRM" try { $Service = Get-Service -Name "WinRM" Assert $Service "WinRM service does not exist on this machine" if ($Service.StartType -ne "Automatic") { Change "Setting WinRM startup type to Automatic" $Service | Set-Service -StartupType Automatic -ErrorAction Stop } if ($Service.Status -in "Stopped", "StopPending") { Change "Starting WinRM service" $Service | Start-Service -ErrorAction Stop } } catch { Error "Caught an exception while setting up WinRM service" } } # PROCESS: # Set LocalAccountTokenFilterPolicy registry value to 1 # DESIRED STATE: LocalAccountTokenFilterPolicy = 1 function Process-Registry { Debug "* Processing: Registry" $Key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" $Name = "LocalAccountTokenFilterPolicy" $Value = 1 $Prop = Get-ItemProperty $Key -ErrorAction SilentlyContinue if (!$Prop) { Error "Parent registry key for $Name does not exist, skipping this step" } else { $M = Get-Member -InputObject $Prop -name $Name -Membertype Properties -ErrorAction SilentlyContinue if (!$M -or $Prop.$Name -ne $Value) { Log "$Name is set to an incorrect value or is empty" Remove-ItemProperty $Key -Name $Name -Force -ErrorAction SilentlyContinue Change "Removed $Name from $Key" Assert ((New-ItemProperty $Key -Name $Name -PropertyType "DWord" -Value $Value).$Name -eq $Value) "Failed to create `"$Name`" registry property" Change "Added $Name to $Key with value $Value" } } } # PROCESS: # Enable PS Remoting # DESIRED STATE: there is at least one session configuration and a WSMan listener (their validity will be checked later) function Process-PSRemoting { Debug "* Processing: PS Remoting" # This snippet was mostly taken from Ansible script # TODO: most of remoting stuff is already taken care of - this function is just a failsafe if (!(Get-PSSessionConfiguration -Verbose:$false) -or !(Get-ChildItem WSMan:\localhost\Listener)) { Log "No PS session configuration or listener found - enabling PS remoting" # Override local verbose preference $Pref = $VerbosePreference $VerbosePreference = "SilentlyContinue" try { Enable-PSRemoting -Force -ErrorAction Stop -Verbose:$false > $null } catch { Error "Caught an exception in Enable-PSRemoting" } Change "PS Remoting enabled" # Set verbose preference back to its original value $VerbosePreference = $Pref } } # Create a HTTP listener function Create-HTTPListener { New-Item 'WSMan:\localhost\Listener' -Transport HTTP -Address "*" -Force > $null Change "Created HTTP listener" } function Verify-Listener($Listener) { return ($Listener -and ` ($Listener.Keys -contains "Transport=HTTP" -or $Listener.Keys -contains "Transport=HTTPS") -and ` $Listener.Keys -contains "Address=*") } # PROCESS: # Loop through PS listeners and ensure there's only one active HTTP listener # (this ignores HTTPS listeners because they will be set up by Ansible later) # DESIRED STATE: there is an active HTTP listener function Process-Listeners { Debug "* Processing: WSMan listeners" # Find valid listeners and also save all listeners $All = Get-ChildItem WSMan:\localhost\Listener $Valid = $All | ? {Verify-Listener -Listener $_} if ((Count $All) -eq 1 -and (Count $Valid) -eq 1) { Log "Found 1 listener: `"$($Valid[0].Name)`"" # only one: either http (ok) or https (also ok) } else { # Remove all listeners $All | % { Change "Removing listener: `"$($_.Name)`"" try { $_ | Remove-Item -Force -Recurse } catch { # Continue even if an exception has happened Log -Color Yellow "Caught an exception while removing listener `"$($_.Name)`"" } } # Create Create-HTTPListener # Verify after creation $Valid = Get-ChildItem WSMan:\localhost\Listener | ? {Verify-Listener -Listener $_} Assert ((Count $Valid) -eq 1) "Listener was just created, but it's missing" Debug "Found listener after creation" } $Listener = $Valid[0] } # PURPOSE: # Search, validate and fix PS session configurations # This also sets up a more secure SDDL # DESIRED STATE: PS session configurations are validated and are now correct function Process-SessionConfig { Debug "* Processing: PS Session Configuration" $SDDL = "O:NSG:BAD:P(A;;GA;;;RM)(A;;GA;;;IU)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)" Get-PSSessionConfiguration | ? {$_.Name -eq "microsoft.powershell" -or $_.Name -eq "microsoft.powershell32"} | % { if ($_.SecurityDescriptorSddl -ne $SDDL) { Change "Changing SDDL on session configuration `"$($_.Name)`"" try { ($_ | Set-PSSessionConfiguration -SecurityDescriptorSddl $SDDL) > $null } catch { Log -Color Yellow "Caught an exception while changing SDDL on `"$($_.Name)`"" } } } } function Process-PSRAuth { Debug "* Processing: PS Remoting Authentication" $Auth = Get-ChildItem WSMan:\localhost\Service\Auth "Basic","Kerberos","Certificate" | % { if (($Auth | ? Name -eq $_).Value -eq $true) { Change "Disabling $_ authentication" Set-Item -Path "WSMan:\localhost\Service\Auth\$_" -Value $false } } if (($Auth | ? Name -eq "CredSSP").Value -eq $false) { Change "Enabling CredSSP authentication" Enable-WSManCredSSP -Role Server -Force > $null } } function Process-Firewall { Debug "* Processing: PS Remoting Firewall" if (Get-NetFirewallRule -Name "WINRM-HTTPS-In-TCP" -ErrorAction SilentlyContinue) { Debug "Found HTTPS rule, will disable HTTP rules" $Script:HasSSL = $true Get-NetFirewallRule -ErrorAction SilentlyContinue | ? {$_.Name -like "WINRM-HTTP-*" -and $_.Enabled -eq $true} | % { Change "Disabling firewall rule for PSR over HTTP: $($_.DisplayName)" Disable-NetFirewallRule -Name $_.Name } } else { Debug "HTTPS rule is missing, will add HTTP rule" $Script:HasSSL = $false if (!(Get-NetFirewallRule -Name "WINRM-HTTP-In-TCP" -ErrorAction SilentlyContinue)) { Change "Adding firewall rule for PSR over HTTP" New-NetFirewallRule -Name "WINRM-HTTP-In-TCP" ` -DisplayName "Windows Remote Management (HTTP-In)" ` -Description "Inbound rule for Windows Remote Management via WS-Management. [TCP 5985]" ` -Group "Windows Remote Management" ` -Program "System" ` -Protocol TCP ` -LocalPort "5985" ` -RemoteAddress "{{ int_net }}" ` -Action Allow ` -Profile Domain,Private > $null } $Rule = Get-NetFirewallRule -Name "WINRM-HTTP-In-TCP" -ErrorAction SilentlyContinue if (!$Rule) { Error "HTTP rule is missing after its creation" } if ($Rule.Enabled -eq $false) { Change "Enabling HTTP rule" $Rule | Enable-NetFirewallRule -ErrorAction Stop } if (($Rule | Get-NetFirewallAddressFilter).RemoteAddress -ne "{{ int_net | ipaddr('network') }}/{{ int_net | ipaddr('netmask') }}") { Change "Changing HTTP rule remote address" $Rule | Set-NetFirewallRule -RemoteAddress "{{ int_net }}" -ErrorAction Stop } if ($Rule.Profile -ne "Domain,Private") { Change "Changing HTTP rule profile" $Rule | Set-NetFirewallRule -Profile Domain,Private -ErrorAction Stop } } } function Test-PSR { Debug "* Processing: PS Remoting Test" try { if ($Script:HasSSL) { Debug "Creating PS session through HTTPS" $Session = New-PSSession -UseSSL -ComputerName "localhost" -SessionOption (New-PSSessionOption -SkipRevocationCheck -SkipCNCheck) -Credential (New-Credential -User $ServiceUser -Password $ServicePassword) } else { Debug "Creating PS session through HTTP" $Session = New-PSSession -ComputerName "localhost" -Credential (New-Credential -User $ServiceUser -Password $ServicePassword) } } catch { Error "Caught an exception while setting up PS session: $_" } Assert $Session "Failed to initiate local PS Remoting session" Assert ((Invoke-Command -Session $Session -ScriptBlock {Write-Output "test"}) -eq "test") "Received wrong output from local PS remoting session" $Session | Remove-PSSession } Log "PS remoting preparation script" -Color Green Log "Debug mode is $(("off","on")[$EnableDebug])" Process-ServiceAccount -Name $ServiceUser -Password $ServicePassword Process-NetworkProfile Process-Registry Process-WinRM Process-PSRemoting Process-Listeners Process-SessionConfig Process-PSRAuth Process-Firewall Process-WinRM Test-PSR Log "Completed" -Color Green