WinRM Issues Caused by Service Principal Name (SPN) HTTP Protocol & Port Associations

Solution as Code:

There is no easy solution to this problem. Certain IIS Pool Accounts will take-over SPN registration of its localhost. In production environments, removing such registrations will lead to undesired effects. Thus, it is recommended that IIS pool account be set as the SYSTEM account, which would identify itself as the named computer account in Active Directory. Hence, SPN errors would not exist. However, such suggestions should only be changed with extensive testing.

To reiterate, these are the 2 steps for substituting the IIS Pool service account with the SYSTEM account – as illustrated below for the case of Microsoft Dynamics:

  1. Run inetmgr.exe > navigate to Application Pools > select target App Pools > Advanced Settings > changed the Identity to LocalSystem > OK > Repeat to change other pool accounts as necessary
  2. Run this function: fixWinRmSpnRegistration (provided herein)

The significance of this post involves Microsoft IIS and WinRM, both of which are important Microsoft features. In addition, downstream dependencies such as Dynamics CRM, Sharepoint, Exchange, etc. may have this SPN registration issue. Hence, this temporary ‘reset’ can be useful to certain scripts as an alternative to explicitly ‘-IncludePortInSPN’ when initiating WinRM sessions. Without having to scan through all verbiage of this article, it’s probably more convenient to paste one of these scripts onto your PowerShell terminal under the context of a System Administrator as workaround to the ‘errorcode 0x80090322’ problem. The first script could be considered more permanent while the the second would be useful in programs where temporary SPN reset would make it more convenient for various iterations to proceed.

Before making any changes, it’s important to record SPN record association of a node as prior settings. Here’s a sample output from running setspn -L $computername:

PS C:\Windows\system32> setspn -L LAX-CRM01
Registered ServicePrincipalNames for CN=LAX-CRM01,OU=CRM,DC=kimconnect,DC=com:
http/LAX-CRM01:5985
http/LAX-CRM01.kimconnect.COM:5985
TERMSRV/LAX-CRM01
TERMSRV/LAX-CRM01.kimconnect.com
WSMAN/LAX-CRM01
WSMAN/LAX-CRM01.kimconnect.com
RestrictedKrbHost/LAX-CRM01
HOST/LAX-CRM01
RestrictedKrbHost/LAX-CRM01.kimconnect.com
HOST/LAX-CRM01.kimconnect.com

PS C:\Windows\system32> setspn -Q http/LAX-CRM01
Checking domain DC=kimconnect,DC=com
CN=CrmIisPoolServiceAccount,OU=CRM,DC=kimconnect,DC=com
http/LAX-CRM01
http/LAX-CRM01.kimconnect.com
Existing SPN found!

From the sample output of the first cmdlet above, we can see that the LAX-CRM01 server does NOT have computer named account as the registrant of its http/ service, except by explicit port 5985 port registration. The second cmdlet tells us that the ‘CrmIisPoolServiceAccount’ is the registrant of the http/ service of the default SPN for this server.

$serverName='SERVERNAMEHERE'

