Change the Friendly Name of a Cert with PowerShell

While working on adding a new feature in the certificate request DSC resource, I came across this handy little trick: You can change the Friendly Name of a certificate using PowerShell.

All you need to do is identify the certificate using Get-ChildItem and then assign the new FriendlyName to it.

ss_cert_changefriendlyname

ss_cert_changefriendlynamecertlm

Sometimes PowerShell still surprises me at how easy it can make things. I didn’t need to search help or the internet – just typed it in and it worked!

Advertisements

Export a Base-64 x.509 Cert using PowerShell on Windows 7

Exporting a Base-64 Encoded x.509 certificate using PowerShell is trivial if you have the Export-Certificate cmdlet available. However, many of the nodes I work with are Windows 7 which unfortunately doesn’t include these cmdlets. Therefore I needed an alternate method of exporting these Base-64 encoded x.509 certificates from these nodes.

So I came up with this little snippet of code:

Hope someone finds it useful.

Install an SSL WS-Management Listener with GPO

Introduction

One of the things I like to do when ever I install a new server is to enable an HTTPS/SSL WS-Management Listener on it so that I can disable the more insecure HTTP WS-Management listener. For more information on WS-Management Listeners see this MSDN article.

There are many benefits to using a secure HTTPS/SSL WS-Management Listener:

  1. Security – the communication channel between client and server is encrypted using SSL.
  2. Authentication – the server is authenticated to the client so you can trust you’re talking to the server you think you’re talking to.

The downside to this is that you need a valid and trusted server authentication certificate on the server to enable this – but if you have a PKI then this is no big deal as you’ll probably have certificate autoenrollment enabled. If you don’t have a PKI, then you should look into installing one.

Installing these listeners manually is fairly straight forward and requires only a single command:

New-WSManInstance -ResourceURI winrm/config/Listener `
-SelectorSet @{Address='*';Transport="HTTPS"} -ValueSet @{Hostname="SERVER01.CONTOSO.COM";CertificateThumbprint="09 49 93 24 53 81 32 16 b7 44 8b 47 ca af 56 3a ef 9f 10 2d"}

All you need to do is enter the appropriate hostname and certificate thumbprint for a server authentication certificate that exists on the server. But who wants to do this manually, right?

Installing with GPO

The slightly tricky part of installing this automatically onto your servers with a GPO is detecting which certificate to use. The certificate must:

  • exist in the local computer personal certificate store.
  • have an Extended Key Usage of Server Authentication.
  • be issued by a CA trusted by any client connecting to the server.
  • The Subject must contain a Common Name that contains either the FQDN computer name or the flat computer name (e.g. CN=SERVER1.CONTOSO.COM or CN=SERVER1).

It is easy to ensure a certificate meets these criteria by using a GPO enabling certificate autoenrollment for computer certificates and that the Computer autoenrollment certificate template will create certificates meeting these requirements. See this page for some basic information on certificate autoenrollment. There are some much more detailed instructions on this around the net if you’re happy to search.

Once you’ve ensured all your computers have been issued such a certificate you would normally need to lookup the certificate on each computer and get the certificate thumbprint and execute the command I showed earlier. This would be a complete pain on any more than 10 computers, and probably pure insanity at a lot of facilities.

So, I put together the following PowerShell commands that could be used to automatically pull the certificate thumbprint for an appropriate certificate:

[String] $Issuer = 'CN=CONTOSO.COM Issuing CA, DC=CONTOSO, DC=COM'
[String] $HostName = [System.Net.Dns]::GetHostByName($ENV:computerName).Hostname
[String] $Thumbprint = (get-childitem -Path Cert:\localmachine\my | Where-Object { 
		        ($_.Extensions.EnhancedKeyUsages.FriendlyName -contains 'Server Authentication') -and
		        ($_.IssuerName.Name -eq $Issuer) -and
		        ($HostName -in $_.DNSNameList.Unicode) -and
                ($_.Subject -eq "CN=$HostName") } | Select-Object -First 1
            ).Thumbprint

All you’d need to set was the Issuer to whatever the Distinguished Name of your issuing CA is – which should be the same for all computers. This simplifies things a lot because the same code could be run on any computer and should always return the correct thumbprint.

The next step was to put it into a script where you could just pass the Distinguished Name of the issuing CA as a parameters. I did this and also added some other optional parameters and uploaded the result to Microsoft Script Center. So you can download this script and put it into a GPO Startup Script:

Installing an HTTPS WS-Management Listener with GPO

Installing an HTTPS WS-Management Listener with GPO

The script is actually a little bit smarter than the above command. If a certificate with a subject can’t be found that matches the FQDN of the computer it will automatically look for one that just uses the flat computer name. You can control this behavior by setting the DNSNameType parameter.

There are some other optional parameters that control other the behavior of the script as well:

DNSNameType

The allowed DNS Name types that will be used to find a matching certificate. If set to FQDN then the script will only try to find a certificate with a subject matching the FQDN of the computer. If set to ComputerName it will only match on the computer name of the computer. By default this is set to Both which will try to match on FQDN first and then ComputerName if it can’t find one matching the FQDN.

MatchAlternate

The certificate found must also have an alternate subject name containing the DNS name found in
the subject as well. This places additional restrictions on the certificate that is used, but is not usually required. This defaults to False.

Port

This parameter lets you specify an alternate port to install the HTTPS/SSL listener on. If you don’t specify it, it will use the default port of 5986.

Don’t Forget your Firewall

It is important to remember that by default this listener is installed onto port 5986, which is not usually open inbound. So you’ll want to add a firewall rule to ensure this port can be reached – another job for GPO. You could even add this setting into the GPO that installs the HTTPS/SSL listener.

Installing with DSC

In theory it should be possible to adapt this code to run in a DSC Script Resource. I haven’t tried this yet, but you can be assured that I will fairly soon. If there was some interest in this I could convert it into a proper DSC Resource (unless there was one already – I haven’t checked). If you are interested, let me know.

Links

If you want to make a copy of the repository, you’ll find it here:

https://github.com/PlagueHO/WSManGPOTools

Right, that is me out for another Sunday. Thanks for reading.

Installing a Two-Tier PKI using nothing but Desired State Configuration – Part 2

Continuing on from yesterday, the goal of this series is show how it is possible to install a two-tier Active Directory Certificate Services environment using only Desired State Configuration. In Part 1, I covered the basic DSC setup and requirements, the AllNodes hash table and the first part of the Root CA configuration script.

Other Parts in this Series

Installing a Two-Tier PKI using nothing but Desired State Configuration – Part 1

Lets get going then!

Step 2: Installing the Subordinate CA

In this configuration we’ll need both Local Credentials for installing the Web Enrollment feature and Domain Credentials for joining the Sub CA to the domain and for registering the CA in AD:

	Node $AllNodes.NodeName {
		# Assemble the Local Admin Credentials
		If ($Node.LocalAdminPassword) {
			[PSCredential]$LocalAdminCredential = New-Object System.Management.Automation.PSCredential ("Administrator", (ConvertTo-SecureString $Node.LocalAdminPassword -AsPlainText -Force))
		}
		If ($Node.DomainAdminPassword) {
			[PSCredential]$DomainAdminCredential = New-Object System.Management.Automation.PSCredential ("$($Node.DomainName)\Administrator", (ConvertTo-SecureString $Node.DomainAdminPassword -AsPlainText -Force))
		}

Just like the Root CA the ADCS Certificate Authority and the ADCS Web Enrollment features need to be installed. But I’m also going to install the Online Responder service as well – you of course don’t need to. I really should configure the CRLPublicationURLs node property as well to make use of this Online Responder, but I’m sure you can figure that part out.

		# Install the RSAT PowerShell Module which is required by the xWaitForResource
		WindowsFeature RSATADPowerShell
		{ 
			Ensure = "Present" 
			Name = "RSAT-AD-PowerShell" 
		} 

		# Install the CA Service
		WindowsFeature ADCSCA {
			Name = 'ADCS-Cert-Authority'
			Ensure = 'Present'
			DependsOn = "[WindowsFeature]RSATADPowerShell" 
		}

		# Install the Web Enrollment Service
		WindowsFeature WebEnrollmentCA {
			Name = 'ADCS-Web-Enrollment'
			Ensure = 'Present'
			DependsOn = "[WindowsFeature]ADCSCA"
		}

		# Install the Online Responder Service
		WindowsFeature OnlineResponderCA {
			Name = 'ADCS-Online-Cert'
			Ensure = 'Present'
			DependsOn = "[WindowsFeature]WebEnrollmentCA"
		}

You might have noticed that we’re also installing the RSAT-AD-PowerShell. This is required by the xWaitForADDomain DSC resource. If you don’t install this feature the domain will never be detected and the DSC Script will progress no further (I found this out the hard way).

On the agenda next, this machine needs to be joined to the domain. It is important to check the domain is up before trying to join it. In my case I was also creating the DC’s (by DSC of course) at the same time as the CA’s so sometimes there was a long wait for the Domain to come up (which is why the large retry count):

		# Wait for the Domain to be available so we can join it.
		xWaitForADDomain DscDomainWait
		{
			DomainName = $Node.DomainName
			DomainUserCredential = $DomainAdminCredential 
			RetryCount = 100 
			RetryIntervalSec = 10 
			DependsOn = "[WindowsFeature]OnlineResponderCA" 
		}

		# Join this Server to the Domain so that it can be an Enterprise CA.
		xComputer JoinDomain 
		{ 
			Name          = $Node.NodeName
			DomainName    = $Node.DomainName
			Credential    = $DomainAdminCredential 
			DependsOn = "[xWaitForADDomain]DscDomainWait" 
		} 

The next step is to create a CAPolicy.inf file, but this file is slightly different from the one created on the Root CA. The process is the same though:

		# Create the CAPolicy.inf file that sets basic parameters for certificate issuance for this CA.
		File CAPolicy
		{
			Ensure = 'Present'
			DestinationPath = 'C:\Windows\CAPolicy.inf'
			Contents = "[Version]`r`n Signature= `"$Windows NT$`"`r`n[Certsrv_Server]`r`n RenewalKeyLength=2048`r`n RenewalValidityPeriod=Years`r`n RenewalValidityPeriodUnits=10`r`n LoadDefaultTemplates=1`r`n AlternateSignatureAlgorithm=1`r`n"
			Type = 'File'
			DependsOn = '[xComputer]JoinDomain'
		}

