commit 5fa02f29d7298b82c1c027ad159b1112adb6f74b Author: dave Date: Wed Nov 23 15:08:22 2022 +0300 init diff --git a/Setup-PSRemoting.cmd b/Setup-PSRemoting.cmd new file mode 100644 index 0000000..3b029e7 --- /dev/null +++ b/Setup-PSRemoting.cmd @@ -0,0 +1,5 @@ +@echo off +pushd %~dp0 +powershell -ExecutionPolicy Bypass -File .\Setup-PSRemoting.ps1 +popd +@pause \ No newline at end of file diff --git a/Setup-PSRemoting.ps1 b/Setup-PSRemoting.ps1 new file mode 100644 index 0000000..e4b560b --- /dev/null +++ b/Setup-PSRemoting.ps1 @@ -0,0 +1,530 @@ +#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 = "remote-admin", + + # Default password for this account + [String]$ServicePassword = "bootstrap123" +) + +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 . + + $Interfaces = $Interfaces | ? { $_.DNSDomain -match ".*corp.monroe.fitness$" } + 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 "10.0.0.0/10" ` + -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 "10.0.0.0/255.192.0.0") { + Change "Changing HTTP rule remote address" + $Rule | Set-NetFirewallRule -RemoteAddress "10.0.0.0/10" -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 \ No newline at end of file