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:

[sourcecode language=“powershell”] 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)) } [/sourcecode]

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.

[sourcecode language=“powershell”] # 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” } [/sourcecode]

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):

[sourcecode language=“powershell”] # 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” } [/sourcecode]

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:

[sourcecode language=“powershell”] # 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’ } [/sourcecode]

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:

[sourcecode language=“powershell”] # 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’ } [/sourcecode]

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:

[sourcecode language=“powershell”] # 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:

[sourcecode language=“powershell”] # 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’ } [/sourcecode]

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.

[sourcecode language=“powershell”] # 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’ } [/sourcecode]

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:

[sourcecode language=“powershell”] # 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’ } [/sourcecode]

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:

[sourcecode language=“powershell”] SubCAs=@(‘SA_SUBCA1’,‘SA_SUBCA2’,‘SA_SUBCA3’) [/sourcecode]

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:

[sourcecode language=“powershell”] # Generate Issuing certificates for any SubCAs Foreach ($SubCA in $Node.SubCAs) { [/sourcecode]

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:

[sourcecode language=“powershell”] # 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” } [/sourcecode]

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.

[sourcecode language=“powershell”] # 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” } [/sourcecode]

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:

[sourcecode language=“powershell”] # 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’ } [/sourcecode]

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:

[sourcecode language=“powershell”] # 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’ } [/sourcecode]

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:

[sourcecode language=“powershell”] # 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’ } [/sourcecode]

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.

[sourcecode language=“powershell”] # 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” } [/sourcecode]

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.