Using Credentials with PsDscAllowPlainTextPassword and PsDscAllowDomainUser in PowerShell DSC Configuration Data

imageWarnings and errors, oh my!

If you have written a DSC configuration containing a credential, then you have likely seen error messages about plain text passwords. And recently a warning was added when using domain credentials. In today's post we explain how to handle these appropriately using DSC configuration data.

Where is the documentation for DSC?

If you have been learning PowerShell DSC, you may have noticed that documentation is scattered between blog posts, TechNet, and MSDN. Recently the PowerShell team announced that there is an official home for all PowerShell DSC documentation. Even better, you can contribute and update it as it is open source on GitHub. The WMF Release Notes are also a great source of documentation open for your contributions.

I noticed that no one has documented these DSC configuration data keywords yet:

  • PsDscAllowPlainTextPassword
  • PsDscAllowDomainUser

I decided to write up a blog post on these topics, and then submit a modified version of it to the documentation on GitHub.

Handling Credentials in DSC

So you have a DSC configuration resource that needs to run as an account other than Local System. For example, sometimes the Package resource needs to install software under a specific user account.

Earlier resources used a hard-coded Credential property name to handle this. WMF 5.0 added an automatic PsDscRunAsCredential property for all resources. Newer resources and your own custom resources can use this automatic property instead of creating their own property for credentials.

Note that some resources are designed to use multiple credentials for a specific reason, and they will have their own credential properties.

To identify the available credential properties on a resource use either Get-DscResource -Name foo -Syntax or the Intellisense in the ISE (CTRL+SPACE).

 PS C:\> Get-DscResource -Name Group -Syntax
Group [String] #ResourceName
{
    GroupName = [string]
    [Credential = [PSCredential]]
    [DependsOn = [string[]]]
    [Description = [string]]
    [Ensure = [string]{ Absent | Present }]
    [Members = [string[]]]
    [MembersToExclude = [string[]]]
    [MembersToInclude = [string[]]]
    [PsDscRunAsCredential = [PSCredential]]
}

In this case I am using a Group resource from the PSDesiredStateConfiguration built-in DSC resource module. It can create and add members to a local group. It accepts both the Credential property and the automatic PsDscRunAsCredential property. However, it is coded to only use the Credential property.

Read more about PsDscRunAsCredential in the PowerShell team blog post Validate PowerShell DSC RunAsCredential or the WMF Release Notes.

The Group resource Credential property

But why do you need a credential to add a member to a group? Well, if the group member to be added is a local account, then you do not need it. DSC runs under local system, so it already has permissions to modify local users and groups.

However, if you need to add a domain member to the local group, then you need a credential. You see, a long time ago we figured out that allowing anonymous queries to Active Directory was probably not a good security practice. Imagine anyone on any network device hitting the LDAP port on a domain controller to read all of your Active Directory users and groups. Yeah, not good.

The Credential property of the Group resource is the domain account used to query Active Directory. For most purposes this can be a generic user account, because by default users can read most of the objects in Active Directory.

Show me some code!

So here is our first attempt at using DSC to populate a local group with a domain user:

 Configuration DomainCredentialExample
{
param(
    [PSCredential]$DomainCredential
)
    Import-DscResource -ModuleName PSDesiredStateConfiguration

    node localhost
    {
        Group DomainUserToLocalGroup
        {
            GroupName        = 'InfoSecBackDoor'
            MembersToInclude = 'contoso\notyouraccount'
            Credential       = $DomainCredential
        }
    }
}

$cred = Get-Credential -UserName contoso\genericuser -Message "Password please"
DomainCredentialExample -DomainCredential $cred

However, when you run this code to generate the MOF file... suddenly your PowerShell ISE is filled with red and orange messages:

 ConvertTo-MOFInstance : System.InvalidOperationException error processing
property 'Credential' OF TYPE 'Group': Converting and storing encrypted
passwords as plain text is not recommended. For more information on securing
credentials in MOF file, please refer to MSDN blog:
https://go.microsoft.com/fwlink/?LinkId=393729

