Automate Exchange installation and configuration with DSC Part 1:Pull server

I’m currently involved in an Exchange 2013 project. One of the goals is to get things automated and to prevent configuration drifts later in the operating phase.

I thougt this would be a great chance to have a look into Powershell Desired State Configuration (DSC). And indeed: I really like it! Especially in combination with xExchange module authored by Mike Hendrickson, Jason Walker, Michael Greene.

I’m not going to write another blog post about the module. Mike’s posts are very good and there is nothing to add. This is the start of a serie around DSC and my personal journey in accomplishing the goals that have been set:

Part 1: Pull server (setup and querying node status)

Part 2: Configure node’s LCM in a bulk operation reading GUID from AD and using individual certificates

Part 3: Automate creation and publishing to pull server of configurations reading Configuration and Environment Data files

To create a pull server for DSC you need to make your first decision: What kind of pull server you are going to install? You can choose one of the following options: HTTP, HTTPS and SMB.

I picked HTTPS as I wanted to use the PSDSCComplianceServer component, which is anyways HTTP based. The server itself is based on Windows Server 2012 R2.

Prerequisites

As I picked Windows Server 2012 R2 as OS WMF 4.0 is already built-in. I picked the example from Technet article here and started to create my configuration for the pull server:

configuration CreatePullServer
{
    param 
    (
        [string[]]$NodeName = 'localhost',
        [ValidateNotNullOrEmpty()]
        [string] $certificateThumbPrint,
        [ValidateNotNullOrEmpty()]
        [string] $PSDSCPullServer
    )
    Import-DSCResource -ModuleName xPSDesiredStateConfiguration
    Node $NodeName
    {
        #configure LCM
        LocalConfigurationManager
        {
            RebootNodeIfNeeded = $True                 #allow a reboot or not
            ConfigurationMode = "ApplyAndAutoCorrect"  #ApplyOnly(does nothing), ApplyAndMonitor(logs only) or ApplyAndAutoCorrect(takes action based on config)
            ConfigurationModeFrequencyMins = 15        #time in minutes for checking
        }
        WindowsFeature IISFeature
        {
            Ensure = "Present"
            Name   = "Web-Server"
        }
        WindowsFeature IISMgmtFeature
        {
            Ensure = "Present"
            Name   = "Web-Mgmt-Console"
            DependsOn = "[WindowsFeature]IISFeature"
        }
        WindowsFeature DSCServiceFeature
        {
            Ensure = "Present"
            Name   = "DSC-Service"
            DependsOn = @("[WindowsFeature]IISMgmtFeature","[WindowsFeature]IISFeature")
        }
        xDscWebService PSDSCPullServer
        {
            Ensure                  = "Present"
            EndpointName            = $PSDSCPullServer
            Port                    = 8080
            PhysicalPath            = "$env:SystemDrive\inetpub\wwwroot\PSDSCPullServer"
            CertificateThumbPrint   =  $certificateThumbPrint         
            ModulePath              = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Modules"
            ConfigurationPath       = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Configuration"            
            State                   = "Started"
            DependsOn               = "[WindowsFeature]DSCServiceFeature"                        
        }
        xDscWebService PSDSCComplianceServer
        {
            Ensure                  = "Present"
            EndpointName            = "PSDSCComplianceServer"
            Port                    = 9080
            PhysicalPath            = "$env:SystemDrive\inetpub\wwwroot\PSDSCComplianceServer"
            CertificateThumbPrint   = "AllowUnencryptedTraffic"
            State                   = "Started"
            IsComplianceServer      = $true
            DependsOn               = @("[WindowsFeature]DSCServiceFeature","[xDSCWebService]PSDSCPullServer")
        }
    }
}

Now you can create the MOF file:

CreatePullServer -certificateThumbPrint '476C7CA2756043ABD0035864564B1389E7539BBF' -PSDSCPullServer 'fabpullsrv.fabrikam.local' -Verbose -Wait -OutputPath C:\DSC\Config

Push the configuration to the node:

Start-DscConfiguration -Verbose -Wait -Path C:\DSC\Config -ComputerName 'localhost' -Force

Don’t forget to configure the Local Configuration Manager (LCM):

Set-DscLocalConfigurationManager -Path C:\DSC\Config -ComputerName 'localhost'

Now you should have a working pull server. Therefore I started to prepare the modules I would need to have them automatically deployed via the pull server. There are several articles how to do so. Here are some examples:

After a while I wanted to query the status of the nodes I have configured. You can do this by querying the PSDSCComplianceServer component on the server. There is a function provided called QueryNodeInformation. You can find the function on the following pages:

On both you can also find the description for the status codes.

