Writing a custom DSC resource for Remote Desktop (RDP) settings

Hello readers!

In this previous post I talked about a simple example to guide you through the Windows PowerShell Desired State Configuration (DSC) authoring process. In this blog post we are going to create a custom DSC resource based on a practical situation where you need to manage your servers remotely.

All the samples used in this blog post, including the DSC module, can be found and installed through PowerShellGet which is part of the Windows Management Framework 5.0

image

The xRemoteDesktopAdmin module is part of the DSC Resource Kit Wave 6


Have you ever been deploying servers, and found yourself unable to set up a remote desktop (RDP) session to those servers, because you forgot to configure remote access? No more with this custom DSC RDP resource! Not having RDP access is especially painful when you provision a VM in Microsoft Azure based on your custom uploaded VHD.

Let’s take a look how we can prevent that experience by using DSC.


The objective of our custom DSC resource

So what things do I need to take into consideration when writing a DSC RDP resource?

  1. How can I configure remote desktop settings?
  2. How can I configure NLA (Network Level Authentication) settings?
  3. How can I add a domain user to the Remote Desktop Users group?
  4. How can I configure Windows Firewall to allow remote access?

 

Let’s look at the beginning. Obviously, I want to configure these settings:

image

That are multiple ways of achieving that. I could configure Group Policy settings, script against WMI, configure the registry, etc.

In this example, let's take an easy route: the registry.


Configuring RDP settings

There are 2 registry entries which hold the settings I’m interested in:

HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections"

This setting either allows or doesn’t allow a remote connection to this computer.

 

HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -name "UserAuthentication"

This setting enables or disables the requirement to use NLA.

Let’s create a custom DSC resource to configure these registry settings.


The GET section

If you have been reading along, in this blog post, you’ll remember the boilerplate approach to configure the DSC GET, SET and TEST sections. Let’s create our schema file first by using the Resource Designer Tool:

image

Because the registry values are of type integer, we need to “convert” them, so to speak. We can use Windows PowerShell’s Switch functionality for that. Let’s see how that translates into the GET section (notice line 016):

001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046 #region GET RDP Settingsfunction Get-TargetResource{[CmdletBinding()][OutputType([System.Collections.Hashtable])]param([Parameter(Mandatory)] [ValidateSet("Present","Absent")] [System.String]$Ensure, [ValidateSet("NonSecure", "Secure")][System.String]$UserAuthentication) switch ($Ensure) { "Present" {[System.Byte]$fDenyTSConnections = 0} "Absent" {[System.Byte]$fDenyTSConnections = 1} } switch ($UserAuthentication) { "NonSecure" {[System.Byte]$UserAuthentication = 0} "Secure" {[System.Byte]$UserAuthentication = 1} } $GetDenyTSConnections = Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections" $GetUserAuth = Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -name "UserAuthentication" $returnValue = @{ Ensure = switch ($GetDenyTSConnections.fDenyTSConnections) { 0 {"Present"} 1 {"Absent"} } UserAuthentication = switch ($GetUserAuth.UserAuthentication) { 0 {"NonSecure"} 1 {"Secure"} } } $returnValue }# Get-TargetResource 'Present' 'Secure' -Verbose# Expectation is a hashtable with configuration of the machine.#endregion

In line 026, we get the registry values; and in the section below, we do the “conversion” of Present and Absent, and return our hash table.


The SET section

In the SET section, we basically apply the same logic. In the make it so section we run Windows PowerShell’s Set-ItemProperty cmdlet to set the registry value:

001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041 #region SET RDP Settingsfunction Set-TargetResource{[CmdletBinding()][OutputType([System.Collections.Hashtable])]param([Parameter(Mandatory)] [ValidateSet("Present","Absent")] [System.String]$Ensure, [ValidateSet("NonSecure", "Secure")][System.String]$UserAuthentication) switch ($Ensure) { "Present" {[System.Byte]$fDenyTSConnections = 0} "Absent" {[System.Byte]$fDenyTSConnections = 1} } switch ($UserAuthentication) { "NonSecure" {[System.Byte]$UserAuthentication = 0} "Secure" {[System.Byte]$UserAuthentication = 1} } $GetEnsure = (Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections").fDenyTSConnections $GetUserAuthentiation = (Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -name "UserAuthentication").UserAuthentication #The make it so section if ($Ensure -ne $GetEnsure) { Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server'-name "fDenyTSConnections" -Value $fDenyTSConnections } if ($UserAuthentication -ne $GetUserAuthentication) { Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -name "UserAuthentication" -Value $UserAuthentication } }# Set-TargetResource 'Present' 'Secure' -Verbose# Expectation is the computer will be configured to accept secure RDP connections. To verify, right click on the Windows button and open System - Remote Settings.#endregion

 


The TEST section

Finally, the TEST section. Remember, this is the section where you can display verbose messages to the user, as in the example below:

 

