PowerShell / SecretManagement

PowerShell module to consistent usage of secrets through different extension vaults
MIT License
317 stars 46 forks source link

Feature: exe to support SSH_ASKPASS #194

Open SteveL-MSFT opened 2 years ago

SteveL-MSFT commented 2 years ago

For automation using SSH and not wanting to deploy keys/certs, it may be useful to have a standalone exe shipped with SecretManagement that could be used with SSH_ASKPASS that emits the secret to stdout. SSH_ASKPASS only takes an executable path with no arguments, so either the exe only uses a pre-defined secret name always or itself uses an env var to get the secret name to retrieve from SecretManagement.

jborean93 commented 2 years ago

An env var would be somewhat dangerous as an admin could easily get that value in the ssh process that's still running exposing the secret. Using some sort of pipe IPC to secretly have PowerShell send the credentials to the ask pass process would be better but is going to be quite hairy to implement securely. In saying that I like the idea for an ASKPASS like utility shipped by SecretManagement.

awakecoding commented 2 years ago

The ideal solution would be to use a one-time named pipe to pass the password through askpass - the named pipe name can easily be passed through an environment variable.

As for connecting to the named pipe and grabbing the password, it could either be done through a pwsh-askpass executable, or optionally bypass the executable to use the named pipe directly within ssh.exe in a patched OpenSSH client for Windows.

The advantage here is that the named pipe server would be the same inside pwsh.exe, but there would be a way to get a more direct askpass injection with a patched OpenSSH client. Unpatched OpenSSH clients, especially those on non-Windows, would always use pwsh-askpass that would read the password from the named pipe and output it on standard output.

Now one difficulty encountered is that AFAIK, PowerShell doesn't build native code, as it is all prebuilt as part of the .NET runtime. A simple pwsh-askpass program in C would work, but getting it integrated into the current build procedure may be difficult because of that.

SteveL-MSFT commented 2 years ago

Just to clarify a bit, the env var I mentioned is only the name of the secret so it would effectively be the same as if you did:

Get-Secret $env:SSH_ASKPASS_SECRETNAME

This is to allow use of different secrets and avoid name collision. The exe would need to host PS7 and literally call the above cmdlet to retrieve the SecureString and emit it as plaintext to stdout.

Jaykul commented 2 years ago

I'm a little confused about the level of nesting executables in this scenario, but ...

If the goal is for remoting and the SSH session is implicit, and the exe is hosting PS7 ... it's not going to be able to use the already unlocked secret vaults, right? I think you've just talked yourself into a situation where you have to prompt for the secret vault password instead of prompting for the SSH password.

awakecoding commented 2 years ago

I'm a little confused about the level of nesting executables in this scenario, but ...

If the goal is for remoting and the SSH session is implicit, and the exe is hosting PS7 ... it's not going to be able to use the already unlocked secret vaults, right? I think you've just talked yourself into a situation where you have to prompt for the secret vault password instead of prompting for the SSH password.

Yes, the intent here would be to inject already unlocked credentials passed through the -Credential parameter currently unsupported with the SSH transport. The thing that may be confusing is that the only way we can pass the credentials down to the child SSH client is with ssh-askpass, a program normally used to interactively prompt the user for credentials. There is no intent to use this interactively for ssh-askpass as it would be used to send credentials already available within the parent PowerShell, at connection time, and explicitly passed with a parameter.

I thought of one limitation of using an ssh-askpass executable: it likely won't work when using the PowerShell SDK, for the same reason PowerShell jobs relying on pwsh.exe cannot work.

Here's my suggestion: inject two environment variables, one for the ssh-askpass program when available, and one for a named pipe. Have the ssh-askpass program read the credentials from a one-time named pipe and print it to stdout, and this should work with a vanilla OpenSSH client. As for Win32 OpenSSH, we could patch it to check for the named pipe first, and skip the ssh-askpass program entirely. This should be sufficient to make the PowerShell SDK work without ssh-askpass at least on Windows, and it could work on non-Windows with a custom OpenSSH distribution.

Jaykul commented 2 years ago

That makes sense, I think.

OpenSSH already supports pipes as a way to get ssh keys from an agent... If you're going to patch OpenSSH to add support for getting passwords through pipes, that pipe should probably work (and be configured) the same way the existing pipe does.

Of course, if you're going to add pipe support to SecretManagement, you're wandering into scary territory. Apps that do this -- like Putty's Pageant and KeePass's KeeAgent -- usually (have an option to) prompt whenever something tries to retrieve a credential 😕

awakecoding commented 2 years ago