Easy enough so far. What I did next was create a CertEnroll folder (c:\windows\System32\CertSrv\CertEnroll) where the Root CA certificate needed to be put. The Web Enrollment Service would have created this too but I can’t configure this service until later. So I’m going to create it manually:

		# Make a CertEnroll folder to put the Root CA certificate into.
		# The CA Web Enrollment server would also create this but we need it now.
		File CertEnrollFolder
		{
			Ensure = 'Present'
			DestinationPath = 'C:\Windows\System32\CertSrv\CertEnroll'
			Type = 'Directory'
			DependsOn = '[File]CAPolicy'
		}

Next up I wanted to download the Root CA Cert to this Sub CA. Strictly this isn’t required till later but I was basically emulating the steps in this document.

The important thing to remember here though is that we need to ensure the Root CA DSC has reached the point where the Root CA certificate and Certificate Revocation List (CRL) is produced and available to us. So this is where we use the new PowerShell DSC 5.0 WaitFor resource:

		# Wait for the RootCA Web Enrollment to complete so we can grab the Root CA certificate
		# file.
		WaitForAny RootCA
		{
			ResourceName = '[xADCSWebEnrollment]ConfigWebEnrollment'
			NodeName = $Node.RootCAName
			RetryIntervalSec = 30
			RetryCount = 30
			DependsOn = "[File]CertEnrollFolder"
		}

		# Download the Root CA certificate file.
		xRemoteFile DownloadRootCACRTFile
		{
			DestinationPath = "C:\Windows\System32\CertSrv\CertEnroll\$($Node.RootCAName)_$($Node.RootCACommonName).crt"
			Uri = "http://$($Node.RootCAName)/CertEnroll/$($Node.RootCAName)_$($Node.RootCACommonName).crt"
			DependsOn = '[WaitForAny]RootCA'
		}

		# Download the Root CA certificate revocation list.
		xRemoteFile DownloadRootCACRLFile
		{
			DestinationPath = "C:\Windows\System32\CertSrv\CertEnroll\$($Node.RootCACommonName).crl"
			Uri = "http://$($Node.RootCAName)/CertEnroll/$($Node.RootCACommonName).crl"
			DependsOn = '[xRemoteFile]DownloadRootCACRTFile'
		}

Note: using HTTP to copy files between the Root CA and the Sub CA’s is not strictly recommended by Microsoft when installing a two-tier PKI because that means the Root CA system has to be connected to the network. Because the Root CA and Sub CA DSC scripts need the machines to directly interact there isn’t any way around this that I can see. But if you were using this in a production environment you could put the Root CA machine onto an isolated virtual network consisting of the Root CA and Sub CA machines only. It is not a perfect solution but it should be reasonable for most situations. The Root CA can still be taken off line and removed after the Sub CA’s have been created.

