Many of you use IIS web servers in corporate network for various purposes, for example, to host internal/external web site, ADCS web services, OCSP, WSUS and this list is very long. It is common to manage them all from a centralized place, for example, from web server administrator’s computer. In other words, IIS servers are not managed directly from console.

The problem

By default IIS do not allow remote administration, you have to enable it by starting (and, likely, setting start type to Automatic) Web Management Service (WMSVC). Ok, you started the service on a web server and attempt to connect to the server from remote IIS management console:

Server Certificate Alert: The certificate was issued to a different server.

You see Server Certificate Alert that complains about wrong certificate (This certificate was issued to a different server). When you click on View Certificate button, you see certificate details.

What happened here? When you start Web Management Service, the service generates self-signed certificate (if it was not generated previously) which is valid for 10 years and uses it for remote administration connection. I always say that self-signed certificates MUST NOT be used anywhere, except test environments and Root CA servers. Therefore we need to configure IIS to use certificate issued from a internal PKI.

Certificate enrollment

If your IIS server already have server certificate, then you can skip this section. Otherwise, you should not skip it. So, at first, we need to get a certificate for Server Authentication purpose. I asked Crypto Guy “Hey Crypto Guy, I have a IIS server and need to get a certificate! Also, I have some IIS servers installed on Server Core. How would you solve this task?” And he answered me “Yo, dude, I have some great options for you and they rely on CertEnroll. Consider to upgrade your skills in CertEnroll’ing.” We had a long discussion and ended up with the following scenarios:

  1. Use default Computer (Machine) certificate template;
  2. Use custom certificate template which accepts subject information from request. While first option will require full server’s FQDN to access, this option allows you to include server’s short (NetBIOS) name and you will be able to access to a server via both, NetBIOS and FQDN names.

And he provided PowerShell scripts for each scenario:

1) using default Computer (Machine) or other certificate template that automatically builds subject information:

function Get-WebAdministrationCertificate {
[CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]$TemplateName,
        [string]$FriendlyName
    )
    $ErrorActionPreference = "Stop"
    # instantiate IX509Enrollment interface: http://msdn.microsoft.com/en-us/library/aa377809(v=vs.85).aspx
    $Request = New-Object -ComObject X509Enrollment.CX509Enrollment
    # initialize interface from template name. Interface will automatically grabs
    # template settings.
    $Request.InitializeFromTemplateName(2,$TemplateName)
    # set optional friendly name
    $Request.CertificateFriendlyName = $FriendlyName
    # attempt to enroll for a certificate
    $Request.Enroll()
    if ($Request.Status.Status -eq 1) {
        # output issued certificate to pipeline.
        New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList `

            @(,[Convert]::FromBase64String($Request.Certificate(1)))
    } else {
        Write-Warning "Enrollment failed, enrollment disposition code: $($Request.Status.Status)"
        Write-Warning "Disposition message: $($Request.Status.ErrorText)"
    }
}

The usage is pretty simple: Pass certificate template common name (not display name) and optionally friendly name for it.

2) a bit modified script used when certificate template builds subject information from incoming request. It is a good practice to place all such request pending to ensure that subject information do not violate security requirements:

function Get-WebAdministrationCertificate {
[CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]$TemplateName,
        [string]$FriendlyName
    )
    $ErrorActionPreference = "Stop"
    # instantiate IX509Enrollment interface: http://msdn.microsoft.com/en-us/library/aa377809(v=vs.85).aspx
    $Request = New-Object -ComObject X509Enrollment.CX509Enrollment
    # initialize interface from template name. Interface will automatically grabs
    # template settings.
    $Request.InitializeFromTemplateName(2,$TemplateName)
    # set optional friendly name
    $Request.CertificateFriendlyName = $FriendlyName
    # generate subject information
    $Subject = New-Object -ComObject X509Enrollment.CX500DistinguishedName
    $Subject.Encode("CN=$([Net.Dns]::GetHostByName((hostname)).HostName)",0)
    $Request.Request.GetInnerRequest(0).Subject = $Subject
    # Write both, NetBIOS and server FQDN to a Subject Alternative Name (SAN) extension
    # for more details look this article: http://en-us.sysadmins.lv/Lists/Posts/Post.aspx?ID=11
    $SAN = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames
    $IANs = New-Object -ComObject X509Enrollment.CAlternativeNames
    $IAN1 = New-Object -ComObject X509Enrollment.CAlternativeName
    $IAN2 = New-Object -ComObject X509Enrollment.CAlternativeName
    $IAN1.InitializeFromString(0x3,$Env:COMPUTERNAME)
    $IAN2.InitializeFromString(0x3,[Net.Dns]::GetHostByName((hostname)).HostName)
    $IAN1, $IAN2 | ForEach-Object {$IANs.Add($_)}
    $SAN.InitializeEncode($IANs)
    $Request.Request.GetInnerRequest(0).X509Extensions.Add($SAN)
    # attempt to enroll for a certificate
    $Request.Enroll()
    if ($Request.Status.Status -eq 1) {
        # output issued certificate to pipeline.
        New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList `

            @(,[Convert]::FromBase64String($Request.Certificate(1)))
    } elseif ($Request.Status.Status -eq 2) {
        Write-Warning "Certificate is pending for CA manager approval."
        Write-Warning "Request details:"
        Write-Warning "Request ID: $($Request.RequestId)"
        Write-Warning "CA server: $($Request.CAConfigString)"
    } else {
        Write-Warning "Enrollment failed, enrollment disposition code: $($Request.Status.Status)"
        Write-Warning "Disposition message: $($Request.Status.ErrorText)"
    }
}