At line:11 char:9
+   Group
At line:297 char:16
+     $aliasId = ConvertTo-MOFInstance $keywordName $canonicalizedValue
+                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Write-Error], InvalidOperationException
    + FullyQualifiedErrorId : FailToProcessProperty,ConvertTo-MOFInstance

WARNING: It is not recommended to use domain credential for node 'localhost'.
In order to suppress the warning, you can add a property named
'PSDscAllowDomainUser' with a value of $true to your DSC configuration data
for node 'localhost'.

The Problem(s)

You actually have two problems here:

  1. An error is telling you that plain text passwords are a resume-generating event. :)
  2. A warning is telling you not to use a domain credential.

Sure. Plain text passwords are a bad idea any way you look at it. And now we are telling you straight up. Don't do it. But what if you are just testing (wink, wink)?

Before we address these problems, we need to take a brief detour. My job is to teach you how to fish, so make some room in your tackle box for a new lure.

Code as Documentation

A popular notion in the world of DevOps is that the code is the documentation. In this case you actually have access to the source.

Remember that absence of certain DSC documentation I mentioned earlier? Well, that is only partially true. Watch this:

 cd (Get-Module PSDesiredStateConfiguration -ListAvailable).ModuleBase
psEdit .\PSDesiredStateConfiguration.psm1

PowerShell modules have a ModuleBase property that tells you where the source lives on disk. Change to that directory, and Shazam! You're staring at the source files. PowerShell DSC configurations are processed by the PSDesiredStateConfiguration.psm1 module file code. A quick look through the localized string data at the top reveals the error and warning message text we just now saw in the script window:

 EncryptedToPlaintextNotAllowed=Converting and storing encrypted passwords as plain text is not recommended. For more information on securing credentials in MOF file, please refer to MSDN blog: https://go.microsoft.com/fwlink/?LinkId=393729
DomainCredentialNotAllowed=It is not recommended to use domain credential for node '{0}'. In order to suppress the warning, you can add a property named 'PSDscAllowDomainUser' with a value of $true to your DSC configuration data for node '{0}'.

WARNING: DO NOT EDIT ANY CODE IN THE PSDesiredStateConfiguration MODULE FILE.

Now we can search for keywords revealed in those messages throughout the entire module file. For *nix folks the PowerShell equivalent of grep is Select-String.

 Select-String -Path .\PSDesiredStateConfiguration.psm1 -Pattern EncryptedToPlaintextNotAllowed
Select-String -Path .\PSDesiredStateConfiguration.psm1 -Pattern DomainCredentialNotAllowed
Select-String -Path .\PSDesiredStateConfiguration.psm1 -Pattern PSDscAllowPlainTextPassword
Select-String -Path .\PSDesiredStateConfiguration.psm1 -Pattern PSDscAllowDomainUser

This gives you the line numbers to examine farther down in the module to see where these messages and keywords are used. Use CTRL+G in your PowerShell ISE to go to these lines.

In the code you will find some comments explaining the behavior of these keywords, in addition to the syntax itself. Let's dump these comments to the screen for easier reading of the bigger story:

 Select-String -Path .\PSDesiredStateConfiguration.psm1 -Pattern "#" |
 Where-Object {$_.LineNumber -ge 548 -and $_.LineNumber -le 640} |
 Format-Table Line -Wrap

Depending on versioning of this module, over time you may need to adjust the line numbers.

Now carefully close the module file without saving any changes.

See how we did that? To find your answers, use the source code as documentation.

PsDscAllowPlainTextPassword

Lucky for us the first error message has a URL where we can find documentation. This link explains that you need to encrypt passwords using a ConfigurationData structure and a certificate. Previously I explained the ins and outs of certificates and DSC here. Go read that post for context if necessary.

However, the PowerShell help tells us nothing about how to force a plain text password. Using the technique I described above, you will discover PsDscAllowPlainTextPassword in the source code. This keyword goes into the configuration data section as follows:

 Configuration DomainCredentialExample
{
param(
    [PSCredential]$DomainCredential
)
    Import-DscResource -ModuleName PSDesiredStateConfiguration

    node localhost
    {
        Group DomainUserToLocalGroup
        {
            GroupName        = 'InfoSecBackDoor'
            MembersToInclude = 'contoso\notyouraccount'
            Credential       = $DomainCredential
        }
    }
}

