A few years ago, I started working on a project to automate deployments of various types of servers. Some of the server types (app/web servers) were simple; others, like Active Directory domain controllers and SQL Servers were a bit more complicated. Those involved the use of some sort of secret - safe mode password, service account, etc. Supplying the needed secrets at runtime was not an option, as I wanted a fully automated solution. In addition, I needed the option to retrieve the secrets at a later date. As I wasn’t the only one initiating the deployments, options like
Get-Credential | ConvertTo-SecureString -AsPlainText -Force | Export-CliXml weren’t an option due to per-user encryption. There had to be a better way.
The Early Years - Keystore v1
After scouring the web, I stumbled across a GitHub repo by Johan Andersson with a PowerShell module that laid out the foundation of what I turned into version 1.0 of the Keystore. The initial concepts were: - Use NTFS file share to store files - File names were SHA1 hashed to obscure the contents of the file - File must contain a username and password, separated by a colon - File was encrypted with a shared certificate - The file contained two lines of text: First line contained thumbprint of the certificate used to encrypt the second line; Second line contained username and password encrypted string Using a self-signed certificate that was only installed on workstations managed by our team, I was able to store a username/password combo and retrieve it as a PSCredential object, perfect for our automation tasks! At first there were only a handful of keystore credentials, but eventually I integrated the storing and retrieval of credentials into processes like BizTalk and SQL Server deployments, both of which require the use of domain users as service accounts.
After a few months, I started to run into a few limitations with the initial implementation (v1):
- Special characters. When a new service account is created, a random password is generated using the .NET API. However, certain characters in the password made it difficult to store, especially the colon character, as that was what one of the functions (Get/New-KeystoreCredential) was using as a string separator for the username and password. Before the string was encrypted, the username and password strings were combined with a colon separator. When the string was decrypted, it was split by the colon character and assumed there would only be two members of the resulting array. This, of course, caused the function to return the incorrect password. I put in a workaround to remove any characters we deemed “restricted” and especially unfriendly to XML. That at least kept me moving forward.
- Search. There was no way to search for keystore credentials; you had to know the exact name as when it was created. Since the filenames were SHA-1 hashed, any variance in the casing when searching would mean an entry was not found. As a workaround, I forced the filename to uppercase both when searching and when saving. However, I couldn’t (easily or quickly) get all Keystore credentials that belonged to, say, a particular server. This lead to limitation #3.
- Speed. The functions to encrypt/decrypt the strings were extremely slow, and since a SHA-1 hash can’t be “un-hashed”, the only way to find a group of keystore credentials was to, one by one, decrypt each file and match on the username, which I had set by default to be the same as the file name. Each decryption pass took, on average, about 400-500ms. At one point there were almost 3,000 files and it took more than 20 minutes to search through all of them!
- Non-credential data. At this point the only type of data that could be stored was a username/password combo. This proved to be an issue when working with web app deployments that required having sensitive information as app settings in the web.config files. While technically possible to store the app setting strings in the username or password field, it just felt…wrong.
- Separation of non-production vs. production credentials. Up to this point, only one certificate was used to encrypt/decrypt the credentials, and the certificate thumbprint was hardcoded in the module. However, I needed a way to allow for other users to get to certain secrets while keeping production ones separate.
In mid-2016, I started the process of redesigning the keystore with the hopes of solving issues 2-5; special characters weren’t really an issue with the exclusion process I implemented. PowerShell 5.0 had been released and there were awesome new cmdlets that dealt with encryption (
Protect/Unprotect-CmsMessage) that piqued my curiosity. In addition, the Azure KeyVault had also just been released and contained cool ideas of how I could improve my implementation of Keystore. Enter Keystore v2. It has been revamped to fix all of the limitations I ran into thus far. Here are some of the new features and improvements.
There are now two Keystore item types: Generic Secret (simple string of text) and Credential (username/password pairing). For credential items, the item (file) name can be different than username.
The new file format uses JSON and is validated against a schema. This allows attributes about an item to be retrieve very quickly without having to decrypt the secret value.
The secret value is now encrypted using Protect-CmsMessage, which is much faster than the original method. In my tests, it went from ~500ms down to ~20-40ms. Also, since JSON is unfriendly to line breaks, the output of Protect-CmsMessage is converted to a Base-64 encoded string.
Now, more than one certificate is supported when encrypting secrets. To make it easier to recall, you can assign a friendly name to a certificate thumbprint.
Originally, the default Keystore store file path was hard-coded with the option to override via a path. Now, you can save one or more custom paths with a friendly name and refer to it by using the StoreName parameter. Two new built-in stores are now available: Self (see below) and CurrentDirectory. CurrentDirectory automatically makes whatever folder you are currently in the default location for storing Keystore items. You still have the option of specifying a file path using the FilePath parameter.
One enhancement I added is similar in concept to what the Windows credential manager provides: the ability to have a per-user Keystore store where you can store your own secrets and not have other people have access to it. The Self store uses a self-signed document encryption certificate that is stored in the current user’s Personal certificate store. The private key is marked as non-exportable, meaning even Administrators on the same system cannot decrypt your secrets.
I plan to publish the module to the PowerShell Gallery at some point, but for now you can get the Keystore module from the GitHub repo. Check it out and feel free to leave me feedback/comments/suggestions.