Following this the Root CA Certificate and CRL can be imported into the local machine root certificate store and also the Active Directory domain. This is done in a single script resource:

		# Install the Root CA Certificate to the LocalMachine Root Store and DS
		Script InstallRootCACert
		{
			PSDSCRunAsCredential = $DomainAdminCredential
			SetScript = {
				Write-Verbose "Registering the Root CA Certificate C:\Windows\System32\CertSrv\CertEnroll\$($Using:Node.RootCAName)_$($Using:Node.RootCACommonName).crt in DS..."
				"$($ENV:SystemRoot)\system32\certutil.exe" -f -dspublish "C:\Windows\System32\CertSrv\CertEnroll\$($Using:Node.RootCAName)_$($Using:Node.RootCACommonName).crt" RootCA
				Write-Verbose "Registering the Root CA CRL C:\Windows\System32\CertSrv\CertEnroll\$($Node.RootCACommonName).crl in DS..."
				"$($ENV:SystemRoot)\system32\certutil.exe" -f -dspublish "C:\Windows\System32\CertSrv\CertEnroll\$($Node.RootCACommonName).crl" "$($Using:Node.RootCAName)"
				Write-Verbose "Installing the Root CA Certificate C:\Windows\System32\CertSrv\CertEnroll\$($Using:Node.RootCAName)_$($Using:Node.RootCACommonName).crt..."
				"$($ENV:SystemRoot)\system32\certutil.exe" -addstore -f root "C:\Windows\System32\CertSrv\CertEnroll\$($Using:Node.RootCAName)_$($Using:Node.RootCACommonName).crt"
				Write-Verbose "Installing the Root CA CRL C:\Windows\System32\CertSrv\CertEnroll\$($Node.RootCACommonName).crl..."
				"$($ENV:SystemRoot)\system32\certutil.exe" -addstore -f root "C:\Windows\System32\CertSrv\CertEnroll\$($Node.RootCACommonName).crl"
			}
			GetScript = {
				Return @{
					Installed = ((Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object -FilterScript { ($_.Subject -Like "CN=$($Using:Node.RootCACommonName),*") -and ($_.Issuer -Like "CN=$($Using:Node.RootCACommonName),*") } ).Count -EQ 0)
				}
			}
			TestScript = { 
				If ((Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object -FilterScript { ($_.Subject -Like "CN=$($Using:Node.RootCACommonName),*") -and ($_.Issuer -Like "CN=$($Using:Node.RootCACommonName),*") } ).Count -EQ 0) {
					Write-Verbose "Root CA Certificate Needs to be installed..."
					Return $False
				}
				Return $True
			}
			DependsOn = '[xRemoteFile]DownloadRootCACRTFile'
		}

I’d actually prefer to break the above code into for separate resources and detect if each one has occurred (and I might do for a later version), but this configuration is extremely large as it is.

Notice here we also used another PowerShell DSC 5.0 feature, the PSDSCRunAsCredential parameter. This parameter is available in all DSC Resources and allows us to specify an alternate credential to run this DSC Resource as. By default a DSC Resource is run as NT AUTHORITY/SYSTEM, which is usually OK, but in this case some of the commands write certificates into DS and therefore need to be run under a Domain Admin account.

Onwards: It is now time to configure the AD CS Certificate Authority and Web Enrollment. Except this time the Certificate Authority configuration will produce a certificate request (REQ) that has to be issued by our Root CA. So what I did was ensure the REQ file is put into the CertEnroll folder – this should make it accessible by in the http:\\SA_SUBCA\CertEnroll\ web site.

		# Configure the Sub CA which will create the Certificate REQ file that Root CA will use
		# to issue a certificate for this Sub CA.
		xADCSCertificationAuthority ConfigCA
		{
			Ensure = 'Present'
			Credential = $DomainAdminCredential
			CAType = 'EnterpriseSubordinateCA'
			CACommonName = $Node.CACommonName
			CADistinguishedNameSuffix = $Node.CADistinguishedNameSuffix
			OverwriteExistingCAinDS  = $True
			OutputCertRequestFile = "c:\windows\system32\certsrv\certenroll\$($Node.NodeName).req"
			DependsOn = '[Script]InstallRootCACert'
		}

		# Configure the Web Enrollment Feature
		xADCSWebEnrollment ConfigWebEnrollment {
			Ensure = 'Present'
			Name = 'ConfigWebEnrollment'
			Credential = $LocalAdminCredential
			DependsOn = '[xADCSCertificationAuthority]ConfigCA'
		}

Seems simple enough – except one small problem. By default IIS doesn’t include REQ files as supported mime types so the file can’t be downloaded. To get around this we need to add REQ as a supported mime type. Unfortunately there is no DSC resource to do this so it’s time to resort to the Script resource:

		# Set the IIS Mime Type to allow the REQ request to be downloaded by the Root CA
		Script SetREQMimeType
		{
			SetScript = {
				Add-WebConfigurationProperty -PSPath IIS:\ -Filter //staticContent -Name "." -Value @{fileExtension='.req';mimeType='application/pkcs10'}
			}
			GetScript = {
				Return @{
					'MimeType' = ((Get-WebConfigurationProperty -Filter "//staticContent/mimeMap[@fileExtension='.req']" -PSPath IIS:\ -Name *).mimeType);
				}
			}
			TestScript = { 
				If (-not (Get-WebConfigurationProperty -Filter "//staticContent/mimeMap[@fileExtension='.req']" -PSPath IIS:\ -Name *)) {
					# Mime type is not set
					Return $False
				}
				# Mime Type is already set
				Return $True
			}
			DependsOn = '[xADCSWebEnrollment]ConfigWebEnrollment'
		}

Right, now an issuing certificate needs to be issued to this Sub CA by the Root CA using the REQ that has been created in the CertEnroll virtual folder on the Sub CA. To do this we need to go back to the Root CA DSC script and continue on with it.

Step 3: Issuing the Sub CA certificate on the Root CA

This is the second component of the Root CA DSC configuration. It is a bit more complicated than the first part because it may need to be run more than once – once for each Sub CA that is being created. Therefore the whole part is wrapped in foreach loop. This is also the purpose of the SubCAs array property of the AllNodes object. Each Sub CA that will be bought up should be in the list:

SubCAs=@('SA_SUBCA1','SA_SUBCA2','SA_SUBCA3')