$cd = @{
    AllNodes = @(
        @{
            NodeName = 'localhost'
            PSDscAllowPlainTextPassword = $true
        }
    )
}

$cred = Get-Credential -UserName contoso\genericuser -Message "Password please"
DomainCredentialExample -DomainCredential $cred -ConfigurationData $cd

Note that NodeName cannot equal asterisk. It must be a specific node name.

At Microsoft, we try to warn you about things you should not do, like plain text passwords. Think of this like the safety on a gun. You are completely capable of shooting yourself in the foot. All you have to do is turn off the safety using the syntax above. Then go update your resume.

The right way to handle credentials... encrypt them.

That help URL we saw in the plain text error message above also told us the correct way to handle credentials. Go read it for the full documentation. You use the public key of the encryption certificate for the target node. Your updated ConfigurationData will look something like this:

 $cd = @{
    AllNodes = @(
        @{
            NodeName = 'localhost'
            CertificateFile = 'C:\PublicKeys\server1.cer'
        }
    )
}

For more information on this technique see the blog post Want to secure credentials in Windows PowerShell Desired State Configuration?.

Why no domain credential?

Now when we run the configuration script again (with or without encryption), we still get a warning that using domain accounts for a credential are not recommended. But why?

Think about it. As a server administrator (or hacked admin) you have access to:

  • C:\Windows\System32\Configuration holding DSC files
  • current.mof containing encrypted credentials (I know you would not deploy plain text passwords. )
  • the certificate store containing the private key used to decrypt credentials

With this access an administrator could:

  • decrypt the password into plain text for bad use elsewhere
  • copy/paste the encrypted password into another MOF file for bad use locally
  • copy/paste the encrypted password into another MOF file for bad use remotely (only where the same private key is installed for decryption)

In an assumed breach environment this could create an untraceable DSC pass-the-hash vulnerability. Using a local account eliminates the passing of domain credentials for bad intent on other servers. This is the concept behind JEA - Just Enough Administration, which is based on DSC. See these links for more information on JEA:

When using credentials with DSC resources you should prefer a local account over a domain account when possible.

How does it know I'm using a domain credential?

Looking at the source code we uncovered above, you will find a function called isDomainUser. Look at the comments and logic in this function:

 # if username contains '\' example: domain\username or '@' example: username@mydomain.com
# it may not be a local user.
# In case of '\', domain name can be local machine.

Now you know. If there is a '\' or '@' in the Username property of the credential, then we will treat it as a domain account. An exception is made for "localhost", "127.0.0.1", and "::1" in the domain portion of the user name.

You can test this logic with something like abc\def for the user name and 1234 as the password. Remember that Get-Credential does not validate the credential you type in. Therefore, you can test for credentials and encryption using any values you like to generate a test MOF file.

PSDscAllowDomainUser

In our DSC Group resource example above, we are querying an Active Directory domain. This requires the use of a domain account. In this case we will add the PSDscAllowDomainUser property to the ConfigurationData block as follows:

 $cd = @{
    AllNodes = @(
        @{
            NodeName = 'localhost'
            PSDscAllowDomainUser = $true
            # PSDscAllowPlainTextPassword = $true
            CertificateFile = "C:\PublicKeys\server1.cer"
        }
    )
}

Now when we run the configuration script to generate the MOF file... no errors, no warnings. Notice in this example we used encryption with a public key certificate.

Life is good. The warning is gone. And your resume-generating event has been averted with credential encryption. Just remember to prefer local accounts whenever possible with DSC resource credential properties.

Tell your peers.

In this post you learned how to read DSC source code to find your own answers. You also solved the mystery of those ConfigurationData block keywords for credentials. Go tell your peers and friends what you found. Use the Twitter and Facebook links below.

This will make great conversation with the family over a glass of egg nog.