1 #Requires -Version 3.0
2 
3 # Configure a Windows host for remote management with Ansible
4 # -----------------------------------------------------------
5 #
6 # This script checks the current WinRM (PS Remoting) configuration and makes
7 # the necessary changes to allow Ansible to connect, authenticate and
8 # execute PowerShell commands.
9 #
10 # All events are logged to the Windows EventLog, useful for unattended runs.
11 #
12 # Use option -Verbose in order to see the verbose output messages.
13 #
14 # Use option -CertValidityDays to specify how long this certificate is valid
15 # starting from today. So you would specify -CertValidityDays 3650 to get
16 # a 10-year valid certificate.
17 #
18 # Use option -ForceNewSSLCert if the system has been SysPreped and a new
19 # SSL Certificate must be forced on the WinRM Listener when re-running this
20 # script. This is necessary when a new SID and CN name is created.
21 #
22 # Use option -EnableCredSSP to enable CredSSP as an authentication option.
23 #
24 # Use option -DisableBasicAuth to disable basic authentication.
25 #
26 # Use option -SkipNetworkProfileCheck to skip the network profile check.
27 # Without specifying this the script will only run if the device's interfaces
28 # are in DOMAIN or PRIVATE zones.  Provide this switch if you want to enable
29 # WinRM on a device with an interface in PUBLIC zone.
30 #
31 # Use option -SubjectName to specify the CN name of the certificate. This
32 # defaults to the system's hostname and generally should not be specified.
33 
34 # Written by Trond Hindenes <trond@hindenes.com>
35 # Updated by Chris Church <cchurch@ansible.com>
36 # Updated by Michael Crilly <mike@autologic.cm>
37 # Updated by Anton Ouzounov <Anton.Ouzounov@careerbuilder.com>
38 # Updated by Nicolas Simond <contact@nicolas-simond.com>
39 # Updated by Dag Wieërs <dag@wieers.com>
40 # Updated by Jordan Borean <jborean93@gmail.com>
41 # Updated by Erwan Quélin <erwan.quelin@gmail.com>
42 # Updated by David Norman <david@dkn.email>
43 #
44 # Version 1.0 - 2014-07-06
45 # Version 1.1 - 2014-11-11
46 # Version 1.2 - 2015-05-15
47 # Version 1.3 - 2016-04-04
48 # Version 1.4 - 2017-01-05
49 # Version 1.5 - 2017-02-09
50 # Version 1.6 - 2017-04-18
51 # Version 1.7 - 2017-11-23
52 # Version 1.8 - 2018-02-23
53 # Version 1.9 - 2018-09-21
54 
55 # Support -Verbose option
56 [CmdletBinding()]
57 
58 Param (
59     [string]$SubjectName = $env:COMPUTERNAME,
60     [int]$CertValidityDays = 1095,
61     [switch]$SkipNetworkProfileCheck,
62     $CreateSelfSignedCert = $true,
63     [switch]$ForceNewSSLCert,
64     [switch]$GlobalHttpFirewallAccess,
65     [switch]$DisableBasicAuth = $false,
66     [switch]$EnableCredSSP
67 )
68 
Write-Lognull69 Function Write-Log
70 {
71     $Message = $args[0]
72     Write-EventLog -LogName Application -Source $EventSource -EntryType Information -EventId 1 -Message $Message
73 }
74 
Write-VerboseLognull75 Function Write-VerboseLog
76 {
77     $Message = $args[0]
78     Write-Verbose $Message
79     Write-Log $Message
80 }
81 
Write-HostLog()82 Function Write-HostLog
83 {
84     $Message = $args[0]
85     Write-Output $Message
86     Write-Log $Message
87 }
88 
New-LegacySelfSignedCertnull89 Function New-LegacySelfSignedCert
90 {
91     Param (
92         [string]$SubjectName,
93         [int]$ValidDays = 1095
94     )
95 
96     $hostnonFQDN = $env:computerName
97     $hostFQDN = [System.Net.Dns]::GetHostByName(($env:computerName)).Hostname
98     $SignatureAlgorithm = "SHA256"
99 
100     $name = New-Object -COM "X509Enrollment.CX500DistinguishedName.1"
101     $name.Encode("CN=$SubjectName", 0)
102 
103     $key = New-Object -COM "X509Enrollment.CX509PrivateKey.1"
104     $key.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
105     $key.KeySpec = 1
106     $key.Length = 4096
107     $key.SecurityDescriptor = "D:PAI(A;;0xd01f01ff;;;SY)(A;;0xd01f01ff;;;BA)(A;;0x80120089;;;NS)"
108     $key.MachineContext = 1
109     $key.Create()
110 
111     $serverauthoid = New-Object -COM "X509Enrollment.CObjectId.1"
112     $serverauthoid.InitializeFromValue("1.3.6.1.5.5.7.3.1")
113     $ekuoids = New-Object -COM "X509Enrollment.CObjectIds.1"
114     $ekuoids.Add($serverauthoid)
115     $ekuext = New-Object -COM "X509Enrollment.CX509ExtensionEnhancedKeyUsage.1"
116     $ekuext.InitializeEncode($ekuoids)
117 
118     $cert = New-Object -COM "X509Enrollment.CX509CertificateRequestCertificate.1"
119     $cert.InitializeFromPrivateKey(2, $key, "")
120     $cert.Subject = $name
121     $cert.Issuer = $cert.Subject
122     $cert.NotBefore = (Get-Date).AddDays(-1)
123     $cert.NotAfter = $cert.NotBefore.AddDays($ValidDays)
124 
125     $SigOID = New-Object -ComObject X509Enrollment.CObjectId
126     $SigOID.InitializeFromValue(([Security.Cryptography.Oid]$SignatureAlgorithm).Value)
127 
128     [string[]] $AlternativeName  += $hostnonFQDN
129     $AlternativeName += $hostFQDN
130     $IAlternativeNames = New-Object -ComObject X509Enrollment.CAlternativeNames
131 
132     foreach ($AN in $AlternativeName)
133     {
134         $AltName = New-Object -ComObject X509Enrollment.CAlternativeName
135         $AltName.InitializeFromString(0x3,$AN)
136         $IAlternativeNames.Add($AltName)
137     }
138 
139     $SubjectAlternativeName = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames
140     $SubjectAlternativeName.InitializeEncode($IAlternativeNames)
141 
142     [String[]]$KeyUsage = ("DigitalSignature", "KeyEncipherment")
143     $KeyUsageObj = New-Object -ComObject X509Enrollment.CX509ExtensionKeyUsage
144     $KeyUsageObj.InitializeEncode([int][Security.Cryptography.X509Certificates.X509KeyUsageFlags]($KeyUsage))
145     $KeyUsageObj.Critical = $true
146 
147     $cert.X509Extensions.Add($KeyUsageObj)
148     $cert.X509Extensions.Add($ekuext)
149     $cert.SignatureInformation.HashAlgorithm = $SigOID
150     $CERT.X509Extensions.Add($SubjectAlternativeName)
151     $cert.Encode()
152 
153     $enrollment = New-Object -COM "X509Enrollment.CX509Enrollment.1"
154     $enrollment.InitializeFromRequest($cert)
155     $certdata = $enrollment.CreateRequest(0)
156     $enrollment.InstallResponse(2, $certdata, 0, "")
157 
158     # extract/return the thumbprint from the generated cert
159     $parsed_cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
160     $parsed_cert.Import([System.Text.Encoding]::UTF8.GetBytes($certdata))
161 
162     return $parsed_cert.Thumbprint
163 }
164 
Enable-GlobalHttpFirewallAccessnull165 Function Enable-GlobalHttpFirewallAccess
166 {
167     Write-Verbose "Forcing global HTTP firewall access"
168     # this is a fairly naive implementation; could be more sophisticated about rule matching/collapsing
169     $fw = New-Object -ComObject HNetCfg.FWPolicy2
170 
171     # try to find/enable the default rule first
172     $add_rule = $false
173     $matching_rules = $fw.Rules | Where-Object  { $_.Name -eq "Windows Remote Management (HTTP-In)" }
174     $rule = $null
175     If ($matching_rules) {
176         If ($matching_rules -isnot [Array]) {
177             Write-Verbose "Editing existing single HTTP firewall rule"
178             $rule = $matching_rules
179         }
180         Else {
181             # try to find one with the All or Public profile first
182             Write-Verbose "Found multiple existing HTTP firewall rules..."
183             $rule = $matching_rules | ForEach-Object { $_.Profiles -band 4 }[0]
184 
185             If (-not $rule -or $rule -is [Array]) {
186                 Write-Verbose "Editing an arbitrary single HTTP firewall rule (multiple existed)"
187                 # oh well, just pick the first one
188                 $rule = $matching_rules[0]
189             }
190         }
191     }
192 
193     If (-not $rule) {
194         Write-Verbose "Creating a new HTTP firewall rule"
195         $rule = New-Object -ComObject HNetCfg.FWRule
196         $rule.Name = "Windows Remote Management (HTTP-In)"
197         $rule.Description = "Inbound rule for Windows Remote Management via WS-Management. [TCP 5985]"
198         $add_rule = $true
199     }
200 
201     $rule.Profiles = 0x7FFFFFFF
202     $rule.Protocol = 6
203     $rule.LocalPorts = 5985
204     $rule.RemotePorts = "*"
205     $rule.LocalAddresses = "*"
206     $rule.RemoteAddresses = "*"
207     $rule.Enabled = $true
208     $rule.Direction = 1
209     $rule.Action = 1
210     $rule.Grouping = "Windows Remote Management"
211 
212     If ($add_rule) {
213         $fw.Rules.Add($rule)
214     }
215 
216     Write-Verbose "HTTP firewall rule $($rule.Name) updated"
217 }
218 
219 # Setup error handling.
220 Trap
221 {
222     $_
223     Exit 1
224 }
225 $ErrorActionPreference = "Stop"
226 
227 # Get the ID and security principal of the current user account
228 $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
229 $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)
230 
231 # Get the security principal for the Administrator role
232 $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
233 
234 # Check to see if we are currently running "as Administrator"
235 if (-Not $myWindowsPrincipal.IsInRole($adminRole))
236 {
237     Write-Output "ERROR: You need elevated Administrator privileges in order to run this script."
238     Write-Output "       Start Windows PowerShell by using the Run as Administrator option."
239     Exit 2
240 }
241 
242 $EventSource = $MyInvocation.MyCommand.Name
243 If (-Not $EventSource)
244 {
245     $EventSource = "Powershell CLI"
246 }
247 
248 If ([System.Diagnostics.EventLog]::Exists('Application') -eq $False -or [System.Diagnostics.EventLog]::SourceExists($EventSource) -eq $False)
249 {
250     New-EventLog -LogName Application -Source $EventSource
251 }
252 
253 # Detect PowerShell version.
254 If ($PSVersionTable.PSVersion.Major -lt 3)
255 {
256     Write-Log "PowerShell version 3 or higher is required."
257     Throw "PowerShell version 3 or higher is required."
258 }
259 
260 # Find and start the WinRM service.
261 Write-Verbose "Verifying WinRM service."
262 If (!(Get-Service "WinRM"))
263 {
264     Write-Log "Unable to find the WinRM service."
265     Throw "Unable to find the WinRM service."
266 }
267 ElseIf ((Get-Service "WinRM").Status -ne "Running")
268 {
269     Write-Verbose "Setting WinRM service to start automatically on boot."
270     Set-Service -Name "WinRM" -StartupType Automatic
271     Write-Log "Set WinRM service to start automatically on boot."
272     Write-Verbose "Starting WinRM service."
273     Start-Service -Name "WinRM" -ErrorAction Stop
274     Write-Log "Started WinRM service."
275 
276 }
277 
278 # WinRM should be running; check that we have a PS session config.
279 If (!(Get-PSSessionConfiguration -Verbose:$false) -or (!(Get-ChildItem WSMan:\localhost\Listener)))
280 {
281   If ($SkipNetworkProfileCheck) {
282     Write-Verbose "Enabling PS Remoting without checking Network profile."
283     Enable-PSRemoting -SkipNetworkProfileCheck -Force -ErrorAction Stop
284     Write-Log "Enabled PS Remoting without checking Network profile."
285   }
286   Else {
287     Write-Verbose "Enabling PS Remoting."
288     Enable-PSRemoting -Force -ErrorAction Stop
289     Write-Log "Enabled PS Remoting."
290   }
291 }
292 Else
293 {
294     Write-Verbose "PS Remoting is already enabled."
295 }
296 
297 # Ensure LocalAccountTokenFilterPolicy is set to 1
298 # https://github.com/ansible/ansible/issues/42978
299 $token_path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
300 $token_prop_name = "LocalAccountTokenFilterPolicy"
301 $token_key = Get-Item -Path $token_path
302 $token_value = $token_key.GetValue($token_prop_name, $null)
303 if ($token_value -ne 1) {
304     Write-Verbose "Setting LocalAccountTOkenFilterPolicy to 1"
305     if ($null -ne $token_value) {
306         Remove-ItemProperty -Path $token_path -Name $token_prop_name
307     }
308     New-ItemProperty -Path $token_path -Name $token_prop_name -Value 1 -PropertyType DWORD > $null
309 }
310 
311 # Make sure there is a SSL listener.
312 $listeners = Get-ChildItem WSMan:\localhost\Listener
313 If (!($listeners | Where-Object {$_.Keys -like "TRANSPORT=HTTPS"}))
314 {
315     # We cannot use New-SelfSignedCertificate on 2012R2 and earlier
316     $thumbprint = New-LegacySelfSignedCert -SubjectName $SubjectName -ValidDays $CertValidityDays
317     Write-HostLog "Self-signed SSL certificate generated; thumbprint: $thumbprint"
318 
319     # Create the hashtables of settings to be used.
320     $valueset = @{
321         Hostname = $SubjectName
322         CertificateThumbprint = $thumbprint
323     }
324 
325     $selectorset = @{
326         Transport = "HTTPS"
327         Address = "*"
328     }
329 
330     Write-Verbose "Enabling SSL listener."
331     New-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset -ValueSet $valueset
332     Write-Log "Enabled SSL listener."
333 }
334 Else
335 {
336     Write-Verbose "SSL listener is already active."
337 
338     # Force a new SSL cert on Listener if the $ForceNewSSLCert
339     If ($ForceNewSSLCert)
340     {
341 
342         # We cannot use New-SelfSignedCertificate on 2012R2 and earlier
343         $thumbprint = New-LegacySelfSignedCert -SubjectName $SubjectName -ValidDays $CertValidityDays
344         Write-HostLog "Self-signed SSL certificate generated; thumbprint: $thumbprint"
345 
346         $valueset = @{
347             CertificateThumbprint = $thumbprint
348             Hostname = $SubjectName
349         }
350 
351         # Delete the listener for SSL
352         $selectorset = @{
353             Address = "*"
354             Transport = "HTTPS"
355         }
356         Remove-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset
357 
358         # Add new Listener with new SSL cert
359         New-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset -ValueSet $valueset
360     }
361 }
362 
363 # Check for basic authentication.
364 $basicAuthSetting = Get-ChildItem WSMan:\localhost\Service\Auth | Where-Object {$_.Name -eq "Basic"}
365 
366 If ($DisableBasicAuth)
367 {
368     If (($basicAuthSetting.Value) -eq $true)
369     {
370         Write-Verbose "Disabling basic auth support."
371         Set-Item -Path "WSMan:\localhost\Service\Auth\Basic" -Value $false
372         Write-Log "Disabled basic auth support."
373     }
374     Else
375     {
376         Write-Verbose "Basic auth is already disabled."
377     }
378 }
379 Else
380 {
381     If (($basicAuthSetting.Value) -eq $false)
382     {
383         Write-Verbose "Enabling basic auth support."
384         Set-Item -Path "WSMan:\localhost\Service\Auth\Basic" -Value $true
385         Write-Log "Enabled basic auth support."
386     }
387     Else
388     {
389         Write-Verbose "Basic auth is already enabled."
390     }
391 }
392 
393 # If EnableCredSSP if set to true
394 If ($EnableCredSSP)
395 {
396     # Check for CredSSP authentication
397     $credsspAuthSetting = Get-ChildItem WSMan:\localhost\Service\Auth | Where-Object {$_.Name -eq "CredSSP"}
398     If (($credsspAuthSetting.Value) -eq $false)
399     {
400         Write-Verbose "Enabling CredSSP auth support."
401         Enable-WSManCredSSP -role server -Force
402         Write-Log "Enabled CredSSP auth support."
403     }
404 }
405 
406 If ($GlobalHttpFirewallAccess) {
407     Enable-GlobalHttpFirewallAccess
408 }
409 
410 # Configure firewall to allow WinRM HTTPS connections.
411 $fwtest1 = netsh advfirewall firewall show rule name="Allow WinRM HTTPS"
412 $fwtest2 = netsh advfirewall firewall show rule name="Allow WinRM HTTPS" profile=any
413 If ($fwtest1.count -lt 5)
414 {
415     Write-Verbose "Adding firewall rule to allow WinRM HTTPS."
416     netsh advfirewall firewall add rule profile=any name="Allow WinRM HTTPS" dir=in localport=5986 protocol=TCP action=allow
417     Write-Log "Added firewall rule to allow WinRM HTTPS."
418 }
419 ElseIf (($fwtest1.count -ge 5) -and ($fwtest2.count -lt 5))
420 {
421     Write-Verbose "Updating firewall rule to allow WinRM HTTPS for any profile."
422     netsh advfirewall firewall set rule name="Allow WinRM HTTPS" new profile=any
423     Write-Log "Updated firewall rule to allow WinRM HTTPS for any profile."
424 }
425 Else
426 {
427     Write-Verbose "Firewall rule already exists to allow WinRM HTTPS."
428 }
429 
430 # Test a remoting connection to localhost, which should work.
431 $httpResult = Invoke-Command -ComputerName "localhost" -ScriptBlock {$env:COMPUTERNAME} -ErrorVariable httpError -ErrorAction SilentlyContinue
432 $httpsOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
433 
434 $httpsResult = New-PSSession -UseSSL -ComputerName "localhost" -SessionOption $httpsOptions -ErrorVariable httpsError -ErrorAction SilentlyContinue
435 
436 If ($httpResult -and $httpsResult)
437 {
438     Write-Verbose "HTTP: Enabled | HTTPS: Enabled"
439 }
440 ElseIf ($httpsResult -and !$httpResult)
441 {
442     Write-Verbose "HTTP: Disabled | HTTPS: Enabled"
443 }
444 ElseIf ($httpResult -and !$httpsResult)
445 {
446     Write-Verbose "HTTP: Enabled | HTTPS: Disabled"
447 }
448 Else
449 {
450     Write-Log "Unable to establish an HTTP or HTTPS remoting session."
451     Throw "Unable to establish an HTTP or HTTPS remoting session."
452 }
453 Write-VerboseLog "PS Remoting has been successfully configured for Ansible."
454