So now that we’ve got that covered we can start adding to the Root CA DSC Configuration. So here’s the start of that foreach loop I was talking about:

        # Generate Issuing certificates for any SubCAs
		Foreach ($SubCA in $Node.SubCAs) {

The first thing to do is wait for the Sub CA to complete creation of the REQ file and download it. So once again we use the WaitForAny resource. Also note the use of the $SubCA variable that is defined by the foreach loop:

			# Wait for SubCA to generate REQ
			WaitForAny "WaitForSubCA_$SubCA"
			{
				ResourceName = '[xADCSCertificationAuthority]ConfigCA'
				NodeName = $SubCA
				RetryIntervalSec = 30
				RetryCount = 30
				DependsOn = '[Script]ADCSAdvConfig'
			}

			# Download the REQ from the SubCA
			xRemoteFile "DownloadSubCA_$SubCA"
			{
				DestinationPath = "C:\Windows\System32\CertSrv\CertEnroll\$SubCA.req"
				Uri = "http://$SubCA/CertEnroll/$SubCA.req"
				DependsOn = "[WaitForAny]WaitForSubCA_$SubCA"
			}

To make things simple I just downloaded the REQ to the CertEnroll folder of this Root CA. Now, things got a little bit tough here. There is no DSC Resource or even PowerShell modules for issuing a certificate from the REQ. We have to fall back to using the DSC Script resource and the CertReq.exe and CertUtil.exe tools. This is a little bit fiddly and reminds me why I love PowerShell’s object based output. I won’t go into detail of what is going on here, but if you want me to expand on it let me know.

			# Generate the Issuing Certificate from the REQ
			Script "IssueCert_$SubCA"
			{
				SetScript = {
					Write-Verbose "Submitting C:\Windows\System32\CertSrv\CertEnroll\$Using:SubCA.req to $($Using:Node.CACommonName)"
					[String]$RequestResult =  "$($ENV:SystemRoot)\System32\Certreq.exe" -Config ".\$($Using:Node.CACommonName)" -Submit "C:\Windows\System32\CertSrv\CertEnroll\$Using:SubCA.req"
					$Matches = [Regex]::Match($RequestResult, 'RequestId:\s([0-9]*)')
					If ($Matches.Groups.Count -lt 2) {
						Write-Verbose "Error getting Request ID from SubCA certificate submission."
						Throw "Error getting Request ID from SubCA certificate submission."
					}
					[int]$RequestId = $Matches.Groups[1].Value
					Write-Verbose "Issuing $RequestId in $($Using:Node.CACommonName)"
					[String]$SubmitResult =  "$($ENV:SystemRoot)\System32\CertUtil.exe" -Resubmit $RequestId
					If ($SubmitResult -notlike 'Certificate issued.*') {
						Write-Verbose "Unexpected result issuing SubCA request."
						Throw "Unexpected result issuing SubCA request."
					}
					Write-Verbose "Retrieving C:\Windows\System32\CertSrv\CertEnroll\$Using:SubCA.req from $($Using:Node.CACommonName)"
					[String]$RetrieveResult =  "$($ENV:SystemRoot)\System32\Certreq.exe" -Config ".\$($Using:Node.CACommonName)" -Retrieve $RequestId "C:\Windows\System32\CertSrv\CertEnroll\$Using:SubCA.crt"
				}
				GetScript = {
					Return @{
						'Generated' = (Test-Path -Path "C:\Windows\System32\CertSrv\CertEnroll\$Using:SubCA.crt");
					}
				}
				TestScript = { 
					If (-not (Test-Path -Path "C:\Windows\System32\CertSrv\CertEnroll\$Using:SubCA.crt")) {
						# SubCA Cert is not yet created
						Return $False
					}
					# SubCA Cert has been created
					Return $True
				}
				DependsOn = "[xRemoteFile]DownloadSubCA_$SubCA"
			}

That is all we actually need to do in the loop on the Root CA. It is now up to each Sub CA to download the new Issuing Certificate and install it.

Step 4: Installing the Issuing Certificate on the Sub CA

Now that an Issuing Certificate is available to be downloaded from the Root CA for each Sub CA, the configuration script for each Sub CA can continue. But as always the script needs to use the WaitFor resource (really have to love this resource) to ensure that the certificate is available:

		# Wait for the Root CA to have completed issuance of the certificate for this SubCA.
		WaitForAny SubCACer
		{
			ResourceName = "[Script]IssueCert_$($Node.NodeName)"
			NodeName = $Node.RootCAName
			RetryIntervalSec = 30
			RetryCount = 30
			DependsOn = "[Script]SetREQMimeType"
		}

		# Download the Certificate for this SubCA.
		xRemoteFile DownloadSubCACERFile
		{
			DestinationPath = "C:\Windows\System32\CertSrv\CertEnroll\$($Node.NodeName).cer"
			Uri = "http://$($Node.RootCAName)/CertEnroll/$($Node.NodeName).cer"
			DependsOn = '[WaitForAny]SubCACer'
		}

Once the Sub CA issuing certificate has been downloaded it can be registered with the Certificate Authority as well which will also add it to Active Directory and the local machine store. Once again, there is no specific DSC Resource to do this so I’ve resorted to the DSC Script resource:

		# Register the Sub CA Certificate with the Certification Authority
		Script RegisterSubCA
		{
			PSDSCRunAsCredential = $DomainAdminCredential
			SetScript = {
				Write-Verbose "Registering the Sub CA Certificate with the Certification Authority C:\Windows\System32\CertSrv\CertEnroll\$($Using:Node.NodeName)_$($Using:Node.CACommonName).crt..."
				"$($ENV:SystemRoot)\system32\certutil.exe" -installCert "C:\Windows\System32\CertSrv\CertEnroll\$($Using:Node.NodeName)_$($Using:Node.CACommonName).crt"
			}
			GetScript = {
				Return @{
				}
			}
			TestScript = { 
				If (-not (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CACertHash')) {
					Write-Verbose "Sub CA Certificate needs to be registered with the Certification Authority..."
					Return $False
				}
				Return $True
			}
			DependsOn = '[xRemoteFile]DownloadSubCACERFile'
		}

Note: It is always important to remember that when using the Script DSC Resource if you want to use any variables that are declared outside the resource you’ll need to prefix them with the Using: keyword. I have wasted many hours tracking down issues caused by missing this vital keyword!

Again, we’re running the above script resource using the PSDSCRunAsCredential parameter to run it using Domain Admin credentials so that the command can register the certificates into AD DS.

Once this is done the AIA and CDP extensions can be configured using the same method as we did for the Root CA. This will also start up the Certificate Service:

		# Perform final configuration of the CA which will cause the CA service to startup
		# It should be able to start up once the SubCA certificate has been installed.
		Script ADCSAdvConfig
		{
			SetScript = {
				If ($Using:Node.CADistinguishedNameSuffix) {
					"$($ENV:SystemRoot)\system32\certutil.exe" -setreg CA\DSConfigDN "CN=Configuration,$($Using:Node.CADistinguishedNameSuffix)"
					"$($ENV:SystemRoot)\system32\certutil.exe" -setreg CA\DSDomainDN "$($Using:Node.CADistinguishedNameSuffix)"
				}
				If ($Using:Node.CRLPublicationURLs) {
					"$($ENV:SystemRoot)\System32\certutil.exe" -setreg CA\CRLPublicationURLs $($Using:Node.CRLPublicationURLs)
				}
				If ($Using:Node.CACertPublicationURLs) {
					"$($ENV:SystemRoot)\System32\certutil.exe" -setreg CA\CACertPublicationURLs $($Using:Node.CACertPublicationURLs)
				}
				Restart-Service -Name CertSvc
				Add-Content -Path 'c:\windows\setup\scripts\certutil.log' -Value "Certificate Service Restarted ..."
			}
			GetScript = {
				Return @{
					'DSConfigDN' = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSConfigDN');
					'DSDomainDN' = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSDomainDN');
					'CRLPublicationURLs'  = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CRLPublicationURLs');
					'CACertPublicationURLs'  = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CACertPublicationURLs')
				}
			}
			TestScript = { 
				If (((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSConfigDN') -ne "CN=Configuration,$($Using:Node.CADistinguishedNameSuffix)")) {
					Return $False
				}
				If (((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSDomainDN') -ne "$($Using:Node.CADistinguishedNameSuffix)")) {
					Return $False
				}
				If (($Using:Node.CRLPublicationURLs) -and ((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CRLPublicationURLs') -ne $Using:Node.CRLPublicationURLs)) {
					Return $False
				}
				If (($Using:Node.CACertPublicationURLs) -and ((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CACertPublicationURLs') -ne $Using:Node.CACertPublicationURLs)) {
					Return $False
				}
				Return $True
			}
			DependsOn = '[Script]RegisterSubCA'
		}

Step 5: Shut down the Root CA

Once all the Sub CAs have installed their certificates the Root CA can be shutdown. This is a nice way of identifying that everything has gone according to plan and all Sub CAs can now issue certificates. It also helps reduce the amount of time the Root CA is online. To do this, once again we use the WaitFor DSC Resource. If there is more than one Sub CA being installed then the Root CA script should wait for the last one to be complete.

			# Wait for SubCA to install the CA Certificate
			WaitForAny "WaitForComplete_$SubCA"
			{
				ResourceName = '[Script]InstallSubCACert'
				NodeName = $SubCA
				RetryIntervalSec = 30
				RetryCount = 30
				DependsOn = "[Script]IssueCert_$SubCA"
			}

			# Shutdown the Root CA - it is no longer needed because it has issued all SubCAs
			Script ShutdownRootCA
			{
				SetScript = {
					Stop-Computer
				}
				GetScript = {
					Return @{
					}
				}
				TestScript = { 
					# SubCA Cert is not yet created
					Return $False
				}
				DependsOn = "[WaitForAny]WaitForComplete_$SubCA"
			}

At this point all the Sub CAs should be operational and the Root CA will have been shut down ready to be put away in a safe somewhere. There are still some minor tasks yet to complete such as configuring the Online Responder, generating and installing a Web Server certificate for the Web Enrollment Server etc. But seeing as this part is now getting extremely long I think I’ll leave them till Part 3 in the next few days. I hope this has been useful!

Additional Information

It is probably very useful to see the full complete DSC configuration files. These files change frequently as I optimize and test the process. As noted they are actually part of another project I’m working on – LabBuilder. They are currently available in my LabBuilder project repository on GitHub.

I will cover the LabBuilder project another day once I have completed testing and documentation on it.

Installing a Two-Tier PKI using nothing but Desired State Configuration – Part 1

I am a firm believer in the concept of Infrastructure as Code. I do think technologies such as Chef and Windows PowerShell Desired State Configuration (DSC) will eventually replace ‘clickety-click’ administration in medium to large environments, and even some smaller sites. If you’re not familiar with these technologies or concepts I’d strongly recommend you take a look at the above links.

Note: This post is going to be quite long and it does assume you have a basic understanding of Desired State Configuration (DSC) and Windows Active Directory Certificate Services (AD CS). If you’re not comfortable creating basic DSC configuration files or have never installed AD CS then you might want to get familiar with doing this before jumping into this post.

Other Parts in this Series

Installing a Two-Tier PKI using nothing but Desired State Configuration – Part 2

The Goal

But it’s all well and good to say I think it is the future, but how about I put my money where my mouth is and actually use DSC to implement something much more complicated than a simple IIS web site. Sure, DSC can and should be used for straight forward infrastructure configuration, but what about something like a two-tier PKI with an offline Standalone Root CA as well as one or more Enterprise Subordinate CAs? As part of my LabBuilder project I was going to have to find out. Basically, I was going to try and implement this using nothing more than Desired State Configuration. If you’re interested in seeing how this is done, continue reading.

Why is this Complicated?

The difficulty with installing a two-tier PKI is that the Root CA and Issuing/Sub CA installation processes are interdependent. For example, to install the Sub CA an Issuing certificate must be issued by the Root CA, but this can only be done by the Sub CA issuing the request. The Request gets copied to the Root CA and issued and then the Issuing Certificate copied back to the Sub CA and installed. Therefore, there would need to be at least two DSC configurations, one on the Root CA and one on each Sub CA and they would be running at the same time, interacting with each other and waiting for various processed on each machine to complete before proceeding. This will become clearer later on in this post. To allow a DSC configuration to wait for a step to complete on another machine in another DSC configuration requires the WaitFor DSC resource that is only available in WMF 5.0.

Requirements

To be able to do this you’ll need several things:

  1. A Hyper-V Host with the following Guest VMs:
    1. A standalone clean Windows Server 2012 R2 Core server that will become the Standalone Root CA. In my system this computer is called SS_ROOTCA.
    2. A Domain Controller that the Enterprise Issuing/Sub CA will become a part of. My domain was called LABBUILDER.COM.
    3. One or more standalone clean Windows Server 2012 R2 Core servers that will become the Enterprise Issuing/Sub CAs. In my system I was only using a single Sub CA and it was named  SA_SUBCA. But you could use multiple Sub CAs, the following scripts do support more than one Sub CA.
  2. A computer to create the DSC configuration files on that has RSAT installed (the version appropriate to the operating system).
  3. WMF 5.0 installed on all the above servers as well as the computer you’re creating the DSC configuration on.
  4. The above servers need to be able to communicate with one another via networking (virtual or physical).

Note: In a production environment it is recommended that the Root CA is kept offline and is never connected to a network. Therefore this DSC process wouldn’t actually work. It could be made to work by doing some tricky things like waiting for external storage to be connected and so on, but I’m not even going to go there for this post.

Server Core vs. Full

I used Windows Server Core installations for my PKI servers, but you could just as easily use Full installations if you wanted. However, I think given what I’m trying to achieve, using Server Core makes more sense – and besides, it’s simply the way to go when you’re talking about Infrastructure as Code. In fact, if AD CS was available on Server Nano I would be trying to use that instead. You can still use RSAT to work with these Server Core installations should you need to.

Resources

The DSC configuration files are going to require a few additional DSC resources. These DSC resources will need to be installed onto the PKI servers and the computer you’re using to compile the DSC Configurations into MOF files. The resources you’ll need are:

  1. PSDesiredStateConfiguration – this is build in to the core DSC installation, so it doesn’t need to be downloaded.
  2. xADCSDeployment – this is a community DSC resource required to perform post installation configuration of ADCS.
  3. xPSDesiredStateConfiguration – we need this community DSC resource for the xRemoteFile DSC Resource.
  4. xComputerManagement – this community DSC resource is required to join the Sub CA to the domain.

The easiest way to do this on PowerShell 5.0 is using the Find-Module and  Install-Module cmdlets from PowerShellGet module to download these from the PowerShell Gallery:

Find-Module xPSDesiredStateConfiguration,xADCSDeployment,xComputerManagement | Install-Module

AllNodes

To make things a little bit more generic I like to put all the variables that the DSC configuration files are going to require into an AllNodes hash table, with one for each server. Also, normally I’ll have a self-signed certificate generated on each server and copied down to the computer that is creating the configuration files so that the various credentials can be encrypted – see this post or this post for details on how this works. This is optional and you can use the PSDscAllowPlainTextPassword = $true option in the Node if you want to just send the credentials in the clear.

Important Note Regarding Credential Encryption in DSC

Don’t use the New-SelfSignedCertificate cmdlet to create a self-signed certificate to encrypt your credentials. It creates a certificate that will not work here (the private key is not accessible). Instead, use this script from the script center. You will waste a lot of time trying to figure out what is wrong.

AllNodes for Root CA

These are the Node parameters that contain the variables that you’ll want to configure for the Root CA. They are fairly self explanatory but they will be covered later on in the post.

AllNodes = @(
		@{
			NodeName = 'SS_ROOTCA'
			Thumbprint = 'CDD4EEAE6000AC7F40C3802C171E30148030C072' 
			LocalAdminPassword = 'P@ssword!1'
			CACommonName = "LABBUILDER.COM Root CA"
			CADistinguishedNameSuffix = "DC=LABBUILDER,DC=COM"
			CRLPublicationURLs = "1:C:\Windows\system32\CertSrv\CertEnroll\%3%8%9.crl\n10:ldap:///CN=%7%8,CN=%2,CN=CDP,CN=Public Key Services,CN=Services,%6%10\n2:http://pki.labbuilder.com/CertEnroll/%3%8%9.crl"
			CACertPublicationURLs = "1:C:\Windows\system32\CertSrv\CertEnroll\%1_%3%4.crt\n2:ldap:///CN=%7,CN=AIA,CN=Public Key Services,CN=Services,%6%11\n2:http://pki.labbuilder.com/CertEnroll/%1_%3%4.crt"
			SubCAs = @('SA_SUBCA')
		}
	)

AllNodes for Sub CA

And these are the parameters for each Subordinate CA. If you had more than on Sub CA then you could add additional nodes. The variables are fairly self explanatory but they will be covered later on in the post.

AllNodes = @(
		@{
			NodeName = 'SA_SUBCA'
			Thumbprint = '8F43288AD272F3103B6FB1428485EA3014C0BCFE' 
			LocalAdminPassword = 'P@ssword!1'
			DomainName = "LABBUILDER.COM"
			DomainAdminPassword = "P@ssword!1"
			PSDscAllowDomainUser = $True
			CACommonName = "LABBUILDER.COM Issuing CA"
			CADistinguishedNameSuffix = "DC=LABBUILDER,DC=COM"
			CRLPublicationURLs = "65:C:\Windows\system32\CertSrv\CertEnroll\%3%8%9.crl\n79:ldap:///CN=%7%8,CN=%2,CN=CDP,CN=Public Key Services,CN=Services,%6%10\n6:http://pki.labbuilder.com/CertEnroll/%3%8%9.crl"
			CACertPublicationURLs = "1:C:\Windows\system32\CertSrv\CertEnroll\%1_%3%4.crt\n2:ldap:///CN=%7,CN=AIA,CN=Public Key Services,CN=Services,%6%11\n2:http://pki.labbuilder.com/CertEnroll/%1_%3%4.crt"
			RootCAName = "SS_ROOTCA"
			RootCACRTName = "SS_ROOTCA_LABBUILDER.COM Root CA.crt"
		}
	)

Step 1: Installing the Root CA

First things first. We need to create a credential object that will be used to perform various steps in the process. This is a local credential as this is a standalone server.

	Node $AllNodes.NodeName {
		# Assemble the Local Admin Credentials
		If ($Node.LocalAdminPassword) {
			[PSCredential]$LocalAdminCredential = New-Object System.Management.Automation.PSCredential ("Administrator", (ConvertTo-SecureString $Node.LocalAdminPassword -AsPlainText -Force))
		}

Next up we’ll install the ADCS Certificate Authority and the ADCS Web Enrollment features. Normally on a standalone Root CA you wouldn’t bother installing the ADCS Web Enrollment feature, but in our case it is an easy way to have the CertEnroll website virtual folder created which we use to transfer the Root CA Cert and the Issuing CA Cert (later on).

		# Install the ADCS Certificate Authority
		WindowsFeature ADCSCA {
			Name = 'ADCS-Cert-Authority'
			Ensure = 'Present'
		}
		
		# Install ADCS Web Enrollment - only required because it creates the CertEnroll virtual folder
		# Which we use to pass certificates to the Issuing/Sub CAs       
		WindowsFeature ADCSWebEnrollment
        {
            Ensure = 'Present'
            Name = 'ADCS-Web-Enrollment'
            DependsOn = '[WindowsFeature]ADCSCA'
        }

Next on the agenda is we create a CAPolicy.inf file – this file configures some basic parameters that will be used by the Root CA certificate and server:

		# Create the CAPolicy.inf file which defines basic properties about the ROOT CA certificate
		File CAPolicy
		{
			Ensure = 'Present'
			DestinationPath = 'C:\Windows\CAPolicy.inf'
			Contents = "[Version]`r`n Signature= `"$Windows NT$`"`r`n[Certsrv_Server]`r`n RenewalKeyLength=4096`r`n RenewalValidityPeriod=Years`r`n RenewalValidityPeriodUnits=20`r`n CRLDeltaPeriod=Days`r`n CRLDeltaPeriodUnits=0`r`n[CRLDistributionPoint]`r`n[AuthorityInformationAccess]`r`n"
			Type = 'File'
			DependsOn = '[WindowsFeature]ADCSWebEnrollment'
		}

And now the ADCS Certificate Authority and the ADCS Web Enrollment features can be configured. Notice we are using some of the Nodes parameters here as well as the Local Administrator Credentials:

		# Configure the CA as Standalone Root CA
		xADCSCertificationAuthority ConfigCA
        {
            Ensure = 'Present'
            Credential = $LocalAdminCredential
            CAType = 'StandaloneRootCA'
			CACommonName = $Node.CACommonName
			CADistinguishedNameSuffix = $Node.CADistinguishedNameSuffix
			ValidityPeriod = 'Years'
			ValidityPeriodUnits = 20
            DependsOn = '[File]CAPolicy'
        }

		# Configure the ADCS Web Enrollment
		xADCSWebEnrollment ConfigWebEnrollment {
            Ensure = 'Present'
            Name = 'ConfigWebEnrollment'
            Credential = $LocalAdminCredential
            DependsOn = '[xADCSCertificationAuthority]ConfigCA'
        }

Now, here is where things get interesting. We need to configure some of the more advanced properties of the CA such as the AIA and CDP extensions. The problem is that there is no DSC resource for doing this and there aren’t even any native PowerShell cmdlets either! So I had to resort to the DSC Script resource in combination with the CertUtil.exe tool and registry entries:

		# Set the advanced CA properties
		Script ADCSAdvConfig
		{
			SetScript = {
				If ($Using:Node.CADistinguishedNameSuffix) {
					& "$($ENV:SystemRoot)\system32\certutil.exe" -setreg CA\DSConfigDN "CN=Configuration,$($Using:Node.CADistinguishedNameSuffix)"
					& "$($ENV:SystemRoot)\system32\certutil.exe" -setreg CA\DSDomainDN "$($Using:Node.CADistinguishedNameSuffix)"
				}
				If ($Using:Node.CRLPublicationURLs) {
					& "$($ENV:SystemRoot)\System32\certutil.exe" -setreg CA\CRLPublicationURLs $($Using:Node.CRLPublicationURLs)
				}
				If ($Using:Node.CACertPublicationURLs) {
					& "$($ENV:SystemRoot)\System32\certutil.exe" -setreg CA\CACertPublicationURLs $($Using:Node.CACertPublicationURLs)
				}
				Restart-Service -Name CertSvc
				Add-Content -Path 'c:\windows\setup\scripts\certutil.log' -Value "Certificate Service Restarted ..."
			}
			GetScript = {
				Return @{
					'DSConfigDN' = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSConfigDN');
					'DSDomainDN' = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSDomainDN');
					'CRLPublicationURLs'  = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CRLPublicationURLs');
					'CACertPublicationURLs'  = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CACertPublicationURLs')
				}
			}
			TestScript = { 
				If (((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSConfigDN') -ne "CN=Configuration,$($Using:Node.CADistinguishedNameSuffix)")) {
					Return $False
				}
				If (((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSDomainDN') -ne "$($Using:Node.CADistinguishedNameSuffix)")) {
					Return $False
				}
				If (($Using:Node.CRLPublicationURLs) -and ((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CRLPublicationURLs') -ne $Using:Node.CRLPublicationURLs)) {
					Return $False
				}
				If (($Using:Node.CACertPublicationURLs) -and ((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CACertPublicationURLs') -ne $Using:Node.CACertPublicationURLs)) {
					Return $False
				}
				Return $True
			}
			DependsOn = '[xADCSWebEnrollment]ConfigWebEnrollment'
		}

The above section was actually detailed in my previous post here. With all that done the Root CA is installed and ready to go. But this DSC configuration script is not yet finished, but we can’t go any further until the Sub CA DSC has progressed. It is important to keep in mind that these DSC scripts are running at the same time on different machines and will interact with one another during this process.

This also seems like an appropriate time to take a break. The really interesting stuff is yet to come, but it is getting close to my bedtime and so I’ll continue this in part 2 tomorrow. This will cover the Sub CA DSC configuration and the final part of the Root CA DSC configuration. Hopefully someone out there has stuck with me till this point! 🙂

Next Part

Installing a Two-Tier PKI using nothing but Desired State Configuration – Part 2

Advanced Certificate Services Configuration with DSC

Recently I’ve been rebuilding my Hyper-V lab environment from scratch (as part of my MCSA/MCSE studying) and decided I would completely script the process using PowerShell only. My goal was to not require a single interactive session with any of the servers to setup the entire environment. This was a multi-site AD environment with several other member servers performing other duties including NPS & NAP, ADCS, WDS, WSUS, ADFS, ADRMS, IIS, DirectAccess, SQL etc. I also wanted this to include a proper Multi-tier PKI environment with both a standalone Root CA and an Enterprise Subordinate CA (as is recommended by Microsoft).

The Problem

To configure a AD CS using DSC is very straight forward using the xADCSDeployment DSC Resource. However, anyone who has installed an enterprise PKI is probably familiar with the fact that that AIA and CDP settings need to be configured for the CA’s so that any generated certificates can include these extensions. Unfortunately the xADCSDeployment DSC Resource doesn’t support setting these Certificate Services options because the underlying AD CS PowerShell cmdlets don’t allow setting these options either.

The Solution

The solution to this problem is to use the DSC Script Resource to call the CertUtil.exe application to set these values. To do this I set the SetScript resource parameter like this:

			SetScript = {
				If ($Using:Node.CADistinguishedNameSuffix) {
					& "$($ENV:SystemRoot)\system32\certutil.exe" -setreg CA\DSConfigDN "CN=Configuration,$($Using:Node.CADistinguishedNameSuffix)"
					& "$($ENV:SystemRoot)\system32\certutil.exe" -setreg CA\DSDomainDN "$($Using:Node.CADistinguishedNameSuffix)"
				}
				If ($Using:Node.CRLPublicationURLs) {
					& "$($ENV:SystemRoot)\System32\certutil.exe" -setreg CA\CRLPublicationURLs $($Using:Node.CRLPublicationURLs)
				}
				If ($Using:Node.CACertPublicationURLs) {
					& "$($ENV:SystemRoot)\System32\certutil.exe" -setreg CA\CACertPublicationURLs $($Using:Node.CACertPublicationURLs)
				}
				Restart-Service -Name CertSvc
				Add-Content -Path 'c:\windows\setup\scripts\certutil.log' -Value "Certificate Service Restarted ..."
			}

The above code expects the $Node object to contain several properties that it will use to set applicable settings in the CA server:

  • CADistinguishedNameSuffix – This is the Directory Services Distinguished Name (e.g. DC=bmdlab,DC=com). If left blank then your CDP and AIA LDAP Addresses will be incorrect.
  • CRLPublicationURLs – The CRL Publication URLs in the same format as you would normally pass to the certutil.exe application.
  • CACertPublicationURLs – The CA Cert Publication URLs (AIA Extension) in the same format as you would normally pass to the certutil.exe application.

Note: To access the $Node object in the scripts resource you’ll need to use Using keyword, otherwise the $Node object won’t be available in the external script scope.

And finally, after these values have been set the CertSvc needs to be restarted.

What about TestScript?

You might look at the above code and wonder, “won’t the CertSvc be restarted every 30 minutes no matter what?”. That is where the TestScript resource parameter comes in. We’ll use this to decide if the current values of the CA are different to what we want them to be. In this case we dive straight to the registry rather than using the CertUtil.exe application.

			TestScript = { 
				If (((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSConfigDN') -ne "CN=Configuration,$($Using:Node.CADistinguishedNameSuffix)")) {
					Return $False
				}
				If (((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSDomainDN') -ne "$($Using:Node.CADistinguishedNameSuffix)")) {
					Return $False
				}
				If (($Using:Node.CRLPublicationURLs) -and ((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CRLPublicationURLs') -ne $Using:Node.CRLPublicationURLs)) {
					Return $False
				}
				If (($Using:Node.CACertPublicationURLs) -and ((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CACertPublicationURLs') -ne $Using:Node.CACertPublicationURLs)) {
					Return $False
				}
				Return $True
			}

Once again I need to ensure the Using scope is used with the Node variable. If any of the node properties don’t match the registry value then false is returned which triggers SetScript. If all of the values match True is returned (meaning everything is the same) and SetScript isn’t fired.

Is That It?

Almost. Finally I needed to implement the GetScript parameter of the resource. This was the easiest part:

			GetScript = {
				Return @{
					'DSConfigDN' = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSConfigDN');
					'DSDomainDN' = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSDomainDN');
					'CRLPublicationURLs'  = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CRLPublicationURLs');
					'CACertPublicationURLs'  = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CACertPublicationURLs')
				}
			}

I could also adjust the TestScript to call the GetScript and then use the returned hash table to compare with the Node values instead of comparing them directly with the registry values. But I didn’t.

Here is what the final script looks like (I didn’t include everything in the DSC Configuration as there were lots of other resources for creating the machine):

		Script ADCSAdvConfig
		{
			SetScript = {
				If ($Using:Node.CADistinguishedNameSuffix) {
					& "$($ENV:SystemRoot)\system32\certutil.exe" -setreg CA\DSConfigDN "CN=Configuration,$($Using:Node.CADistinguishedNameSuffix)"
					& "$($ENV:SystemRoot)\system32\certutil.exe" -setreg CA\DSDomainDN "$($Using:Node.CADistinguishedNameSuffix)"
				}
				If ($Using:Node.CRLPublicationURLs) {
					& "$($ENV:SystemRoot)\System32\certutil.exe" -setreg CA\CRLPublicationURLs $($Using:Node.CRLPublicationURLs)
				}
				If ($Using:Node.CACertPublicationURLs) {
					& "$($ENV:SystemRoot)\System32\certutil.exe" -setreg CA\CACertPublicationURLs $($Using:Node.CACertPublicationURLs)
				}
				Restart-Service -Name CertSvc
				Add-Content -Path 'c:\windows\setup\scripts\certutil.log' -Value "Certificate Service Restarted ..."
			}
			GetScript = {
				Return @{
					'DSConfigDN' = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSConfigDN');
					'DSDomainDN' = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSDomainDN');
					'CRLPublicationURLs'  = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CRLPublicationURLs');
					'CACertPublicationURLs'  = (Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CACertPublicationURLs')
				}
			}
			TestScript = { 
				If (((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSConfigDN') -ne "CN=Configuration,$($Using:Node.CADistinguishedNameSuffix)")) {
					Return $False
				}
				If (((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('DSDomainDN') -ne "$($Using:Node.CADistinguishedNameSuffix)")) {
					Return $False
				}
				If (($Using:Node.CRLPublicationURLs) -and ((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CRLPublicationURLs') -ne $Using:Node.CRLPublicationURLs)) {
					Return $False
				}
				If (($Using:Node.CACertPublicationURLs) -and ((Get-ChildItem 'HKLM:\System\CurrentControlSet\Services\CertSvc\Configuration').GetValue('CACertPublicationURLs') -ne $Using:Node.CACertPublicationURLs)) {
					Return $False
				}
				Return $True
			}
			DependsOn = '[xADCSWebEnrollment]ConfigWebEnrollment'
		}

Hopefully someone will make sense of all this. It should also be useful in other similar situations where there are no relevant DSC resources.

Certificate Web Enrollment on a Server and a Misleading Error Message

The Certificate Web Enrollment component of Certificate Services is fairly helpful for allowing easy certificate request and enrollment from any computer.

Requesting a certificate via the Web Enrollment service web page.

Requesting a certificate via the Web Enrollment service web page.

It does require Internet Explorer because of an Active X control that runs on the page, but this is acceptable. It also needs to be connected to using HTTPS – which is also fine. Except when it isn’t. Or more accurately, reports that you are not connected via HTTPS when you in fact are.

The following error message appears when connecting to this page from Internet Explorer 11 on a Windows Server 2012 R2 member server or DC:

The page requires an HTTPS connection - but it is connected via HTTPS!

The page requires an HTTPS connection – but it is connected via HTTPS!

“In order to complete certificte enrollment, the Web site for the CA must be configured to use HTTPS authentication.”

Clearly, Internet Explorer was using HTTPS to communicate with this web site. Clicking OK on the error message and hoping to just ignore it wasn’t possible because most of the drop down boxes on the form were not being populated – preventing it from being submitted:

After clicking OK on the error message the page is broken.

After clicking OK on the error message the page is broken.

I spent quite some time investigating the cause of this error, including checking the certificate chain that the client was using:

The certificate and certificate chain - nothing wrong here.

The certificate and certificate chain – nothing wrong here.

Of course if the certificate or the chain was bad then SSL wouldn’t be being used – which it clearly was.

Eventually I identified the cause of the problem. The Active X was being prevented from being run properly because of IE security. This is probably a good thing on a Server operating system, but the error mesage being presented in this case was very misleading.

The way to fix this is to add the web site to the trusted sites list in Internet Explorer:

  1. Select Internet Options from the Internet Explorer Settings menu:ss_cs_advancedcertificateqrequest_internetexplorersettings
  2. Select the Security tab and click Trusted Sites:ss_cs_advancedcertificateqrequest_ietrustedsites
  3. Click the Sites button.
  4. Enter the https URL of the Certificate Web Enrollment site and click Add:ss_cs_advancedcertificateqrequest_trustthissite
  5. Click Close.

The web site can now be refreshed and should work correctly at last:

Certificate Web Enrollment form working correctly with SSL.

Certificate Web Enrollment form working correctly with SSL.

Hopefully this helps someone out there avoid this annoying problem.

B\M/D