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