function fixWinRmSpnRegistration($serverName){    

    $domain=$env:USERDNSDOMAIN
    if ($serverName -like "*$domain"){
        $fqdn=$servername
        }else{$fqdn="$servername.$domain"} 

    # Including ActiveDirectory module as prerequisite
    function includeActiveDirectoryModule{
        if (!(get-module activedirectory -ea silentlycontinue)){
        try{
            Import-Module ServerManager
            Add-WindowsFeature RSAT-AD-PowerShell
            import-module activedirectory
            }
        catch{
            install-module activedirectory
            import-module activedirectory
            }
        }
        }

    # Function to copy all group memberships of service account to computer account
    function copyMembership($fromIdentity,$toIdentity){
        includeActiveDirectoryModule
        $groupNames=(Get-ADPrincipalGroupMembership $fromIdentity).Name
        $groupNames|%{Add-ADGroupMember -Identity "$_" -Members $toIdentity -ea SilentlyContinue}
        $currentMemberships=(Get-ADPrincipalGroupMembership $toIdentity).Name
        write-host "$toIdentity now has these memberships:`r`n---------------------------------`r`n$($currentMemberships|out-string)"
        }

    function confirmation($content,$testValue="I confirm",$maxAttempts=3){
            $confirmed=$false;
            $attempts=0;        
            $content|write-host
            write-host "Please review this content for accuracy.`r`n"
            while ($attempts -le $maxAttempts){
                if($attempts++ -ge $maxAttempts){
                    write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
                    break;
                    }
                $userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm";
                if ($userInput.ToLower() -ne $testValue.ToLower()){
                    cls;
                    $content|write-host
                    write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again..`r`n"
                    }else{
                        $confirmed=$true;
                        write-host "Confirmed!`r`n";
                        break;
                        }
                }
            return $confirmed;
        }

    function fixSpn($serverName){
        write-host "Entering fixSpn function..."
        
        includeActiveDirectoryModule
        $serviceAccountObject=(invoke-expression "setspn -Q http/$serverName")[1]
        $serviceAccountName=.{$matches=$null;if($serviceAccountObject -match 'CN=(.*?),'){$matches[1]}else{$null}}
        $serviceAccount=.{try{                          
                            (get-aduser $serviceAccountObject -ea SilentlyContinue).SamAccountName
                            }catch{
                                (get-adcomputer $serviceAccountName -ea SilentlyContinue).Name
                                }}
        
        if ($serviceAccount -eq $null){
            write-warning "No SPN HTTP registration found matching $serverName"
            }
        elseif($serviceAccount -ne $serverName){
            $computerAccount="$serverName`$"        
            write-warning "Please make sure that the service account $serviceAccount (IIS Pool identity) has been changed to $computerAccount"
            $confirmed=confirmation
            if($confirmed){
                write-host "Copying memberships of $serviceAccount to $computerAccount"
                copyMembership $serviceAccount $computerAccount

                write-host "Removing SPN HTTP registration of $serviceAccount from $computername..."
                $null=invoke-expression "setspn -D http/$fqdn $serviceAccount"
                $null=invoke-expression "setspn -D http/$servername $serviceAccount"
    
                write-host "Registering SPN HTTP to $serverName to its corresponding SYSTEM account named $computerAccount"
                $null=invoke-expression "setspn -s http/$fqdn $serverName"
                $null=invoke-expression "setspn -s http/$servername $serverName"
 
                write-host "Validation..."
                $registeredComputer=(get-adcomputer (invoke-expression "setspn -Q http/$serverName")[1] -ea SilentlyContinue).Name
                if($registeredComputer -eq $serverName){
                    write-host "SPN HTTP has been registered toward $serverName successfully." -ForegroundColor Green
                    return $true
                    }else{
                    write-host "SPN HTTP has NOT been registered toward $serverName." -ForegroundColor Red
                    return $false
                    }
                }else{
                    write-host "Current SPN HTTP registration is already registered toward $serverName." -ForegroundColor Green
                    return $true
                    }
            }
        
        }

    # Check to see if this server currently having SPN issues
    function getException($scriptString){
        $ErrorActionPreference='stop'
        Try {
            if($scriptString.gettype() -eq [string]){
                invoke-expression $scriptString
                }
            else{$scriptString.Invoke()}
            write-host 'No errors detected.'
            return $null
            }
        Catch{
            return $Error[0].Exception
            }
    }

    $errorCode='0x80090322'
    $scriptString="`$testSession=new-pssession $serverName"
    $catchErrorType=getException $scriptString
    
    if ($catchErrorType -eq $null){
        write-host "WinRM works fine on $fqdn"
        if($testSession){Remove-PSSession $testSession}
        return $true
        }
    elseif($catchErrorType -match $errorCode){
        $fixedSpn=fixSpn $serverName
        if($fixedSpn){
            write-host "SPN for $serverName is fixed."
            return $true
            }else{
                write-warning "SPN for $serverName is NOT fixed."
                return $false
                }
        }
    else{
        write-warning "This function has not been programmed to deal with this exception.`r`n$catchErrorType"
        return $false
        }
}

fixWinRmSpnRegistration $serverName
# temporarySpnReset.ps1

$serverName='SERVERNAMEHERE'
  
