SecretManagement icon indicating copy to clipboard operation
SecretManagement copied to clipboard

Multi-Thread\Runspace usage

Open adamdriscoll opened this issue 3 years ago • 5 comments

I've realized that the secret management module is not thread safe. In PowerShell Universal, we create a new runspace when accessing secret management, but it doesn't matter because secret management maintains its own static runspace. This results in errors like this:

2021-12-16 15:15:12.669 -08:00 [ERR] Failed to read secret: Unable to get secret Executor from vault BuiltInLocalVault
System.Management.Automation.PSInvalidOperationException: Unable to get secret Executor from vault BuiltInLocalVault
 ---> System.Management.Automation.PSInvalidOperationException: The pipeline was not run because a pipeline is already running. Pipelines cannot be run concurrently.
   at System.Management.Automation.Runspaces.PipelineBase.DoConcurrentCheck(Boolean syncCall, Object syncObject, Boolean isInLock)
   at System.Management.Automation.Runspaces.RunspaceBase.DoConcurrentCheckAndAddToRunningPipelines(PipelineBase pipeline, Boolean syncCall)
   at System.Management.Automation.Runspaces.PipelineBase.CoreInvoke(IEnumerable input, Boolean syncCall)
   at System.Management.Automation.Runspaces.PipelineBase.Invoke(IEnumerable input)
   at System.Management.Automation.PowerShell.Worker.ConstructPipelineAndDoWork(Runspace rs, Boolean performSyncInvoke)
   at System.Management.Automation.PowerShell.Worker.CreateRunspaceIfNeededAndDoWork(Runspace rsToUse, Boolean isSync)
   at System.Management.Automation.PowerShell.CoreInvokeHelper[TInput,TOutput](PSDataCollection`1 input, PSDataCollection`1 output, PSInvocationSettings settings)
   at System.Management.Automation.PowerShell.CoreInvoke[TInput,TOutput](PSDataCollection`1 input, PSDataCollection`1 output, PSInvocationSettings settings)
   at System.Management.Automation.PowerShell.Invoke(IEnumerable input, PSInvocationSettings settings)
   at Microsoft.PowerShell.SecretManagement.PowerShellInvoker.InvokeScriptWithHost[T](PSCmdlet cmdlet, String script, Object[] args, Exception& terminatingError) in D:\a\_work\1\s\src\code\Utils.cs:line 1546
   --- End of inner exception stack trace ---

I'll put some synchronization code around our service but was wondering if this could be possible in secret management itself.

adamdriscoll avatar Dec 16 '21 23:12 adamdriscoll

I think this was also causing a process crash.

image

adamdriscoll avatar Dec 16 '21 23:12 adamdriscoll

I'm happy to create a PR for this since it's really causing some problems with my customers. What I really need is two-fold.

  • The ability to manage runspace creation for PowerShellInvoker
  • The ability to create a runspace per call to the secret management module in a non-static way

What I'm considering doing is providing some way to register a Runspace factory method with the PowerShellInvoker class and if it exists, then it will be used to create runspaces on every call.

If it doesn't exist, then the existing functionality will be used as to not change the behavior of the module. I have to imagine my use-case is not very widely used.

adamdriscoll avatar Dec 22 '21 14:12 adamdriscoll

Sorry for the delayed response. Yes, I think this can be accommodated. I went with static runspaces for importing/running extension vaults to minimize resource/perf issues, and also I was thinking only of single threaded use.

It seems the best way to do this is via SecretManagement configuration scoped to current user, and will require new Set/Get Configuration cmdlets. I don't know when I'll have time to do the work, but if you create a PR, I'll be happy to review.

PaulHigin avatar Jan 12 '22 23:01 PaulHigin

/cc: @anamnavi

PaulHigin avatar Jan 12 '22 23:01 PaulHigin

Would managing runspace creation as suggested resolve the issue of accessing the vault across multiple threadjobs as in the example below?

using namespace System.Collections.Generic
using namespace ThreadJob

Import-Module ThreadJob

$secretNames = @(
    [Array of the names of several secrets]
)

$storeAuthPath = [path to exported vault password]

$script = {
    param(
        [Parameter(Mandatory=$true)]
        [string]$storeAuthPath,
        [Parameter(Mandatory=$true)]
        [string]$secretName
    )
    $ErrorActionPreference = 'Stop'
    Import-Module Microsoft.PowerShell.SecretManagement
    Import-Module Microsoft.PowerShell.SecretStore
    
    try {
        Get-Secret -Name $secretName
    }
    catch {        
        $password = Import-Clixml -Path $storeAuthPath
        Unlock-SecretStore -Password $password -PasswordTimeout 5
        Get-Secret -Name $secretName
    }
}

$jobs = New-Object List[ThreadJob]

foreach ($secretName in $secretNames) {
    $job = Start-ThreadJob -ScriptBlock $script -ArgumentList(@($storeAuthPath,  $secretName))
    $jobs.Add($job)
}

$secret = $jobs | Receive-Job -Wait

$secret | ConvertFrom-SecureString -AsPlainText

JTDotNet avatar May 24 '24 11:05 JTDotNet