001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056 #region TEST RDP Settingsfunction Test-TargetResource{[CmdletBinding()][OutputType([System.Collections.Hashtable])]param([Parameter(Mandatory)] [ValidateSet("Present","Absent")] [System.String]$Ensure, [ValidateSet("NonSecure", "Secure")][System.String]$UserAuthentication) switch ($Ensure) { "Present" {[System.Byte]$fDenyTSConnections = 0} "Absent" {[System.Byte]$fDenyTSConnections = 1} } switch ($UserAuthentication) { "NonSecure" {[System.Byte]$UserAuthentication = 0} "Secure" {[System.Byte]$UserAuthentication = 1} } $GetfDenyTSConnections = (Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections").fDenyTSConnections $GetUserAuthentiation = (Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -name "UserAuthentication").UserAuthentication $bool = $false if ($fDenyTSConnections -eq $GetfDenyTSConnections -and $UserAuthentication -eq $GetUserAuthentiation) { Write-Verbose "RDP settings are matching the desired state" $bool = $true } else { Write-Verbose "RDP settings are Non-Compliant!" if ($fDenyTSConnections -ne $GetfDenyTSConnections) { Write-Verbose "DenyTSConnections settings are non-compliant, Value should be $fDenyTSConnections - Detected value is: $GetfDenyTSConnections" } if ($UserAuthentication -ne $GetUserAuthentiation) { Write-Verbose "UserAuthentication settings are non-compliant, Value should be $UserAuthentication - Detected value is: $GetUserAuthentiation" } } $bool }# Test-TargetResource 'Present' 'Secure' -Verbose# Expectation is a true/false output based on whether the machine matches the declared configuration.#endregionExport-ModuleMember -Function *-TargetResource

Cool, that was the most important part! Where are we with our to-do list?

  1. How can I configure remote desktop settings? – Done!
  2. How can I configure NLA (Network Level Authentication) settings? - Done!
  3. How can I add a domain user to the Remote Desktop Users group? ?
  4. How can I configure Windows Firewall to allow remote access?

 


Adding a domain user to my local Remote Desktop Users group

So far, so good. I can cross requirement 1 and 2 off my list. Because I probably want non-admin users to access this server remotely as well , I need to figure out a way to add users; not any user though, but specific domain users. Let’s test-drive this with the Group DSC resource which ships as part of Windows (see this TechNet article) and create a simple DSC configuration:

001002003004005006007008009010011012013014015016017018019020021022023024025 configuration DSCGroupTest{ node ('localhost') { Group ConfigureRDPGroup { Ensure = 'Present' GroupName = "Remote Desktop Users" Members = 'xwing\MyRDPuser' Credential = $Credential } }}$workingdir = 'C:\DSC\DSCGroupTest\MOF'DSCGroupTest -OutputPath $workingdirStart-DscConfiguration -ComputerName 'localhost' -wait -force -verbose -path $workingdir

In the above configuration, I want to add a domain user xwing\MyRDPuser to the local Remote Desktop Users group, so let’s run this:

image

Hmm…that doesn’t look right. Could not find a principal with the provided name [xwing\myrdpuser]

We need a credential to reach out to Active Directory to get and add the domain user account. If you test with a local user account, this would work, but what use would it have to add a local user to the Remote Desktop Users group, right? Smile

Obviously, we need to pass credentials. So how do we do that?


Passing credentials in DSC

There are two ways of passing credentials in a DSC configuration. There’s a not recommended one for testing purposes, for example; because it will store your password in clear text in the MOF file.  But there’s also a secure recommended one which will encrypt our credentials. Let’s look at both.

First, tell DSC that you really want to use and store passwords in clear text. Next, you'll need to pass that credential. We are going to add a DSC configuration data section which will take care of us saying “yes, we are really sure that we want to use a clear text password”:

001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037 $ConfigData = @{ AllNodes = @( @{ NodeName="localhost"; PSDscAllowPlainTextPassword = $true })}Configuration AllowRemoteDesktopAdminConnections{ $password = ConvertTo-SecureString "YourPasswordHere" -AsPlainText -Force $Credential = New-Object System.Management.Automation.PSCredential ("Contoso\RDP_Admin", $password) node ('localhost') { Group RDPGroup { Ensure = 'Present' GroupName = "Remote Desktop Users" Members = 'Contoso\RDP_User' Credential = $Credential } }}# Set your working directory for the output of the MOF file$workingdir = 'C:\RDP\MOF'# Create MOF with configuration dataAllowRemoteDesktopAdminConnections -ConfigurationData $ConfigData -OutputPath $workingdir# Apply the configurationStart-DscConfiguration -ComputerName 'localhost' -wait -force -verbose -path $workingdir

In line 005, we add PSDscAllowPlainTextPassword to let us use plain text passwords. In line 013 and 014, we create a credential object based on the domain user name and password. There are different options for harvesting credentials; I’m just showing you an example here. In line 023, we reference those credentials. In line 034, I’m specifying the configuration data to be included when you create a MOF file. Finally, we create the MOF file by invoking Start-DscConfiguration on line 037.

