Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2015/wait-who-is-dsc-running-as-again

Wait, who is DSC running as again?

Published 23 November 2015
Updated 25 August 2016

Having been working on more automation with PowerShell DSC in the last week, I hit upon an interesting issue. For many operations, it doesn't really matter what user your script is executing as. Most local operations that only affect the current machine just work. However, every so often you come across an operation that you need to perform as a specific user. So how can you impersonate a different user for parts of your scripts?

What's the default user context?

The first question to address is what account does an "ordinary" script run as? We can write a simple Script resource that outputs the details of the current user. For example:

Script ShowUser
{
    GetScript = { }
    TestScript = { $False }
    SetScript = {
        write-verbose "User: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
        write-verbose "Current User: $env:UserName"
    }
}

					

The GetScript and TestScript blocks here aren't important. The SetScript block is just displaying the current user in two different ways - using the current Windows security principle object, and the current environment variable for the user name. What do we get? This:

Default User

The code is running as NT AUTHORITY\SYSTEM - the current computer's account. That account lacks certain security rights. The obvious rights it doesn't have is network access, but as I discovered recently I also can't grant the "log on a service" right to other user accounts via a process running like that. So how can you run code as different users?

How do we get the credential details of another user?

PowerShell provides a data type for dealing with user credentials: the PSCredential type. The simplest way to get some credentials is to use the Get-Credential commandlet. This will prompt the current user to enter some credentials. Because it's interactive, you can't do this in the .MOF - only DSC script that generates the .MOF file. Hence the call has to go outside the Configuration block in your script, and the credential needs to be passed into it.

To achieve this, we need to add a new parameter to the DSC script, to allow us to pass in our chosen credential:

Configuration ExampleCredentials
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $PackagePath,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $CustomCredential
    )
 
    Node $AllNodes.where{ $_.Role.Contains("Required-Role-Name") }.NodeName
    {
       #
       # Resources go here...
       #
    }
}

					

When the DSC Script is executed, the Get-Credentials Commandlet can capture a credential to pass in as the last parameter for the call to the DSC script:

ExampleCredentials -ConfigurationData "\\vboxsvr\DSC\configData.psd1" -PackagePath "\\VBOXSVR\DSC\location" -CustomCredential (Get-Credential)
Start-DscConfiguration -Path .\ExampleCredentials -Wait -Force -Verbose 

					

Now, while this prevents you from having to hard-code credentials into your DSC script, remember that the data is saved in your .MOF file. Hence this isn't secure. In order to persuade PowerShell that you really mean to do this, you need to add an extra bit of data to your Node configuration data:

@{
    AllNodes = @(
        @{
            NodeName = "*"
            PSDscAllowPlainTextPassword = $true
        }
        #
        # Other node data goes here
        #
    );
}

					

Here, the PSDscAllowPlainTextPassword setting has been applied to any Node in the data file, allowing the insecure credentials.

The work I'm doing with DSC is all for development environments at present, hence the credentials in these files are not really secret. However, if you you need to include credentials that should be kept secret, you are able to encrypt .MOF files using certificates.

But what if you need your scripts to run unattended? How can you capture credentials if there's no user around to enter them? Simple enough. One appraoch is to create the Credential object manually, and pass that to the DSC invocation instead:

$username = "SomeDomain\AnyUser"
$password = ConvertTo-SecureString "your-password" -Force -AsPlainText
$credential = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $password

ExampleCredentials -ConfigurationData "\\vboxsvr\DSC\configData.psd1" -PackagePath "\\VBOXSVR\DSC\location" -CustomCredential $credential
Start-DscConfiguration -Path .\ExampleCredentials -Wait -Force -Verbose 

					

Obviously, this has further security implications, since your credentials are now in plain text in both the DSC script and the .MOF file. So handle with care...

While I was testing these approaches I encountered some odd behaviour. I found that the second approach worked more reliably for the script I was running than the first one did. Intuitively this seems wrong to me – hence it was probably my fault and caused by something else I was doing incorrectly in my tests. I need to find more research time for this, and work out the cause of the issues I saw...

How can we use this credential?

Quite a few DSC resources can have a credential object passed to them, which they will try to use for impersonation. For example, you can run a script using a specific credential:

Script CredentialExample
{
    Credential = $CustomCredential
    GetScript = { }
    TestScript = { $False }
    SetScript = {
        #
        # Actions go here
        #
    }
}

					

Note the use of the Credential property, passing in the value of the $CustomCredential parameter we give to the DSC script when it runs.

Another aspect of this that I've not quite understood yet, is that once you pass in credentials to a script resource like this, some operations no longer work. For example, the Write-Verbose commandlet will not display results. For example, consider this script:

Script CredentialExample
{
    Credential = $CustomCredential
    GetScript = { }
    TestScript = { $False }
    SetScript = {
        $env:UserName | Out-File "c:\dsc\username.txt" -Encoding ASCII
        Write-Verbose "$env:UserName"
    }
}

					

This tries to do two things: Write the current user's name to a text file, and write it to the screen. When it runs, the screen display is:

No User

and the text file ends up containing:

Text File

So, despite the lack of the expected on-screen output, the correct file is written to disk – meaning the code did run as the correct user.

A second alternative to running a script block (or other basic resource) in this way, is to run an external program as a different user. This isn't natively supported by PowerShell DSC, but an extension is available which can allow it. If you download an install the xPSDesiredStateConfiguration extension module, you get a new Resource named xWindowsProcess. This allows any Windows program or PowerShell script to be run as a particular user. For example, we can write a Script resource to generate a PowerShell script, and then execute it as a different user:

Script GenerateScript
{
    GetScript = { }
    TestScript = { $False }
    SetScript = {
        $tmp = "$($using:Node.TempFolder)"
        $file = "$tmp\_TestImpersonation.ps1"           
        "whoami > $tmp\_TestImpersonation.txt" | Out-File $file -Encoding ASCII
    }
}

					

This writes a .ps1 script file to our DSC temporary directory, which does one thing: It pipes the result of the whoami command to a second text file. This will show that the script ran, and record which user it ran as. Note that this resource is not being passed a credential.

Next you need to import the module you downloaded above. Immediately before the Node line in your DSC script, add the import statement:

Import-DSCResource -ModuleName xPSDesiredStateConfiguration

					

Then we can add an xWindowsProcess resource to execute a task - in this case the PowerShell script we output above:

xWindowsProcess RunScript
{
    DependsOn = "[script]GenerateScript"
    Path = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
    Arguments = "-NonInteractive -File `$($Node.TempFolder)\_TestImpersonation.ps1`
    Credential = $CustomCredential
    Ensure = "Present"
}

					

This fires up a new instance of the PowerShell runtime as the user we specified with out credentials. It specifies that PowerShell has to run in a non-interactive session, and that it should execute the script created in the previous step.

When this runs, you end up with a script written to disk which then writes a text file confirming that user name the script was run as. This is a silly example – but It shows you can run most tasks or processes using the same approach.

While this approach works for a PowerShell script, it does not seem to work correctly for a batch file. An attempt to execute batch files, or to run the cmd.exe shell and pass it a batch file will return errors. The batch file appears to be executed ok, but the DSC Resource seems unable to detect the child process running the script correctly - hence it can return erroneous errors about being unable to start the script. That means DSC doesn't wait for the batch file to complete before executing subsequent resources, which will probably lead to race conditions in your scripts. But you don't write batch files any more, do you? 😉

↑ Back to top