Look what changed: I added a code that fills subject information and creates Subject Alternative Name (SAN) extension, where we put both NetBIOS and FQDN names to this extension. Also I added additional status checking that handles request pending state. In this case we need to inform caller about request data: request ID and CA server path that processes this request. You will use this data to retrieve and install issued certificate when CA manager approves it.

Therefore we come to a second script that will install issued certificate:

function Install-Certificate {
[CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$ConfigString,
        [Parameter(Mandatory = $true, Position = 1)]
        [int]$RequestID
    )
    $CertRequest = New-Object -ComObject CertificateAuthority.Request
    if ($CertRequest.RetrievePending($RequestID,$ConfigString) -eq 3) {
        $Response = New-Object -ComObject X509Enrollment.CX509Enrollment
        $Response.Initialize(0x2)
        $Response.InstallResponse(0x4,$CertRequest.GetCertificate(1),0x1,"")
        $Cert = New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList `

            @(,[Convert]::FromBase64String($CertRequest.GetCertificate(1)))
    }
}

This function is necessary only when certificate request previously was placed to a pending state. Ok, we have issued certificate, now we can configure IIS to use it for remote management connections:

Configure certificate

We reached the last step: write certificate information to registry. Certificate information is written to the following registry key:

HKLM:\SOFTWARE\Microsoft\WebManagement\Server

and there is SslCertificateHash value of REG_BINARY type which accepts certificate’s thumbprint. Here is a sample function that achieves this task:

function Configure-WebAdministrationCertificate {
[CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
    )
    # if everything is ok and certificate was successfully installed, we can procceed
    # with certificate registration in registry:
    if (!(Test-Path "HKLM:\SOFTWARE\Microsoft\WebManagement\Server")) {
        [void](New-Item -Path "HKLM:\SOFTWARE\Microsoft\WebManagement\" -Name "Server" -Force -ErrorAction Stop)
    }
    # split certificate's thumbprint to hex octets
    $Tokens = $Certificate.Thumbprint -split "([a-fA-F0-9]{2})" | ? {$_}
    # convert each octet to a byte and write them to a byte array.
    [Byte[]]$Bytes = $Tokens | %{[Convert]::ToByte($_,16)}
    Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\WebManagement\Server -Name SslCertificateHash -Value $Bytes
    # configure Http.sys bindings:
    Import-Module WebAdministration
    del IIS:\SslBindings\0.0.0.0!8172
    $Certificate | New-Item -Path IIS:\SslBindings\0.0.0.0!8172 -Force
    # restart Web Management service
    Restart-Service -Name wmsvc
    # phinal!
}

Now, we should not receive any errors regarding server certificate when connecting to a remote IIS server from IIS console.


Share this article:

Comments:

S?bastien D

Hey there. Thanks for this post, it's been quite helpful. Although I'd like to point out that your cmdlet for updating the certificate in the registry is not sufficient. Apparently that registry key is just stored there for the IIS GUI. So changing it will not make the new certificate effective for remote connexions. The actual ip:port + certificate binding is done at the HTTP.SYS level and can be managed through netsh. You can view the current bindings using: netsh http show sslcert If you only do the registry modification, you'll notice that the thumbprint displayed using netsh is still the one of the self-signed certificate, not the one you have created. A more complete solution should include the following: http://serverfault.com/questions/385180/how-to-assign-an-different-ssl-certificate-for-the-iis7-management-service-on-s Which recreates the HTTP.SYS binding using NetSH. Combining your registry update & the netsh calls does work. Although I later found out another (much cleaner) approach that relies on the WebAdministration (iis) powershell module. That approach is described there and is IMHO a safer approach (for the long run): http://technet.microsoft.com/en-us/magazine/dn198619.aspx

Vadims Podans

> The actual ip:port + certificate binding is done at the HTTP.SYS the subject is not about SSL certificate binding to web sites. The sole purpose of the whole article is IIS remote management.

Math

Vadims, maybe you could help me.

I have published a cert template for computers.

Is it possible to enroll (using script) via certreq.exe (or other way) certficate to Local Computer\Personal\Certficates store?

I want to configure PowerShell remoting on all my servers using HTTPS, and waiting for reboot server and user autoenrollment is not comfortable :)

 

Thanks for the articles. The are great!

thom schumacher

Can you give an example of what your cert looked like for the template name. 

 

I was only able to use the Dotted decimal number for the InitializefromTemplateName method. 

The example here: https://social.technet.microsoft.com/Forums/windowsserver/en-US/187698d0-5602-4301-9d0c-85e89d948ea2/user-powershell-to-get-the-template-used-to-create-a-certificate?forum=winserversecurity

Is how i got the name. but only the Dotted decimal number worked not the name of the template.

Vadims Podāns

This may indicate that certificate template does not belong to your AD forest or it was deleted.

den

Hi, absolutely awesome information! Your work saved my ass dealing with customer's PKI not allowing SAN the traditional way!

In the second script there's a typo in line "Write-Warning "Request ID: $(Request.RequestId)"", should contain additional $ between "$(" and "Request."

thanks you so much!!
 

Vadims Podāns

Thanks, I fixed the typo.

tjay

Hi Vadims, is it posible to use a policyserver/configstring with username and password with this script? I would like to use it with the CEP/CES services. I can't seem to find an object under $Request that i can use to define the parameters and $Request.PolicyServer only allows get.

Vadims Podāns

> is it posible to use a policyserver/configstring with username and password with this script?

No, it is not possible in the current version of the script. If you want to use custom enrollment server (not the one chosen by CA selection algorithm implemented in CertEnroll), then you have to save request manually and use ICertRequest COM interface to submit request and retrieve the certificate. Then use CertEnroll to install the response. Take a look at these blog posts: Introducing to certificate enrollment APIs (part 2) — creating offline requests and Introducing to certificate enrollment APIs (part 3) — certificate request submission and response installation. They cover all the information you need.

tjay

I found some code on GitHub that shows how you can do it and i came up with the below PowerShell code that seems to work.
https://github.com/pauldotknopf/WindowsSDK7-Samples/blob/master/security/x509%20certificate%20enrollment/CSharp/enrollWithIX509EnrollmentHelper/enrollWithIX509EnrollmentHelper.cs

They also used CX509EnrollmentHelper but i found it wasn't needed and I got access denied when i tried to use Initialize().

Do you see or know of any downside of doing it like this?

[string]$TemplateName="WebServer"
[string]$FriendlyName="somedomain.com"
$PolicyServer = New-Object -ComObject X509Enrollment.CX509EnrollmentPolicyWebService
$PolicyServer.Initialize("https://<CEPserverURL>/ADPolicyProvider_CEP_UsernamePassword/service.svc/CEP",$null,4,$true,3)
$PolicyServer.SetCredential(0,4,"dom\user","Passw0rd!")
$PolicyServer.LoadPolicy(0)
$Templates = $PolicyServer.GetTemplates()
$Template = $Templates.ItemByName($TemplateName)

$Request = New-Object -ComObject X509Enrollment.CX509Enrollment
$Request.InitializeFromTemplate(2,$PolicyServer,$Template)

Vadims Podāns

I totally missed the IX509EnrollmentPolicyWebService interface! The code looks legit to me.


Post your comment:

Please, solve this little equation and enter result below. Captcha