This is how your MOF file looks. Notice the clear text password:

image

But the good news is that you are now ready to test-drive your configuration to add a domain user to your local Remote Desktop Users group. Let’s apply this configuration:

image

image

Nice!


Using certificates to securely pass credentials to DSC

So you want to do things the right and secure way? Excellent! Though this requires some configuration, it's worth your investment. A very good walkthrough about how to do this has been provided by the Windows PowerShell team right here. Essentially, you need a certificate on each target node which is capable of encryption. If you apply the configuration from another node than the target node, you must have the target node’s public key to do the encryption. Using DSC configuration data, you specify where the certificate file (.cer) can be found, and then specify the certificate thumbprint for the target node to do the decryption.

Here is what my DSC configuration data section looks like:

001002003004005006007008009 $ConfigData = @{ AllNodes = @( @{ NodeName="DSCnode1"; CertificateFile = "C:\Certificates\DSCnode1.cer" Thumbprint = "E36D15C59BDBABB8525E48568844DD7079C1C3DD" })}

As mentioned in this blog post, you need to tell the Local Configuration Manager (LCM) of the target node which certificate to use to decrypt the credentials. You can do that like this (line 016-018) :

001002003004005006007008009010011012013014015016017018019020021 $ConfigData = @{ AllNodes = @( @{ NodeName="DSCnode1"; CertificateFile = "C:\Certificates\DSCnode1.cer" Thumbprint = "E36D15C59BDBABB8525E48568844DD7079C1C3DD" })}configuration AllowRemoteDesktopAdminConnections{ node ('DSCnode1') { LocalConfigurationManager { CertificateId = $node.Thumbprint } }}

Configuring the firewall to allow a remote desktop connection

Okay, back to our to-do list:

    1. How can I configure remote desktop settings? – Done!
    2. How can I configure NLA (Network Level Authentication) settings? - Done!
    3. How can I add a domain user to the Remote Desktop Users group? - Done!
    4. How can I configure Windows Firewall to allow remote access?

The last piece remains: how can we make sure that Windows Firewall is not going to block our RDP connection? Well…we make it so. Smile

In the DSC resource kit – which you most likely by now have downloaded and tested, right? – you will find the xNetworking module. This includes the xFirewall DSC resource, which takes care of our last hurdle:

001002003004005006007008009010011012013014015016 configuration AllowRemoteDesktopAdminConnections{ node ('DSCnode1') { xFirewall AllowRDP { Name = 'DSC - Remote Desktop Admin Connections' DisplayGroup = "Remote Desktop" Ensure = 'Present' State = 'Enabled' Access = 'Allow' Profile = 'Domain' } }}

Is it that easy? Yes, it is.


Download and install the xRemoteDesktopAdmin module

The xRemoteDesktopAdminin module can be downloaded and installed through PowerShellGet which is part of the Windows Management Framework 5.0

image

It contains:

  • The custom DSC RDP module and resource
  • A DSC data (psd1) file

The sample configurations cover:

  • Just enabling RDP connections
  • Configuring the Windows Firewall to allow a remote session
  • Adding a domain user using clear text passwords
  • Adding a domain user using encrypted credentials

 

Below is the sample configuration for configuring remote desktop settings, configuring a Windows Firewall rule, and adding a domain user (using clear text passwords – for testing purposes only):

Happy automating, until next time!


001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056 # The configuration data section specifies to allow using a plain text stored password$ConfigData = @{ AllNodes = @( @{ NodeName="DSCnode1"; PSDscAllowPlainTextPassword = $true })}Configuration AllowRemoteDesktopAdminConnections{ $password = ConvertTo-SecureString "YourPasswordHere" -AsPlainText -Force $Credential = New-Object System.Management.Automation.PSCredential ("Contoso\RDP_Admin", $password) Import-DscResource -Module xRemoteDesktopAdmin, xNetworking node ('DSCnode1') { xRemoteDesktopAdmin RemoteDesktopSettings { Ensure = 'Present' UserAuthentication = 'Secure' } xFirewall AllowRDP { Name = 'DSC - Remote Desktop Admin Connections' DisplayGroup = "Remote Desktop" Ensure = 'Present' State = 'Enabled' Access = 'Allow' Profile = 'Domain' } Group RDPGroup { Ensure = 'Present' GroupName = "Remote Desktop Users" Members = 'Contoso\RDP_User' Credential = $Credential } }}# Set your working directory for the output of the MOF file$workingdir = 'C:\DSC\RDP\MOF'# Create MOF with configuration dataAllowRemoteDesktopAdminConnections -ConfigurationData $ConfigData -OutputPath $workingdir# Apply the configurationStart-DscConfiguration -ComputerName 'DSCnode1' -wait -force -verbose -path $workingdir