Sadly it didn’t work when I tried. I only got the following error:

Compliance_01

Invoke-WebRequest : HTTP Error 401.2 – Unauthorized
You are not authorized to view this page due to invalid authentication headers.Most likely causes:
No authentication protocol (including anonymous) is selected in IIS.
Only integrated authentication is enabled, and a client browser was used that does not support integrated authentication.
Integrated authentication is enabled and the request was sent through a proxy that changed the authentication headers before they reach the Web server.
The Web server is not configured for anonymous access and a required authorization header was not received.
The “configuration/system.webServer/authorization” configuration section may be explicitly denying the user access.
Things you can try:
Verify the authentication setting for the resource and then try requesting the resource using that authentication method.
Verify that the client browser supports Integrated authentication.
Verify that the request is not going through a proxy when Integrated authentication is used.
Verify that the user is not explicitly denied access in the “configuration/system.webServer/authorization” configuration section.
Create a tracing rule to track failed requests for this HTTP status code. For more information about creating a tracing rule for failed requests, click here.
Detailed Error Information:
Module   IIS Web Core
Notification   AuthenticateRequest
Handler   svc-Integrated-4.0
Error Code   0x80070005
Requested URL   http://fabpullsrv.fabrikam.local:9080/PSDSCComplianceServer.svc/Status
Physical Path   C:\inetpub\wwwroot\PSDSCComplianceServer\PSDSCComplianceServer.svc\Status
Logon Method   Not yet determined
Logon User   Not yet determined
More Information:
This error occurs when the WWW-Authenticate header sent to the Web server is not supported by the server configuration. Check the authentication method for
the resource, and verify which authentication method the client used. The error occurs when the authentication methods are different. To determine which type
of authentication the client is using, check the authentication settings for the client.
View more information »
Microsoft Knowledge Base Articles:
907273
253667

The error clearly states that there is a problem with the authentication. So I compared the configuration with the site of the pull server:

Compliance_02

On the pull server is Anonymous Authentication enabled, while on the PSDSCComplianceServer component it was disabled:

Compliance_03

After I enabled Anonymous Authentication I got a different error:

Compliance_04

I compared the web.config files from both sites. There is also a hint in the comments section of the MSDN blog post:

Compliance_05The solution for me was to add the module of type ‘Microsoft.Powershell.DesiredStateConfiguration.PullServer.AuthenticationPlugin’ in the web.config and enable Anonymous Authentication:

Compliance_06

Another way is to install and enable WindowsAuthentication on the IIS. The configuration for the first solution, which is the complex one, was to add to the configuration for the pull server some lines:

I used the DSC Script resource and parsed the web.config file to check the authentication and the module. It looks like this:

