DSC IIS bindings and SSL certificates

This was a tricky one that really didn’t leave me with an ideal solution.

In using DSC, I want my compiled node configurations to be generic, like “webserver” instead of “webserver01”, in order for them to be re-used by VMs sharing the same characteristics, and to avoid duplicating information like IP addressing and VM names which has already been specified in the Terraform configuration for deployment.

At the same time, I want to be able to deploy a web server with a site on port 443 and a self-signed certificate which is created by DSC.

Combining these two ideas was not something I found I could accomplish with pre-existing modules.

I first looked to the xWebsiteAdministration DSC module, which contains the functions xWebsite among others. With this, I could use the following syntax:

xWebSite Admin { 
            Name = "Admin"
            PhysicalPath = "E:\inetpub\AdminSite"
            State = "Started"
            ApplicationPool = "Admin"
            Ensure = "Present"
            BindingInfo     = @(
                MSFT_xWebBindingInformation
                {
                    Protocol              = "HTTPS"
                    Port                  = 443
                    CertificateThumbprint = ""
                    CertificateStore      = "MY"
                })
            LogPath = "E:\inetpub\logs\AdminSite"
            DependsOn = "[File]E_AdminSite"
         }

Here, I want to pass in the thumbprint of my previously generated self-signed certificate. However, I don’t know it’s thumbprint, and it will be unique when I deploy this node configuration between a Web01 and a Web02 VM.

I tried resolving the thumbprint like this: CertificateThumbprint = (Get-ChildItem Cert::\LocalMachine\My | where {$_.Subject -like “*$($Node.ClientCode).com*”}), however that continued to give me DSC errors when it was attempting to apply, about a null reference.

When I came across this StackOverflow question it was clear why this wasn’t working. Since the compilation happens on Azure servers, of course they won’t have a certificate matching my subject name, and thus it can’t generate a thumbprint.

So instead I thought, I can just create a script that applies the certificate after the website has been created. Here is where I ran into additional problems:

  • If I create the xWebsite with HTTPS and 443 with no certificate, it errors
  • If I create the xWebsite with no binding information, it default assigns HTTP with port 80 (conflicting with another website that I have)
  • If I create the xWebsite with HTTP and port 8080 as a placeholder value, now I have IIS listening on ports I don’t actually want open
  • If I create the xWebsite with HTTP and port 8080 and then cleanup that binding afterwards with a Script, on the next run DSC is going to try and re-apply that binding, since I’ve effectively said it is my desired state

Ultimately what I was left with was creating a script that deployed the whole website, and not using xWebsite at all. Like I said, not ideal but it does work to meet my requirements.

Here’s the script that I’ve worked out:

Script WebsiteApps          
        {            
            # Must return a hashtable with at least one key            
            # named 'Result' of type String            
            GetScript = {            
                Return @{            
                    Result = [string]$(Get-ChildItem "Cert:\LocalMachine\My")            
                }            
            }            
            
            # Must return a boolean: $true or $false            
            TestScript = {            
                Import-Module WebAdministration
                # Grab the IP based on the interface name, which is previously set in DSC
                $ip1  = (get-netipaddress -addressfamily ipv4 -InterfaceAlias $($Using:Node.VLAN)).IPAddress
                # Find out if we've got anything bound on this IP for port 443
                $bindcheck = get-webbinding -name "Apps" -IPAddress $ip1 -Port 443
                $bindcheckwildcard = get-webbinding -name "Apps" | where-object { $_.BindingInformation -eq "*:80:"}
                # If site exists
                    if (Test-Path "IIS:\Sites\Apps")
                    { 
                        Write-Verbose "Apps site exists."
                        # if log file setting correct
                        if ((get-itemproperty "IIS:\Sites\Apps" -name logfile).directory -ieq "E:\inetpub\logs\AppsSite")
                           {
                               Write-Verbose "Log file is set correctly."
                                   # if IP bound on port 443
                                   if ($bindcheckhttps)
                                   { 
                                       Write-Verbose "443 is bound for Apps."
                                       #if SSL certificate bound
                                       if (Test-path "IIS:\SslBindings\$ip1!443")
                                       {
                                            Write-Verbose "SSL Certificate is bound for Apps"
                                            # wildcard binding check for Apps
                                            if (-not ($bindcheckwildcard)) {
                                                Write-Verbose "* binding does not exist for Apps."
                                                Return $true
                                            }
                                            else
                                            {
                                                Write-Verbose "* binding exists for Apps."
                                                Return $false
                                            }
                                       }
                                       else
                                       {
                                           Write-Verbose "SSL Certificate is NOT bound for Apps"
                                           Return $false
                                       }
                                   }
                                   else
                                   {
                                       Write-Verbose "IP not bound on 443 for Apps."
                                       Return $false
                                   }
                           }
                            else 
                            {
                               Write-Verbose "Log file path is not set correctly"
                               Return $false
                            } 
                   }
                   else
                   {
                       Write-Verbose "Apps site does not exist"
                       Return $false
                   }
                }    
            
            # Returns nothing            
            SetScript = {
                $computerName = $Env:Computername
                $domainName = $Env:UserDnsDomain
                
                $apps = Get-Item "IIS:\Sites\Apps"
                $ip1  = (get-netipaddress -addressfamily ipv4 -InterfaceAlias $($Using:Node.VLAN)).IPAddress
                $bindcheckhttps = get-webbinding -name "Apps"  -IPAddress $ip1 -Port 443
                $bindcheckwildcard = get-webbinding -name "Apps" | where-object { $_.BindingInformation -eq "*:80:"}

                 # If site not exists
                    if (-not (Test-Path "IIS:\Sites\Apps"))
                    { 
                        Write-Verbose "Creating Apps site"
                        New-Website -Name "Apps" -PhysicalPath E:\inetpub\AppsSite -ApplicationPool "Apps"
                    }
                     
                    # if port 443 not bound
                    if (-not ($bindcheckhttps))
                    { 
                        Write-Verbose "Binding port 443"
                        $apps = Get-Item "IIS:\Sites\Apps"
                        New-WebBinding -Name $apps.Name -protocol "https" -Port 443 -IPAddress $ip1
                    }
                    if ($bindcheckwildcard)
                    {
                        Write-Verbose "Removing wildcard binding for Apps"
                        get-webbinding -name "Apps" | where-object { $_.BindingInformation -eq "*:80:"} | Remove-Webbinding
                    } 
                            
                    #if SSL certificate not bound        
                    if (-not (Test-path "IIS:\SslBindings\$ip1!443"))
                        {
                            Write-Verbose "Binding SSL certificate"
                            Get-ChildItem cert:\LocalMachine\My | where-object { $_.Subject -match "CN\=$Computername\.$DomainName" } | select -First 1 | New-Item IIS:\SslBindings\$ip1!443
                        }
                    
                    # if log file setting correct
                    if (-not ((get-itemproperty "IIS:\Sites\Apps" -name logfile).directory -ieq "E:\inetpub\logs\AppsSite"))
                        {
                            Write-Verbose "Setting log file to the proper directory"
                            Set-ItemProperty "IIS:\Sites\Apps" -name logFile -value @{directory="E:\inetpub\logs\AppsSite"}
                        }
                } 
            DependsOn = "[xWebAppPool]Apps","[Script]GenerateSelfSignedCert"
        }

 