function resetSpn($serverName){
    $domain=$env:USERDNSDOMAIN 
    if ($serverName -like "*$domain"){
        $fqdn=$servername
    }else{$fqdn="$servername.$domain"}
                                                                                                                                                               function fixSpn($serverName,$fqdn){
    if (!(get-command get-aduser -ea silentlycontinue)){
        try{
            Import-Module ServerManager
            Add-WindowsFeature RSAT-AD-PowerShell
            }
        catch{
            install-module activedirectory
            }
        }
    $serviceAccountObject=(invoke-expression "setspn -Q http/$serverName")[1]
    $serviceAccountName=.{$matches=$null;if($serviceAccountObject -match 'CN=(.*?),'){$matches[1]}else{$null}}
    $serviceAccount=.{try{
                         
                        (get-aduser $serviceAccountObject -ea SilentlyContinue).SamAccountName
                        }catch{
                            (get-adcomputer $serviceAccountName -ea SilentlyContinue).Name
                            }}
   
    if ($serviceAccount -eq $null){
        write-host "No SPN HTTP registration found matching $serverName" -ForegroundColor Green
        }
    elseif($serviceAccount -ne $serverName){
        write-host "Current SPN HTTP registration`t: $serviceAccount`r`nProgram now removes that association"
        $null=invoke-expression "setspn -D http/$fqdn`:5985 $serviceAccount"
        $null=invoke-expression "setspn -D http/$servername`:5985 $serviceAccount"
        $null=invoke-expression "setspn -D http/$fqdn $serviceAccount"
        $null=invoke-expression "setspn -D http/$servername $serviceAccount"
   
        write-host "Registering SPN HTTP port 5985 to $serverName"
        $null=invoke-expression "setspn -a http/$fqdn`:5985 $serverName"
        $null=invoke-expression "setspn -a http/$servername`:5985 $serverName"
 
        write-host "Initiating a WinRM session so that $env:computername has a cached token toward $servername`r`nPlease note that this is NOT permanent."
        $session=new-pssession $serverName
        remove-pssession $session

        write-host "Re-registering SPN HTTP to $serviceAccount"
        $null=invoke-expression "setspn -s http/$fqdn $serviceAccount"
        $null=invoke-expression "setspn -s http/$servername $serviceAccount"

        write-host "Validation..."
        $registeredComputer=(get-adcomputer (invoke-expression "setspn -Q http/$serverName`:5985")[1] -ea SilentlyContinue).Name
        if($registeredComputer -eq $serverName){
            write-host "SPN HTTP port 5985 has been registered toward $serverName successfully." -ForegroundColor Green
            return $true
            }else{
            write-host "SPN HTTP port 5985 has NOT been registered toward $serverName." -ForegroundColor Red
            return $false
            }
        }else{
        write-host "Current SPN HTTP port 5985 registration is already registered toward $serverName." -ForegroundColor Green
        return $true
        }
    }
   
    function getException($scriptString){
        $ErrorActionPreference='stop'
        Try {
            if($scriptString.gettype() -eq [string]){
                invoke-expression $scriptString
                }
            else{$scriptString.Invoke()}
            write-host 'No errors detected.'
            return $null
            }
        Catch{
            return $Error[0].Exception
            }
    }
   
    $errorCode='0x80090322'
    $scriptString="`$thisPsSession=new-pssession $fqdn"
    $catchErrorType=getException $scriptString
   
    if ($catchErrorType -eq $null){
        write-host "WinRM works fine on $fqdn"
        if($thisPsSession){Remove-PSSession $thisPsSession}
        return $true
        }
    elseif($catchErrorType -match $errorCode){
        $fixedSpn=fixSpn $serverName $fqdn
        if($fixedSpn){return $true}
        else{return $false}
        }
    else{
        write-host "WinRM is broken, and this function is not meant to deal with this error."
        write-warning $catchErrorType
        return $false
        }
    }
   
resetSpn $serverName
Symptoms:
enter-pssession : Connecting to remote server DYNAMICS-CRM failed with the following error message : WinRM cannot process the request. The following error with errorcode 0x80090322 occurred while using Kerberos authentication: An unknown security error occurred.
Possible causes are:
-The user name or password specified are invalid.
-Kerberos is used when no authentication method and no user name are specified.
-Kerberos accepts domain user names, but not local user names.
-The Service Principal Name (SPN) for the remote computer name and port does not exist.
-The client and remote computers are in different domains and there is no trust between the two domains.
After checking for the above issues, try the following:
-Check the Event Viewer for events related to authentication.
-Change the authentication method; add the destination computer to the WinRM TrustedHosts configuration setting or use HTTPS transport.
Note that computers in the TrustedHosts list might not be authenticated.
-For more information about WinRM configuration, run the following command: winrm help config. For more
information, see the about_Remote_Troubleshooting Help topic.
At line:1 char:1
+ enter-pssession DYNAMICS-CRM
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (DYNAMICS-CRM:String) [Enter-PSSession], PSRemotingTransportException
+ FullyQualifiedErrorId : CreateRemoteRunspaceFailed
Troubleshooting:

