Skip to main content
added 136 characters in body
Source Link
briantist
  • 1.8k
  • 13
  • 9

Additionally, using CliXML with [PSCredential] completely avoids dealing directly with secure strings and the various conversions.

Additionally, using CliXML with [PSCredential] completely avoids dealing directly with secure strings and the various conversions.

Source Link
briantist
  • 1.8k
  • 13
  • 9

One of the main problems here I think is that the encrypted version of the password can only be decrypted by the account who ran the unencrypted configuration first, on the same computer.

Your current configuration format has no provision for this.

If that's ok, then why ever write the unencrypted credentials to disk? Let the unencrypted <Password /> element be blank and force a prompt on first run to fix it or don't allow a run without it being encrypted (force a Protect-Config call first).

If you need to allow for multiple encrypted versions of the same credential to account for multiple accounts reading the config (and I strongly recommend you account for this), then you either need multiple config files (where everything but the credential is redundant) or you need to change your configuration format.


Is there a reason you need to define your own configuration format? Is there a reason it needs to be XML? There are lots of alternatives.

Serialization

PowerShell has pretty good object serialization that, critically for this purpose, supports [PSCredential] natively.

Additionally there's a file format that I think you are already familiar with, .psd1 which is basically just supports a very stripped down, restricted language version of a PowerShell script, which is used to store a [HashTable] for configuration data.

You can use Import-PowerShellDataFile to get yourself a [HashTable] from this file format directly.

You can also define a [hashtable] type parameter on a function, and then use the [ArgumentToConfigurationDataTransformation()] attribute on it. This means you can directly pass a [hashtable], or you can pass a .psd1 filename to it and it will be automatically be read as a configuration file.

Going back to XML, if you store your data in a [hashtable] initially, you can use CliXML as your configuration format (direct serialization).

So given:

$myConfig = @{
    MyDatabase = @{
        Instance = 'MyInstance'
        Catalog = 'MyCatalog'
    }
    Path = 'C:\MyPath'
}

Maybe you just directly do:

$myConfig | Export-CliXml -LiteralPath C:\myConfig.xml

It's still human readable, though less modifyable directly. On the other hand it gives you the ability to store some complex objects.


Credential Storage

Let's take another step back and think about where you really want to be storing your credentials; custom config format or not.

I like the idea having credentials be stored in separate files. Your config can refer to them either by full path, or by a name (key?) that is used to build the path; I especially like if they are in a specific (sub?) directory.

This allows for little change to your current config format, but adds flexibility for multiple users and computers, and for the ability to completely exclude your credentials from source control which makes sense even when they are encrypted because they are only good on a single computer.

Let's see what this would look like.

Directory Structure:

.\Project
|__ Config
|____ myConfig.xml
|__ Credentials
|____ cred_Account1_adminUser_thisComputer.xml
|____ cred_Account1_svcAcct_thisComputer.xml

(probably want to add Credentials to .gitignore or equivalent)

myConfig.xml

<?xml version="1.0" encoding="utf-8"?>
<Config>
  <MyDatabase>
    <Credential Name="Account1" />
    <DbInstance>MyServer\MyInstance</DbInstance>
    <DbCatalog>MyCatalog</DbCatalog>
  </MyDatabase>
  <Path>\\server\share\subfolder</Path>
</Config>

Code:

function Import-MyCredential {
[CmdletBinding()]
[OutputType([PSCredential])]
param(
    [Parameter(Mandatory, ValueFromPipeline)]
    [ValidateNotNullOrEmpty()]
    [String]
    $AccountName ,
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [String]
    $BasePath = $PSScriptRoot
)

    Process {
        $fileName = "cred_${AccountName}_${env:USERNAME}_${env:COMPUTERNAME}.xml"
        $file = $BasePath | Join-Path -ChildPath 'Credentials' | Join-Path -ChildPath $fileName

        Import-CliXml -LiteralPath $fileName
    }
}

This is your helper function to build the path from the key, based on running user and computer, and get you back a [PSCredential].

Now, I really don't like working directly with XML so I'm not going to rewrite your existing functions to deal with the new style of <Credential> element but you clearly have the skill to do so.

Storing Credentials

The next question then is how to store credentials. I strongly encourage you not to allow reading a plaintext password from disk even if just to rewrite it. Don't encourage that.

The process of writing the above style files is very simple:

$cred = Get-Credential
$cred | Export-CliXml -LiteralPath ("$PSScriptRoot\Credentials\cred_${AccountName}_${env:USERNAME}_${env:COMPUTERNAME}.xml")

(of course parameterize and clean that up)

The question then, is what is the workflow to get there?

  • Let the config refer to creds that don't exist; then prompt to write the cred file.
  • Allow (or require) writing the cred file first.

That's up to you and depends on how you want things to go.

To reiterate, I don't think allowing the writing of plaintext adds any benefit to outweigh its risks and drawbacks. If ok with an admin rewriting a password in a config file then they can be expected to have to run a command once to properly prompt for it and store it encrypted instead.