You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
536 lines
17 KiB
536 lines
17 KiB
2 years ago
|
#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
|