Let’s check SPN records in Active Directory for the DYNAMICS-CRM server’s http/ protocols

PS C:\Windows\system32> setspn -Q http/DYNAMICS-CRM
Checking domain DC=kimconnect,DC=com
CN=crmIisPoolServiceAccount,OU=CRM Security Groups,DC=kimconnect,DC=com
http/DYNAMICS-CRM.kimconnect.com
http/DYNAMICS-CRM
Existing SPN found!

The results above prove that http/ defaults are registered by a Service Principal Name (SPN) ‘crmIisPoolServiceAccount’.

PS C:\Windows\system32> setspn -Q http/DYNAMICS-CRM:5985
Checking domain DC=kimconnect,DC=com
CN=DYNAMICS-CRM,OU=CRM,DC=kimconnect,DC=com
HTTP/DYNAMICS-CRM.kimconnect.com:5985
HTTP/DYNAMICS-CRM:5985
TERMSRV/DYNAMICS-CRM
TERMSRV/DYNAMICS-CRM.kimconnect.com
WSMAN/DYNAMICS-CRM
WSMAN/DYNAMICS-CRM.kimconnect.com
RestrictedKrbHost/DYNAMICS-CRM
HOST/DYNAMICS-CRM
RestrictedKrbHost/DYNAMICS-CRM.kimconnect.com
HOST/DYNAMICS-CRM.kimconnect.com
Existing SPN found!

This subsequent output confirms that http/ WinRM (port 5985) have been registered by SPN ‘DYNAMICS-CRM’ (the SYSTEM or computer account) – this appears to be the correct setting. This means that WinRM should be working on port 5985. We can further confirm this by initiating a WinRM session with inclusion of ports in SPN session as shown below:

PS C:\Windows\system32> enter-pssession DYNAMICS-CRM -SessionOption $(new-pssessionoption -IncludePortInSPN)
[DYNAMICS-CRM]: PS C:\Users\cuilo\Documents> |

The successful connection above proves that WinRM is working fine when being connected by using the explicit -SessionOption. However, if that is acceptable, we wouldn’t be reading this article. Hence, let’s take a closer look at the inner workings of SPN registrations to increase our understanding.