AFAIK, the SSH agent pipe protocol is only meant for keys, not for passwords. The original feature description from Steve mentions that the askpass executable could be shipped with SecretManagement, but it really doesn't need to be. Here is how I envision it from what is visible externally:

$HostName = "hostname"
$Username = "username"
$Credential = Get-Credential -UserName $UserName # or use SecretManagement module here
Enter-PSSession -UserName $UserName -HostName $HostName -SSHTransport -Credential $Credential

If you try this right now, it will fail because Enter-PSSession doesn't support the -Credential parameter. You can only let ssh.exe prompt interactively for the password, where you can optionally set the SSH_ASKPASS environment to have the OpenSSH client call it and expect the password printed to stdout, as a way to implement custom password prompts.

Now here's a description of what I envision would happen inside Enter-PSSession for the SSH transport when the -Credential parameter is used:

$PipeName = "pwsh-askpass-$(New-Guid)"
$Env:SSH_ASKPASS="pwsh-askpass.exe"
$Env:SSH_ASKPASS_PIPE_NAME="$PipeName"
# launch temporary named pipe server that accepts *one* client connection and sends the credentials over it
# launch OpenSSH client (ssh.exe), let it launch pwsh-askpass.exe, which will read password from name pipe
# close temporary named pipe server if it wasn't closed already after serving the password once

The same sequence would work with a patched OpenSSH client that checks for SSH_ASKPASS_PIPE_NAME to read directly from the named pipe without using pwsh-askpass.exe. PowerShell would still set both environment variables, such that if the OpenSSH client doesn't recognize SSH_ASKPASS_PIPE_NAME, it will still accept SSH_ASKPASS, and launch pwsh-askpass.exe that would read the password from the named pipe. It's the best of both worlds, and we could have OpenSSH for Windows support this natively. OpenSSH on other platforms would likely always fallback to using SSH_ASKPASS unless we can use a patched OpenSSH client.

I already confirmed the named pipe approach works with quick and dirty patching and a simple named pipe server in PowerShell, the next step would be to develop a pwsh-askpass.exe helper executable:

function Invoke-PSAskPassServer
{
    param(
        [Parameter(Position=0)]
        [string] $PipeName,
        [Parameter(Mandatory=$true)]
        [string] $Password
    )

    $Pipe = $Writer = $null

    try {
        $Pipe = [System.IO.Pipes.NamedPipeServerStream]::new($PipeName,
        [System.IO.Pipes.PipeDirection]::Out, 1,
        [System.IO.Pipes.PipeTransmissionMode]::Byte)
        $Pipe.WaitForConnection()
        $Writer = [System.IO.StreamWriter]::new($Pipe)
        $Writer.AutoFlush = $true
        $Writer.NewLine = "`n"
        $Writer.WriteLine($Password)
        $Writer.Close()
    } finally {
        if ($null -ne $Writer) {
            $Writer.Dispose()
        }
        if ($null -ne $Pipe) {
            $Pipe.Dispose()
        }
    }
}

$PipeName = "ssh-creds"
Invoke-PSAskPassServer $PipeName -Password "Password123!"
awakecoding commented 2 years ago

Just a quick update from my side:

I have a working Devolutions OpenSSH distribution that includes the Win32-OpenSSH patches. I've got it for Windows at this point but it already builds for Linux, I intend to make a cross-platform distribution for Remote Desktop Manager.

I implemented my suggestion and added support for an askpass named pipe server, where the named pipe name is passed by an environment variable. It works, and this is how I've got initial password injection for PowerShell Remoting over SSH into RDM Windows, where RDM spawns a short-lived name pipe server when launching PowerShell.

I am satisfied with the solution, and could submit the patches for Win32-OpenSSH for official inclusion upstream. The only remaining issue would be to make it work with a regular OpenSSH distribution on non-Windows.

For the second case (vanilla OpenSSH) then we could develop a "pwsh-askpass" executable inside the Win32-OpenSSH distribution, and get it built for all platforms. As I was getting familiar with OpenSSH I realized this was already a common thing (third-party askpass executables in downstream OpenSSH forks).

One of the issues with making a simple pwsh-askpass executable that could read the password from a named pipe is just that it would be better to make a tiny native executable in C, but PowerShell doesn't currently do that easily in its build process.

However, the Win32-OpenSSH build process is already geared only for native C tools, so adding a tiny pwsh-askpass executable would be a lot easier there. The main difficulty is that it is currently only built for Windows, but that could be fixed. I'm doing my own cross-platform distribution and got all the dependencies prebuilt already, so I can give a helping hand with that if necessary.

Let me know what you think, I already opened two PRs with Win32-OpenSSH for small issues I found while preparing my own distribution, and it got merged pretty quickly. I can open more PRs if we decide to go ahead with this.