-
-
Save bender-the-greatest/3e2f6e6d606eebaeedbacd8722e52396 to your computer and use it in GitHub Desktop.
<# | |
.SYNOPSIS | |
Configures a secure WinRM listener over HTTPS to enable | |
SSL-based WinRM communications. This script has not been | |
tested on Windows Server 2003R2 or earier, and may not | |
work on these OSes for a variety of reasons. | |
If Windows Remote Management is disabled (e.g. service | |
stopped, GPO Policy, etc.), this script will likely fail. | |
.DESCRIPTION | |
This script is designed to be used in two ways: | |
1. Fire on 1001 or 1006 event ids in the | |
CertificateServicesClient-Lifecycle-System event log | |
2. Use the -FindCert parameter to select the most recent | |
valid certificate created from the 'WinRM' certificate template. | |
This script is designed to take parameters from the | |
CertificateServicesClient-Lifecycle-System event log. | |
This can be used gracefuly with Task Scheduler provided | |
you add a bit of custom XML code to your event trigger(s). | |
The easiest way to generate the proper XML for a task is to: | |
1. Create the task in the Task Scheduler GUI. Set everything | |
else you would otherwise need in the Task. | |
2. Export the task to a file. Remove the task you created in | |
Step 1. | |
3. Add whatever XML you need. Boilerplate XML is outlined below. | |
4. Save the modified XML file and mport the task into Task Scheduler. | |
Supported Event IDs | |
1001 - Replace | |
1006 - New/Enroll | |
In order to use event properties as variables to the script, | |
you must use the add the <ValueQueries> element under | |
each <EventTrigger> used to fire off your script like so: | |
<Triggers> | |
<EventTrigger> | |
<ValueQueries> | |
<Value name="Template"> | |
Event/UserData/CertNotificationData/CertificateDetails/Template/@Name | |
</Value> | |
<Value name="Thumbprint"> | |
Event/UserData/CertNotificationData/CertificateDetails/@Thumbprint | |
</Value> | |
<Value name="Context"> | |
Event/UserData/CertNotificationData/@Context | |
</Value> | |
<Value name="EventID"> | |
Event/System/EventID | |
</Value> | |
</ValueQueries> | |
</EventTrigger> | |
</Triggers> | |
The way this works is that the text of the <Value> element is actually | |
XPATH syntax corresponding to an element or attribute value. If you inspect | |
the XML tab of an event log entry, you can see how these paths correlate. | |
Each <Value> element can be referenced by name in the Action arguments. | |
For example, in the new action window: | |
Program/script: \path\to\Configure-SecureWinRM.ps1 | |
Add arguments: -Template '$(Template)' -Thumbprint '$(Thumbprint)' -Context '$(Context)' -EventID '$(EventID)' | |
Notice how the variable names correspond to the @name | |
attribute of <Value>. | |
Event triggers will not work prior to OS version 6.2 | |
as the certificate lifecycle events were introduced in | |
Win8/WS2012. However, there is a secondary parameter | |
which can be used for older servers, which looks for the | |
any certificate generated from the WinRM certificate | |
template and selects the certificate with the furthest | |
expiry date (as long as the Valid From date has already | |
passed). However, there should generally only be one | |
WinRM certificate generated at a time. | |
.PARAMETER Template | |
The name of the template used to generate the certificate which | |
will be used for WinRM. Should come from the following XML path | |
in the event log entry: | |
Event/UserData/CertNotificationData/CertificateDetails/Template/@Name | |
The template must be 'WinRM' or the script will stop executing by design. | |
.PARAMETER Thumbprint | |
The thumbprint of the certificate to be used for WinRM. Should | |
come from the following XML path in the event log entry: | |
Event/UserData/CertNotificationData/CertificateDetails/@Thumbprint | |
.PARAMETER Context | |
The context into which the certifcate was installed (e.g. User, Machine, etc.) | |
If this value is anything but 'Machine' the script will stop executing by design. | |
Should come from the following XPATH of the event log entry: | |
Event/UserData/CertNotificationData/@Context | |
.PARAMETER EventID | |
The EventID which triggered the task calling this script. Must be 1001 or 1006 | |
or the script will stop executing by design. Should come from the following | |
XML path of the event log entry: | |
Event/System/EventID | |
.PARAMETER FindCert | |
When this parameter is used, the previous parameters are not required. | |
When -FindCert is specified, the script will search the certificate store path | |
specified by -CertStoreLocation for any currently valid certificates that were | |
generated using the 'WinRM' template. If more than one suitable certificate is | |
found, it will select the certificate which has the furthest expiry date. | |
.PARAMETER CertStoreLocation | |
A string (or array of strings) representing certificate store paths which | |
will be searched for suitable certificates. Set to 'Cert:\LocalMachine\My' | |
by default. | |
#> | |
[CmdletBinding()] | |
Param( | |
[Parameter(ParameterSetName='FromEventLog', Mandatory=$true)] | |
[string]$Template, | |
[Parameter(ParameterSetName='FromEventLog', Mandatory=$true)] | |
[string]$Thumbprint, | |
[Parameter(ParameterSetName='FromEventLog', Mandatory=$true)] | |
[string]$Context, | |
[Parameter(ParameterSetName='FromEventLog', Mandatory=$true)] | |
[int]$EventID, | |
[Parameter(ParameterSetName='AutoCert')] | |
[switch]$FindCert, | |
[string[]]$CertStoreLocation = 'Cert:\LocalMachine\My' | |
) | |
# Event log IDs | |
$REPLACE_EVENT = 1001 | |
$ENROLL_EVENT = 1006 | |
## BEGIN FUNCTIONS ## | |
function checkParams { | |
if ( -Not $FindCert ) { | |
if ( -Not $Template -And -Not $Thumbprint -And -Not $Context -And -Not $EventID ) { | |
throw 'This script cannot be run without arguments' | |
} | |
if ( $Template.ToLower() -ne 'winrm' ) { | |
Write-Verbose 'WARNING: Refusing to act on certificate sourced from non-WinRM certificate template.' | |
# Exit cleanly here, as this script could be fired off when other certs are renewed or enrolled | |
exit 0 | |
} | |
if ( $Context.ToLower() -ne 'machine' ) { | |
Write-Verbose 'WARNING: Refusing to act on non-LocalMachine certificate' | |
# Exit cleanly here, as this script could be fired off when other certs are renewed or enrolled | |
exit 0 | |
} | |
if ( ( $EventID -ne $REPLACE_EVENT ) -And ( $EventID -ne $ENROLL_EVENT ) ) { | |
Write-Verbose ( "WARNING: Refusing to act on any EventID other than {0} or {1}" -f $REPLACE_EVENT, $ENROLL_EVENT ) | |
# Exit cleanly here, as this script could be fired off when other certs are renewed or enrolled | |
exit 0 | |
} | |
if ( $Thumbprint -eq $null ) { | |
Write-Error "-Thumbprint cannot be null" | |
# If this is null something is wrong, trigger a failure | |
exit 1 | |
} | |
} | |
# Enforce -CertStoreLocation paths being rooted under Cert:\LocalMachine | |
if ( ( $CertStoreLocation | Where-Object { $_.ToLower().StartsWith('cert:\localmachine') } ).Count -ne $CertStoreLocation.Count ) { | |
Write-Error 'All -CertStoreLocation paths must be rooted under Cert:\LocalMachine' | |
exit 2 | |
} | |
} | |
function execute { | |
Param( | |
[Parameter(Mandatory=$true)] | |
[string]$Command, | |
[switch]$SuppressOutput, | |
[switch]$ThrowOnError | |
) | |
Write-Debug ( "EXECUTE>> {0}" -f $Command ) | |
if ( $SuppressOutput ) { | |
Invoke-Expression $Command -ErrorVariable execute_error | Out-Null | |
} else { | |
Invoke-Expression $Command -ErrorVariable execute_error | Write-Host | |
} | |
if ( $ThrowOnError -And $execute_error ) { throw $execute_error } | |
# Invoke-Expression always makes $? $true | |
( $result = ( -Not $LASTEXITCODE ) ) | |
Write-Debug ( "Result of last command: {0}" -f $result ) | |
} | |
function getFQDN { | |
Write-Verbose 'Checking FQDN' | |
$shortname = ( Get-WmiObject win32_computersystem ).DNSHostName | |
if ( ( Get-WmiObject win32_computersystem ).PartOfDomain ) { | |
"{0}.{1}" -f $shortname, ( Get-WmiObject win32_computersystem ).Domain | |
} else { | |
$shortname | |
} | |
} | |
function certExists { | |
Param( | |
[Parameter(Mandatory=$true)] | |
[string]$Thumbprint, | |
[Parameter(Mandatory=$true)] | |
[string[]]$CertStoreLocation | |
) | |
Write-Verbose ( "Checking {1} for certificate with thumbprint {0}" -f $Thumbprint, ( $CertStoreLocation -Join ', ' ) ) | |
$cert = Get-ChildItem $CertStoreLocation | Where-Object { $_.Thumbprint -eq $Thumbprint } | |
[bool]$cert | |
} | |
function listenerExists { | |
Write-Verbose 'Checking if secure WinRM listener is already configured' | |
$command = 'winrm get winrm/config/listener?Address=*+Transport=HTTPS' | |
execute -Command $command | |
} | |
# Creates secure WinRM listener. Returns $true if success and $false if failed | |
function createSecureListener { | |
Param( | |
[Parameter(Mandatory=$true)] | |
[string]$CertificateThumbprint, | |
[Parameter(Mandatory=$true)] | |
[string]$FQDN, | |
[int]$Port = 5986 | |
) | |
Write-Host ( "Creating new secure listener for WinRM on port {0}" -f $Port ) | |
Write-Verbose ( "Setting WinRM Hostname property to {0}" -f $FQDN ) | |
Write-Verbose ( "Setting WinRM CertificateThumbprint property to {0}" -f $CertificateThumbprint ) | |
$command = "winrm create winrm/config/listener?Address=*+Transport=HTTPS" | |
$configItems = "`"@{ Hostname=```"$FQDN```"; CertificateThumbprint=```"$CertificateThumbprint```"; Port=```"$Port```" }`"" | |
execute -Command ( "{0} {1}" -f $command, $configItems ) | |
} | |
# Updates the secure WinRM listener. Returns $true on success and $false on $failure | |
function updateSecureListener { | |
Param( | |
[string]$CertificateThumbprint, | |
[string]$FQDN, | |
[int]$Port | |
) | |
if ( $CertificateThumbprint -Or $FQDN -Or $Port ) { | |
Write-Host 'Updating existing secure listener for WinRM' | |
$configItems = New-Object System.Collections.Generic.List[string] | |
if ( $CertificateThumbprint ) { | |
Write-Verbose ( "Setting WinRM CertificateThumbprint property to {0}" -f $CertificateThumbprint ) | |
$configItems.Add( "CertificateThumbprint=```"{0}```"" -f $CertificateThumbprint ) | |
} | |
if ( $FQDN ) { | |
Write-Verbose ( "Setting WinRM Hostname property to {0}" -f $FQDN ) | |
$configItems.Add( "Hostname=```"{0}```"" -f $FQDN ) | |
} | |
if ( $Port ) { | |
Write-Verbose ( "Setting Winrm Port property to {0}" -f $Port ) | |
$configItems.Add( "Port=```"{0}```"" -f $Port ) | |
} | |
$command = "winrm set winrm/config/listener?Address=*+Transport=HTTPS `"@{{ {0} }}`"" -f ( $configItems -Join '; ' ) | |
execute $command | |
} else { | |
Write-Verbose 'WARNING: No parameters were specified. No configuration was updated.' | |
$true | |
} | |
} | |
# Locates a suitable WinRM certificate based off of the certificate template. | |
# Used in the event that the certificate exists before the scheduled task was | |
# created (AKA first time run after the certificate template was already applied). | |
# If more than one certificate is found, the certificate with the latest expiration | |
# is used (as long as the current time is between NotBefore and NotAfter). | |
# Returns a suitable certificate object for use with WinRM. | |
function findWinRMCert { | |
Param( | |
[Parameter(Mandatory=$true)] | |
[string[]]$CertStoreLocation | |
) | |
$templateNameField = 'Certificate Template Information' | |
# Get certs that have been applied with a certificate template and currently valid (by date) | |
Write-Verbose ( "Searching for suitable certificates in {0}" -f ( $CertStoreLocation -Join ', ' ) ) | |
$now = Get-Date | |
$certs = Get-ChildItem $CertStoreLocation | Where-Object { | |
$_.Extensions.Oid.FriendlyName -eq $templateNameField ` | |
-And $now -ge $_.NotBefore -And $now -le $_.NotAfter | |
} | Sort-Object -Property NotAfter -Descending | |
$winrm_certs = New-Object System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2] | |
# Sorcery to check the template name | |
$certs | Foreach-Object { | |
$temp = $_.Extensions | Where-Object { $_.Oid.FriendlyName -eq $templateNameField } | |
$templateName = ( $temp.Format($true) -Split "`r`n" | Where-Object { $_.StartsWith('Template=') } ) -Replace 'Template=', '' | |
if ( $templateName.ToLower().StartsWith('winrm') ) { | |
$winrm_certs.Add( $_ ) | |
} | |
} | |
if ( $winrm_certs ) { | |
$winrm_certs[0] | |
} else { | |
$null | |
} | |
} | |
## END FUNCTIONS ## | |
$OldErrorActionPreference = $ErrorActionPreference | |
$ErrorActionPreference = "Stop" | |
try { | |
checkParams | |
$fqdn = getFQDN | |
Write-Verbose ( "Using detected FQDN of {0}" -f $fqdn ) | |
if ( -Not $FindCert ) { | |
if ( -Not ( certExists $Thumbprint ) ) { throw ( "Certificate thumbprint {0} not found" -f $Thumbprint ) } | |
} else { | |
$foundCert = ( findWinRMCert $CertStoreLocation ) | |
if ( $foundCert ) { | |
$Thumbprint = $foundCert.Thumbprint | |
} else { | |
$Thumbprint = $null | |
} | |
if ( -Not $Thumbprint ) { throw 'Could not find a suitable certificate for use with WinRM' } | |
} | |
if ( -Not ( listenerExists ) ) { | |
$ret = createSecureListener -CertificateThumbprint $Thumbprint -FQDN $fqdn | |
if ( -Not $ret ) { throw "createSecureListener failed" } | |
} else { | |
$ret = updateSecureListener -CertificateThumbprint $Thumbprint -FQDN $fqdn | |
if ( -Not $ret ) { throw "updateSecureListener failed" } | |
} | |
} finally { | |
$ErrorActionPreference = $OldErrorActionPreference | |
} |
Unfortunately, WS 2022 is still going through testing in my org and this script hasn't been evaluated against 2022 instances yet. That said, this should work fine for 2012R2 through 2019 at the very least.
Additionally, what you're saying shouldn't be happening. The error you get is expected if there is no listener, but it should not be throwing an error at line 183. It should at least flow to the next lines for evaluation before an error is thrown, if one were to be (notice I never specify $ThrowOnError
when calling execute
, and in fact it is currently a no-op bug since winrm.exe
doesn't seem to actually output to the error stream). Something else is going on here.
I would be surprised if it's anything in this code directly based on how you've presented the issue as the code does not flow in the way you described. There should be another error being thrown at the end of the script. Try running without -Debug
and let me know what the final error is.
Also, in case you haven't, make sure you read the full synopsis and description. Note that this script will not work with self-signed certificates; the certificate must come from an AD Certificate Services tempate named WinRM
.
Also, how are you invoking the script? The arguments are useful but impractical since you can only parameterize locally-defined scheduled task triggers and not against GPO-managed tasks. Even in my environments we only ever call this with -FindCert
, waiting for the day GPO-defined scheduled tasks can have parameters defined in the task XML.
Hi,
Thanks for your reply.
I'm manually launching the script from powershell admin command line with -FindCert
option.
The certificate template is called WinRM and comes from an ADCS.
I've just launched it on a new WS2022 core server and it worked perfectly... so I'll need to reproduce the problem I've encountered and let you know. with screen captures if possible.
Hey,
This is failing to run. Line 315 is missing a parameter:
if ( -Not ( certExists $Thumbprint ) ) { throw ( "Certificate thumbprint {0} not found" -f $Thumbprint ) }
Changing it to the following corrects the issue:
if ( -Not ( certExists $Thumbprint $CertStoreLocation) ) { throw ( "Certificate thumbprint {0} not found" -f $Thumbprint ) }
Replacing Get-WmiObject with Get-CimInstance -ClassName makes it functional on both PowerShell 5 and PowerShell 7 (6 onwards) - 3 references in getFQDN from line 194
Firstly, thanks for sharing this script. Its helped me alot!
I get the same error as tof06250 when executing listenerExists
winrm : WSManFault
At line:1 char:1
+ winrm get winrm/config/listener?Address=*+Transport=HTTPS
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (WSManFault:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
Updating the createListener function worked a treat to handle the error to feed to createSecureListener on line 327
function ListenerExists {
Write-Verbose 'Checking if secure WinRM listener is already configured'
try {
$command = 'winrm get winrm/config/listener?Address=*+Transport=HTTPS'
$result = execute -Command $command
return $true
} catch {
Write-Verbose "Error checking listener configuration: $_"
return $false
}
}
Lastly, are you disabling HTTP and creating FW rules via GPO in conjuction with this script?
Cheers,
Brad.
I'll try to find some time to update this with the suggestions so far eventually. When I originally uploaded this I may have done so with an older revision that had some issues.
@BradSherwin This only sets up WinRM over SSL, it does not touch the HTTP configuration or create FW rules (in our environment we don't rely on Windows Firewall). With most domain configurations, WinRM without SSL can still be securely used as long as Basic Authentication isn't used.
Hi,
I've come across your script and tried it on a WS 2022 server..
I get an error with the function execute that I don't really understand.
Here's the output on a server that doesn't have WinRM HTTPS listener:
Manually typing this :
Value in the $Command line sent to execute function:
When it comes to the line 179
it launches line 183
Then goes straight to line 334
without launching what it's supposed to:
Can you help me with this please?
Thanks