It appears that the query above does not show a complete associations between SPN’s and Computer Accounts. In fact, the IIS service account on the CRM-DYNAMICS server has a persistent hold on the redirection of protocol http port 5985. That is, the list above only shows one instance of HTTP/*, there are two instances: HTTP/* and HTTP/*:5985. If we want to re-associate HTTP/* back to the original IIS Pool Service account, we can repeat these steps to re-associate the HTTP/* registration account from $servername to $registerAccount. In fact, I believe that is the final solution that doesn’t require changing IIS pool impersonation account while allowing WinRM to work as intended

Solution:

Here is a quick snippet to (a) remove the registration of the default http protocol from the IIS pool service account (b) create an explicit registration of http port 5985 in association with the computer account and (c) re-registering the default http protocol association toward the service account

Part A:
$domain='kimconnect.com'
$servername='DYNAMICS-CRM'
$fqdn="$servername.$domain"
$registerAccount='crmIisPoolServiceAccount'

# Delete the http association with the service account
setspn -D http/$fqdn:5985 $registerAccount
setspn -D http/$servername:5985 $registerAccount
setspn -D http/$fqdn $registerAccount
setspn -D http/$servername $registerAccount
# Output:
######################################################
#PS C:\Windows\system32> setspn -D http/$fqdn $registerAccount
#Unregistering ServicePrincipalNames for CN=crmIisPoolServiceAccount,OU=CRM Security Groups,DC=kimconnect,DC=com
# http/DYNAMICS-CRM.kimconnect.com
#Updated object
Part B:
# Create port-specific SPN association to the server's computer account
# This step may error out as WinRM should already have port 5985 registered if it has been enabled prior to IIS
SetSPN -s HTTP/$servername:5985 $servername
SetSPN -s HTTP/$fqdn:5985 $servername
Part C:
# Re-create http SPN assocation toward the IIS Pool service account
SetSPN -s HTTP/$servername $registerAccount
SetSPN -s HTTP/$fqdn $registerAccount
Result:

Before:

PS C:\Windows\system32> setspn -Q http/DYNAMICS-CRM:5985
Checking domain DC=kimconnect,DC=com
CN=DYNAMICS-CRM,OU=kimconnect,DC=com
HTTP/DYNAMICS-CRM.kimconnect.com:5985
HTTP/DYNAMICS-CRM:5985
TERMSRV/DYNAMICS-CRM
TERMSRV/DYNAMICS-CRM.kimconnect.com
WSMAN/DYNAMICS-CRM
WSMAN/DYNAMICS-CRM.kimconnect.com
RestrictedKrbHost/DYNAMICS-CRM
HOSTDYNAMICS-CRM
RestrictedKrbHost/DYNAMICS-CRM.kimconnect.com
HOST/DYNAMICS-CRM.kimconnect.com
Existing SPN found!

After:

PS C:\Windows\system32> setspn -Q http/DYNAMICS-CRM:5985
Checking domain DC=kimconnect,DC=com
CN=DYNAMICS-CRM,OU=kimconnect,DC=com
HTTP/DYNAMICS-CRM.kimconnect.com:5985
HTTP/DYNAMICS-CRM:5985
HTTP/
TERMSRV/DYNAMICS-CRM
TERMSRV/DYNAMICS-CRM.kimconnect.com
WSMAN/DYNAMICS-CRM
WSMAN/DYNAMICS-CRM.kimconnect.com
RestrictedKrbHost/DYNAMICS-CRM
HOSTDYNAMICS-CRM
RestrictedKrbHost/DYNAMICS-CRM.kimconnect.com
HOST/DYNAMICS-CRM.kimconnect.com
Existing SPN found!

Moment of truth: WinRM can be initiated without -SessionOption

PS C:\Windows\system32> enter-pssession DYNAMICS-CRM
[DYNAMICS-CRM]: PS C:\Users\cuilo\Documents> |

Since we have not changed the default HTTP protocol, here’s validation of such settings being ‘same as before’:

######################################################
PS C:\Windows\system32> setspn -Q http/DYNAMICS-CRM
Checking domain DC=kimconnect,DC=com
CN=crmIisPoolServiceAccount,OU=CRM Security Groups,DC=kimconnect,DC=com
http/DYNAMICS-CRM.kimconnect.com
http/DYNAMICS-CRM
Existing SPN found!
Known Errors:
# Error: caused by attempting to set an SPN that already has associations
#PS C:\Windows\system32> SetSPN -s HTTP/$servername:5985 $servername
#Checking domain DC=kimconnect,DC=com
#CN=DYNAMICS-CRM,CN=Computers,DC=kimconnect,DC=com
# HTTP/
# TERMSRV/DYNAMICS-CRM
# TERMSRV/DYNAMICS-CRM.kimconnect.com
# WSMAN/DYNAMICS-CRM
# WSMAN/DYNAMICS-CRM.kimconnect.com
# RestrictedKrbHost/DYNAMICS-CRM
# HOST/DYNAMICS-CRM
# RestrictedKrbHost/DYNAMICS-CRM.kimconnect.com
# HOST/DYNAMICS-CRM.kimconnect.com
#Duplicate SPN found, aborting operation!

#Explanation:
# Each SPN can only be registered to one account. Active Directory will not allow duplicate registration of a named SPN. Hence, it may be necessary to remove an SPN and its association to an account prior to re-registering to another account.
# Error:
#Enter-PSSession : Connecting to remote server DYNAMICS-CRM failed with the following error message : WinRM cannot
#process the request. The following error occurred while using Kerberos authentication: Cannot find the computer
#DYNAMICS-CRM. Verify that the computer exists on the network and that the name provided is spelled correctly. For more
#information, see the about_Remote_Troubleshooting Help topic.
#At line:1 char:1
#+ Enter-PSSession -Computername $servername -SessionOption $sessionOptions
#+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# + CategoryInfo : InvalidArgument: (DYNAMICS-CRM:String) [Enter-PSSession], PSRemotingTransportException
# + FullyQualifiedErrorId : CreateRemoteRunspaceFailed

# Explanation:
# The computer account needs to register in AD as the SPN for WinRM of that machine name

Leave a Reply

Your email address will not be published. Required fields are marked *