commit
5fa02f29d7
@ -0,0 +1,5 @@ |
||||
@echo off |
||||
pushd %~dp0 |
||||
powershell -ExecutionPolicy Bypass -File .\Setup-PSRemoting.ps1 |
||||
popd |
||||
@pause |
@ -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 |
Loading…
Reference in new issue