2 thoughts to “DSC IIS bindings and SSL certificates”

  1. Hey Jeff!
    This was a great help 3 years after publishing haha, I was able to use most of this when trying to come up with a solution for managing/updating SSL Cert bindings in IIS via DSC. We’re using azure keyvault extensions to push a new cert once it’s ready to all our web servers, but was looking for a way to actually update the bindings and some google-fu led me to your script. Basically recycled your logic and am working on finetuning it.

    Thought i’d share my test script that I was hung up on that you helped with in case others are looking for another example. Sorry for bad formatting.

    TestScript = {
    Import-Module WebAdministration

    # Grab all certs in the store that azure drops our cert in that matches our.custom.site wildcard.
    $Certs = (Get-ChildItem Cert:\LocalMachine\Webhosting |
    Where-Object {$_.Subject.Contains(‘*.agency.staffbridge.com’)} |
    Sort-Object -Descending {[System.DateTime]::Parse($_.GetExpirationDateString())})

    # Get the NEWEST certificate in store
    $NewCert = ($Certs | Select-Object -First 1)

    # Get the latest certificate in store
    $OldCert = ($certs | Select-Object -First 2 | Select-Object -Last 1)

    # Grab list of all bindings that would be *.agency.saffbridge.com
    $Bindings = Get-WebConfiguration `
    -Filter “/system.applicationHost/sites/site/bindings/binding[@protocol=’https’]” |
    Where {$_.bindinginformation -like “*agency.staffbridge.com*”}

    # Checks all site bindings to match against NEW thumbprint, we want all TRUE to be stored in $Results
    $results = @(
    foreach ($site in $bindings){
    $site.certificatehash -eq $NewCert.Thumbprint
    }
    )

    # If there are any FALSE in $Results, the test function should fail — returning false here
    if($certs.count -gt 1){

    if ($results -contains $true){
    Write-Verbose “There are site bindings that are out of date”
    Return $false

    }else{
    Write-Verbose “All site bindings have the most recent Cert”
    return $true
    }
    }
    else{
    Write-Verbose “Only one Cert to compare against, no action”
    return $true
    }
    }

    From there I have a pretty simple set script, haven’t fully tested but it came from another script that I did the same thing sans DSC, so shouldn’t be too far off:

    SetScript = {

    #Replaces any site binding with the old thumbpint
    Get-WebConfiguration -Filter “/system.applicationHost/sites/site/bindings/binding[@protocol=’https’]” |
    Where-Object {$_.certificateHash -eq $oldcert.Thumbprint} |
    % {
    Write-Verbose “Updating Certificate for $($_.bindinginformation) ”
    $_.RemoveSslCertificate()
    $_.AddSslCertificate($NewCert.Thumbprint, ‘WebHosting’)
    }
    }

    1. That’s good stuff Ben, thanks for sharing!

      I didn’t know about the Azure KeyVault VM Extension, looks really useful. Are you programmatically creating the Managed Identity for your VMs for that?

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.