configuration CreatePullServer
{
    param 
    (
        [string[]]$NodeName = 'localhost',
        [ValidateNotNullOrEmpty()]
        [string] $certificateThumbPrint,
        [ValidateNotNullOrEmpty()]
        [string] $PSDSCPullServer
    )
    Import-DSCResource -ModuleName xPSDesiredStateConfiguration
    Node $NodeName
    {
        ...
        Script TweakPSDSCComplianceServerWebConfig 
        {
            SetScript = {
              $webconfig = "$env:SystemDrive\inetpub\wwwroot\PSDSCComplianceServer\web.config"
              $xml = [xml](Get-Content $webconfig)
              $root = $xml.get_DocumentElement()
              $WebServer=$root.SelectNodes("//system.webServer")
              $currentDate = (get-date).tostring("MM_dd_yyyy-hh_mm_ss") 
              $backup = $webconfig + "_$currentDate" + ".bak"
              #check auth
              If ($WebServer.security.authentication.anonymousAuthentication.enabled -ne 'true') {
                $WebServer.security.authentication.anonymousAuthentication.SetAttribute('enabled','true')
                #first create a backup
                $xml.Save($backup)
                $xml.Save($webconfig)
              }
              #check module
              If ($WebServer.modules -eq $null) {
                #first create a backup
                $xml.Save($backup)
                #create modules element
                $modules = $xml.CreateElement('modules')
                #create add element
                $add = $xml.CreateElement('add')
                $add.SetAttribute('name','AuthenticationModule')
                $add.SetAttribute('type','Microsoft.Powershell.DesiredStateConfiguration.PullServer.AuthenticationPlugin')
                #append modules
                $xml.SelectSingleNode('//system.webServer').AppendChild($modules) | Out-Null
                #append add to modules
                $xml.SelectSingleNode('//system.webServer/modules').AppendChild($add) | Out-Null
                $xml.Save($webconfig) 
              }
              Else{
                #get modules
                $modules = @($root.SelectNodes("//system.webServer/modules/add"))
                [int]$byname=($modules | ?{$_.name -eq 'AuthenticationModule'} | Measure-Object).Count
                [int]$bytype= ($modules | ?{$_.type -eq 'Microsoft.Powershell.DesiredStateConfiguration.PullServer.AuthenticationPlugin'} | Measure-Object).Count
                If ($bytype -gt '0') {
                  break
                }
                ElseIf ($byname -gt '0') {
                ForEach ($module in $modules) {
                  If ($module.name -eq 'AuthenticationModule') {
                    If ($module.type -ne 'Microsoft.Powershell.DesiredStateConfiguration.PullServer.AuthenticationPlugin') {
                      #first create a backup
                      $xml.Save($backup)
                      $module.SetAttribute('type','Microsoft.Powershell.DesiredStateConfiguration.PullServer.AuthenticationPlugin')
                      $xml.Save($webconfig)
                    }
                  }
                }
              }
              Else {
                #first create a backup
                $xml.Save($backup)
                #create add element
                $add = $xml.CreateElement('add')
                $add.SetAttribute('name','AuthenticationModule')
                $add.SetAttribute('type','Microsoft.Powershell.DesiredStateConfiguration.PullServer.AuthenticationPlugin')
                #append add to modules
                $xml.SelectSingleNode('//system.webServer/modules').AppendChild($add) | Out-Null
                $xml.Save($webconfig)
              }
             }    
            }
            TestScript = {
              $webconfig = "$env:SystemDrive\inetpub\wwwroot\PSDSCComplianceServer\web.config"
              $xml = [xml](Get-Content $webconfig)
              $root = $xml.get_DocumentElement()
              $WebServer=$root.SelectNodes("//system.webServer")
              #check auth
              If ($WebServer.security.authentication.anonymousAuthentication.enabled -ne 'true') {
                return @($False)
                break;    
              }
              #get modules
              $modules = @($root.SelectNodes("//system.webServer/modules/add"))
              [int]$byname=($modules | ?{$_.name -eq 'AuthenticationModule'} | Measure-Object).Count #| Out-Null
              [int]$bytype= ($modules | ?{$_.type -eq 'Microsoft.Powershell.DesiredStateConfiguration.PullServer.AuthenticationPlugin'} | Measure-Object).Count
              If ($bytype -gt '0') {
                return @($True)
              }
              ElseIf ($byname -gt '0') {
                ForEach ($module in $modules) {
                  If ($module.name -eq 'AuthenticationModule') {
                    If ($module.type -eq 'Microsoft.Powershell.DesiredStateConfiguration.PullServer.AuthenticationPlugin') {
                      return @($True)
                    }
                  }
                }
              }
              return @($False)
            }
            GetScript = {
                    @{
                        TestScript = $TestScript
                        SetScript  = $SetScript
                        GetScript  = $GetScript
                    }            
            }
            DependsOn              = "[xDscWebService]PSDSCComplianceServer"
        }
    }
}

Or the easy way using DSC WindowsFeature resource (just install WindowsAuthentication on IIS):

configuration CreatePullServer
{
    param 
    (
        [string[]]$NodeName = 'localhost',
        [ValidateNotNullOrEmpty()]
        [string] $certificateThumbPrint,
        [ValidateNotNullOrEmpty()]
        [string] $PSDSCPullServer
    )
    Import-DSCResource -ModuleName xPSDesiredStateConfiguration
    Node $NodeName
    {
        ...
        WindowsFeature IISWindowsAuth
        {
            Ensure = "Present"
            Name   = "Web-Windows-Auth"
            DependsOn = "[WindowsFeature]IISFeature"
        }
        WindowsFeature DSCServiceFeature
        {
            Ensure = "Present"
            Name   = "DSC-Service"
            DependsOn = @("[WindowsFeature]IISMgmtFeature","[WindowsFeature]IISFeature","[WindowsFeature]IISWindowsAuth")
        }
    }
}

After this I was able to query the status:

Compliance_07

Now the only thing what bothered me was the fact that you only get IP address and the status code. But also here a solution already exist: Ashley McGlone and Jonathan Walz posted it in the comments section of the MSDN blog post. I just put everything together in a script, which could be downloaded here.

When you run the script you will get the IP address resolved and also the description of the status code:

Compliance_08

The script accepts 2 parameters:

  • PullServer: The DSC pull server name
  • Port: The port on which the PSDSCComplianceServer component on the server is listening(default is 9080)

This was the first part. In the next I will cover how I configured the nodes LCM dynamically using AD’s object GUID and individual certificates.