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?
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:
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?
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...
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:
and the text file ends up containing:
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? 😉