I’ve been working with a PowerShell script to automatically deploy an application to an environment. The script is initiated on one machine and uses PowerShell Remoting to perform the install on one or more target machines. On the target machines the install process needs the username and password of the service account that the application will be configured to run as.
I despise handling passwords in my scripts and applications so I avoid it wherever possible, and where I can’t avoid it, I make sure I handle them securely. And this is where the fun starts.
By default, PowerShell Remoting suffers from the same multi-hop authentication problem as any other system using Windows security, i.e. the credentials used to connect to the target machine cannot be used to connect from the target machine to another resource requiring authentication. The most promising solution to this in PowerShell Remoting is CredSSP which enables credentials to be delegated but it has some challenges:
- It is not enabled by default, you need to execute Enable-WSManCredSSP or use Group Policy to configure the involved machines to support CredSSP.
- It is not available on Windows XP or Server 2003, a minor concern given that these OSes should be dead by now, but worth mentioning.
- PS Remoting requires CredSSP to be used with “fresh” credentials even though CredSSP as a technology supports delegating default credentials (this is how RDP uses CredSSP).
It is the last point about fresh credentials that kills CredSSP for me, I don’t want to persist another password for my non-interactive script to use when establishing a remoting connection. There is a bug on Microsoft Connect about this that you can vote up.
Instead I need to revert to the pre-CredSSP (and poorly documented) way of supporting multi-hop authentication with PS Remoting: Delegation. You basically require at least two things to be configured correctly in Active Directory for this to work.
- The user account that is being used to authenticate the PS Remoting session must have its AD attribute “Account is sensitive and cannot be delegated” unchecked.
- The computer account of the machine PS Remoting is connecting to must have either the “Trust this computer for delegation to any service” option enabled or have the “Trust this computer for delegation to specified services only” option enabled with a list of which services on which machines can passed delegated credentials. The latter is more secure if you know which services you’ll need.
I’m not sure which caches needed to expire because it took a while for these changes to start working for me but once they did After the PS Remoting target computer refreshed its Kerberos ticket (every 10 hours by default) I could use PS Remoting with the default authentication method (Kerberos) and could authenticate to resources beyond the connected machine. Rebooting the target computer or using the “klist purge” command against the target computer’s system account will force the Kerberos ticket to be refreshed.
With that hurdle overcome, the fun continues with the handling of the application’s service account credentials. PowerShell’s ConvertTo- and ConvertFrom-SecureString cmdlets enable me to encrypt the service account password using a Windows-managed encryption key specific to the current user, in my case this is the user performing the deployment and authenticating the PS Remoting session. As a one-time operation I ran an interactive PowerShell session as the deployment user and used `Read-Host -AsSecureString | ConvertFrom-SecureString` to encrypt the application service account password and I stored the result in a file alongside the deployment script.
At deployment time, the script uses ConvertTo-SecureString to retrieve the password from the encrypted file and configure the application’s service account. At least it worked with my interactive proof-of-concept testing. It failed to decrypt the password at deployment time with the error message “Key not valid for use in specified state”. After quite some digging I found the culprit.
The Convert*-SecureString cmdlets are using the DPAPI and the current user’s key when a key isn’t specified explicitly. DPAPI is dependent on the current user’s Windows profile being loaded to obtain the key. When using Enter-PSSession to do my interactive testing, the user profile is loaded for me but when using Invoke-Command inside my deployment script, the user profile is not loaded by default so DPAPI can’t access the key to decrypt the password. Apparently this is a known issue with impersonation and the DPAPI. It’s also worth noting that when using the DPAPI with a user key for a domain user account with a roaming profile, I found it needs to authenticate to the domain controller, a nice surprise for someone trying to configure delegation to specific services only.
My options now appear to be one of:
- Use the OpenProcessToken and LoadUserProfile win32 API functions to load the user profile before making DPAPI calls.
- Ignore the Convert*-SecureString cmdlets and call the DPAPI via the .NET ProtectedData class so I can use the local machine key for encryption instead of the current user’s key.
- Decrypt the password outside the PS Remoting session and pass the unencrypted password into the Remoting session ready to be used. I don’t like the security implications of this.
I’ll likely go with option (2) to get something working as soon as possible and look into safely implementing option (1) when I have more time.