Skip to content

Commit

Permalink
Add SecretManagement support (#406)
Browse files Browse the repository at this point in the history
  • Loading branch information
rmbolger authored Nov 20, 2021
1 parent bf34634 commit 5fd47aa
Show file tree
Hide file tree
Showing 13 changed files with 875 additions and 103 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/pester.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jobs:
- uses: actions/checkout@v2
- name: Run Pester tests
run: |
Install-Module Microsoft.PowerShell.SecretManagement
Import-Module Pester -MinimumVersion 5.2
$cfg = [PesterConfiguration]@{Run=@{Exit=$true}}
Invoke-Pester -Configuration $cfg
Expand All @@ -22,6 +23,7 @@ jobs:
- uses: actions/checkout@v2
- name: Run Pester tests
run: |
Install-Module Microsoft.PowerShell.SecretManagement
Import-Module Pester -MinimumVersion 5.2
$cfg = [PesterConfiguration]@{Run=@{Exit=$true}}
Invoke-Pester -Configuration $cfg
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ Checkout the main branch of the repository and run `mkdocs serve` to self-host a

## Unit Tests

The tests in this project now use [Pester v5](https://pester.dev/docs/quick-start). My overall code coverage is still pretty low. So if you're handy at writing tests or have suggestions to improve the existing ones, suggestions and pull requests are most welcome. The recommended way to run the tests is in a separate PowerShell process due to some limitations in how Pester is able to test internal module stuff. For example:
The tests in this project now use [Pester v5](https://pester.dev/docs/quick-start). You will also need to have the `Microsoft.PowerShell.SecretManagement` module installed, but no vault extension modules or configuration is required.

My overall code coverage is still pretty low. So if you're handy at writing tests or have suggestions to improve the existing ones, suggestions and pull requests are most welcome. The recommended way to run the tests is in a separate PowerShell process due to some limitations in how Pester is able to test internal module stuff. For example:

```powershell
cd \path\to\Posh-ACME
Expand Down
7 changes: 2 additions & 5 deletions Posh-ACME/Private/Export-PluginArgs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,8 @@ function Export-PluginArgs {
# but we have to pre-serialize things like SecureString and PSCredential
# first because ConvertTo-Json can't deal with those natively.

# determine whether we're using a custom key
$encParam = @{}
if (-not [String]::IsNullOrEmpty($acct.sskey)) {
$encParam.Key = $acct.sskey | ConvertFrom-Base64Url -AsByteArray
}
# get the encryption parameter
$encParam = Get-EncryptionParam -Account $acct -EA Stop

$pDataSafe = @{}
foreach ($key in $pData.Keys) {
Expand Down
64 changes: 64 additions & 0 deletions Posh-ACME/Private/Get-EncryptionParam.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
function Get-EncryptionParam {
[OutputType([hashtable])]
[CmdletBinding()]
param(
[Parameter(Mandatory,Position=0)]
[PSTypeName('PoshACME.PAAccount')]$Account
)

# return early if sskey is empty or not defined
if ([String]::IsNullOrEmpty($Account.sskey)) {
return @{}
}

if ('VAULT' -ne $Account.sskey) {
# an sskey value of anything except 'VAULT' should mean the key string
# is directly attached to the account object
$keyString = $Account.sskey
}
else {
# retrieve the key from the SecretManagement Vault if possible

# make sure we have the necessary SecretManagement commands available
if (-not (Get-Command 'Unlock-SecretVault' -EA Ignore) -or
-not (Get-Command 'Get-Secret' -EA Ignore) )
{
Write-Error "Unable to retrieve encryption key. Commands associated with SecretManagement module not found. Make sure Microsoft.PowerShell.SecretManagement is installed and accessible." -Category 'NotInstalled'
return @{}
}

# make sure we have a vault name
$vaultName = $env:POSHACME_VAULT_NAME
if ([string]::IsNullOrWhiteSpace($vaultName)) {
Write-Error "Unable to retrieve encryption key. SecretManagement Vault name not found. Make sure POSHACME_VAULT_NAME and related environment variables are defined." -Category 'ObjectNotFound'
return @{}
}

# build the secret name
if ([String]::IsNullOrEmpty($env:POSHACME_VAULT_SECRET_TEMPLATE)) {
$secretName = 'poshacme-{0}-sskey' -f $Account.VaultGuid
} else {
Write-Debug "Using custom secret template: $($env:POSHACME_VAULT_SECRET_TEMPLATE)"
$secretName = $env:POSHACME_VAULT_SECRET_TEMPLATE -f $Account.VaultGuid
}

# if a vault password is defined, explicitly unlock the vault
if (-not [string]::IsNullOrEmpty($env:POSHACME_VAULT_PASS)) {
$ssPass = ConvertTo-SecureString $env:POSHACME_VAULT_PASS -AsPlainText -Force
Unlock-SecretVault -Name $vaultName -Password $ssPass
}

# Attempt to get the key
try {
Write-Debug "Attempting to retrieve secret '$secretName' from vault '$vaultName'"
$keyString = Get-Secret -Vault $vaultName -Name $secretName -AsPlainText -EA Stop
} catch {
$PSCmdlet.WriteError($_)
return @{}
}
}

# return the hydrated key as a hashtable to splat
$keyBytes = $keyString | ConvertFrom-Base64Url -AsByteArray
return @{ Key = $keyBytes }
}
144 changes: 144 additions & 0 deletions Posh-ACME/Private/Set-AltPluginEncryption.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
function Set-AltPluginEncryption {
[CmdletBinding()]
param(
[Parameter(Mandatory,Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
[Alias('Name')]
[string]$ID,
[Parameter(Mandatory)]
[switch]$Enable,
[switch]$Reset
)

Begin {
# make sure we have a server configured
if (-not ($server = Get-PAServer)) {
throw "No ACME server configured. Run Set-PAServer first."
}

# save the current account to revert to if necessary
$revertToAccount = Get-PAAccount
}

Process {

# set the specified account as current
if (-not $revertToAccount -or $revertToAccount.id -ne $ID) {
Write-Debug "Temporarily switching to account '$ID'"
Set-PAAccount -ID $ID
}

# return early if there's nothing to do
$oldSSKey = $script:Acct.sskey
if ($Enable -and -not $Reset -and -not [String]::IsNullOrWhiteSpace($oldSSKey)) {
Write-Debug "AltPluginEncryption is already enabled on account '$ID'."
return
} elseif (-not $Enable -and [String]::IsNullOrWhiteSpace($oldSSKey)) {
Write-Debug "AltPluginEncryption is already disabled on account '$ID'."
return
}

# grab a copy of the orders and plugin args before we break
# the ability to decrypt them
$orderData = @(Get-PAOrder -List | ForEach-Object {
@{
Order = $_
PluginArgs = ($_ | Get-PAPluginArgs)
}
})
Write-Debug "Order data found for $($orderData.Count) orders."

if ($Enable) {

# generate a new key in case we need it
$newSSKey = New-AesKey

# check for vault config
if (-not [string]::IsNullOrWhiteSpace($env:POSHACME_VAULT_NAME)) {
try {
$vaultName = $env:POSHACME_VAULT_NAME

# make sure we have the necessary SecretManagement commands available
if (-not (Get-Command 'Unlock-SecretVault' -EA Ignore) -or
-not (Get-Command 'Get-Secret' -EA Ignore) )
{
throw "Commands associated with SecretManagement module not found. Make sure Microsoft.PowerShell.SecretManagement is installed and accessible."
}

# if a vault password is defined, explicitly unlock the vault
if (-not [string]::IsNullOrEmpty($env:POSHACME_VAULT_PASS)) {
$ssPass = ConvertTo-SecureString $env:POSHACME_VAULT_PASS -AsPlainText -Force
Unlock-SecretVault -Name $vaultName -Password $ssPass
}

# get or create the vault guid
$vaultGuid = $script:Acct.VaultGuid
if ([string]::IsNullOrWhiteSpace($vaultGuid)) {
$vaultGuid = (New-Guid).ToString().Replace('-','')
}

# build the secret name
if ([String]::IsNullOrEmpty($env:POSHACME_VAULT_SECRET_TEMPLATE)) {
$secretName = 'poshacme-{0}-sskey' -f $vaultGuid
} else {
Write-Debug "Using custom secret template: $($env:POSHACME_VAULT_SECRET_TEMPLATE)"
$secretName = $env:POSHACME_VAULT_SECRET_TEMPLATE -f $vaultGuid
}

# check for an existing key value
$oldSecret = Get-Secret -Vault $vaultName -Name $secretName -AsPlainText -EA Ignore

if ($Reset -or -not $oldSecret) {
# attempt to write a new vault key
Write-Debug "Attempting to add new secret '$secretName' to vault '$vaultName'."
Set-Secret -Vault $vaultName -Name $secretName -Secret $newSSKey -EA Stop
Write-Verbose "Enabling AltPluginEncryption for account '$ID' with new vault key."
} else {
# use the existing vault key
Write-Verbose "Enabling AltPluginEncryption for account '$ID' with existing vault key."
$newSSKey = $oldSecret
}

$script:Acct | Add-Member 'sskey' 'VAULT' -Force
$script:Acct | Add-Member 'VaultGuid' $vaultGuid -Force
}
catch {
Write-Warning "Unable to save encryption key to secret vault. $($_.Exception.Message)"

# just save the key onto the account
Write-Debug "Saving account $ID with new sskey."
$script:Acct | Add-Member 'sskey' $newSSKey -Force
}
} else {
# just save the key onto the account
Write-Debug "Saving account $ID with new sskey."
$script:Acct | Add-Member 'sskey' $newSSKey -Force
}

} else {
# remove the key
Write-Verbose "Disabling AltPluginEncryption for account '$ID'"
$script:Acct | Add-Member 'sskey' $null -Force
}

$acctFile = Join-Path $server.Folder "$ID\acct.json"
$script:Acct | Select-Object -Property * -ExcludeProperty id,Folder |
ConvertTo-Json -Depth 5 |
Out-File $acctFile -Force -EA Stop

# re-export all the plugin args
$orderData | ForEach-Object {
Write-Debug "Re-exporting plugin args for order '$($_.Order.Name)' with plugins $($_.Order.Plugin -join ',') and data $($_.PluginArgs | ConvertTo-Json -Depth 5)"
Export-PluginArgs @_ -IgnoreExisting
}
}

End {
$curAcct = Get-PAAccount
if ($revertToAccount -and
(-not $curAcct -or ($curAcct.id -ne $revertToAccount.id) ))
{
Write-Debug "Reverting to previously active account '$($revertToAccount.id)'"
Set-PAAccount -ID $revertToAccount.id
}
}
}
61 changes: 0 additions & 61 deletions Posh-ACME/Private/Update-PluginEncryption.ps1

This file was deleted.

9 changes: 3 additions & 6 deletions Posh-ACME/Public/Get-PAPluginArgs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function Get-PAPluginArgs {
}

Process {
trap { $PSCmdlet.ThrowTerminatingError($PSItem) }
trap { $PSCmdlet.WriteError($_); return }

# try to find an order using the specified parameters
$order = Get-PAOrder @PSBoundParameters
Expand All @@ -42,11 +42,8 @@ function Get-PAPluginArgs {
# drop support for PS 5.1.
$pDataSafe = Get-Content $pDataFile -Raw -Encoding utf8 -EA Ignore | ConvertFrom-Json

# determine whether we're using a custom key
$encParam = @{}
if (-not [String]::IsNullOrEmpty($acct.sskey)) {
$encParam.Key = $acct.sskey | ConvertFrom-Base64Url -AsByteArray
}
# get the encryption parameter
$encParam = Get-EncryptionParam -Account $acct -EA Stop

# Convert it to a hashtable and do our custom deserialization on SecureString
# and PSCredential objects.
Expand Down
11 changes: 6 additions & 5 deletions Posh-ACME/Public/New-PAAccount.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,6 @@ function New-PAAccount {
Folder = Join-Path $server.Folder $ID
}

# add a new AES key if specified
if ($UseAltPluginEncryption) {
$acct.sskey = New-AesKey
}

# save it to memory and disk
$acct.id | Out-File (Join-Path $server.Folder 'current-account.txt') -Force -EA Stop
$script:Acct = $acct
Expand All @@ -214,5 +209,11 @@ function New-PAAccount {
ConvertTo-Json -Depth 5 |
Out-File (Join-Path $acct.Folder 'acct.json') -Force -EA Stop

# add a new AES key if specified
if ($UseAltPluginEncryption) {
$acct | Set-AltPluginEncryption -Enable
$acct = Get-PAAccount
}

return $acct
}
25 changes: 6 additions & 19 deletions Posh-ACME/Public/Set-PAAccount.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -98,28 +98,15 @@ function Set-PAAccount {

$saveAccount = $false

# deal with local changes
# deal with encryption changes
if ('UseAltPluginEncryption' -in $PSBoundParameters.Keys -or $ResetAltPluginEncryption) {

# UseAltPluginEncryption takes precedence over ResetAltPluginEncryption
# So if Use:$false + Reset is specified, the key is removed rather than rotated

if ([String]::IsNullOrEmpty($acct.sskey) -and $UseAltPluginEncryption)
{
Write-Verbose "Adding new sskey for account $($acct.ID)"
Update-PluginEncryption $acct.ID -NewKey (New-AesKey)
}
elseif (-not [String]::IsNullOrEmpty($acct.sskey) -and
'UseAltPluginEncryption' -in $PSBoundParameters.Keys -and
-not $UseAltPluginEncryption)
{
Write-Verbose "Removing sskey for account $($acct.ID)"
Update-PluginEncryption $acct.ID -NewKey $null
}
elseif (-not [String]::IsNullOrEmpty($acct.sskey) -and $ResetAltPluginEncryption) {
Write-Verbose "Changing sskey for account $($acct.ID)"
Update-PluginEncryption $acct.ID -NewKey (New-AesKey)
$encSplat = @{
Enable = $UseAltPluginEncryption.IsPresent
Reset = $ResetAltPluginEncryption.IsPresent
}
if ($encSplat.Reset) { $encSplat.Enable = $true }
$acct | Set-AltPluginEncryption @encSplat
}

# deal with server side changes
Expand Down
Loading

0 comments on commit 5fd47aa

Please sign in to comment.