Add support for SSH private key passphrases (#2331)

This adds support for encrypted SSH keys via passphrase.

Co-authored-by: Louis Ruch <louisruch@gmail.com>
Co-authored-by: Michael Gaffney <mgaffney@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2022-08-08 15:15:03 -04:00
1 parent 07bca9dd66
commit 02dd28f587
45 files changed
+1921 -382

No files matched your search

@@ -19,6 +19,7 @@ type usernamePassword struct {
type sshPrivateKey struct {
Username string `mapstructure:"username"`
PrivateKey string `mapstructure:"private_key"`
Passphrase string `mapstructure:"private_key_passphrase"`
raw *targets.SessionCredential
consumed bool
+13 -7
View File
@@ -71,7 +71,7 @@ func (s *sshFlags) buildArgs(c *Command, port, ip, addr string, creds credential
var password string
if len(retCreds.usernamePassword) > 0 {
// For now just grab the first username password credential brokered
// Mark credential as consumed so it is not printed to user
// Mark credential as consumed so that it is not printed to user
retCreds.usernamePassword[0].consumed = true
username = retCreds.usernamePassword[0].Username
@@ -109,11 +109,17 @@ func (s *sshFlags) buildArgs(c *Command, port, ip, addr string, creds credential
// If we want to consume check if we have a private key available first
case len(creds.sshPrivateKey) > 0:
// For now just grab the first ssh private key credential brokered
// Mark credential as consumed so it is not printed to user
retCreds.sshPrivateKey[0].consumed = true
cred := retCreds.sshPrivateKey[0]
username = retCreds.sshPrivateKey[0].Username
privateKey := retCreds.sshPrivateKey[0].PrivateKey
username = cred.Username
privateKey := cred.PrivateKey
cred.consumed = true
if cred.Passphrase != "" {
// Don't re-print everything, but print the passphrase they'll need
cred.consumed = false
delete(cred.raw.Credential, "username")
delete(cred.raw.Credential, "private_key")
}
pkFile, err := ioutil.TempFile("", "*")
if err != nil {
@@ -126,7 +132,7 @@ func (s *sshFlags) buildArgs(c *Command, port, ip, addr string, creds credential
return nil
})
// SSH requires the private key file to end with a newline.
// When injesting a ssh_private_key from a file:// Boundary calls strings.TrimSpace
// When ingesting an ssh_private_key from a file:// Boundary calls strings.TrimSpace
// which will also trim newlines.
if !strings.HasSuffix(privateKey, "\n") {
privateKey = fmt.Sprintln(privateKey)
@@ -142,7 +148,7 @@ func (s *sshFlags) buildArgs(c *Command, port, ip, addr string, creds credential
// Next check if we have a username password credential
case len(creds.usernamePassword) > 0:
// We cannot use the password of the credential outside of sshpass but we
// We cannot use the password of the credential outside of sshpass, but we
// can use the username.
// N.B. Do not mark credential as consumed, as user will still need enter
// the password when prompted.
@@ -10,9 +10,10 @@ import (
)
const (
usernameFlagName = "username"
passwordFlagName = "password"
privateKeyFlagName = "private-key"
usernameFlagName = "username"
passwordFlagName = "password"
privateKeyFlagName = "private-key"
privateKeyPassphraseFlagName = "private-key-passphrase"
)
func (c *Command) extraHelpFunc(helpMap map[string]func() string) string {
@@ -177,7 +178,8 @@ func printItemTable(item *credentials.Credential, resp *api.Response) string {
}
var keySubstMap = map[string]string{
"username": "Username",
"password_hmac": "Password HMAC",
"private_key_hmac": "Private Key HMAC",
"username": "Username",
"password_hmac": "Password HMAC",
"private_key_hmac": "Private Key HMAC",
"private_key_passphrase_hmac": "Private Key Passphrase HMAC",
}
@@ -3,10 +3,14 @@ package credentialscmd
import (
"errors"
"fmt"
"os"
"strings"
"github.com/hashicorp/boundary/api/credentials"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/go-secure-stdlib/password"
"golang.org/x/crypto/ssh"
)
func init() {
@@ -16,8 +20,9 @@ func init() {
}
type extraSshPrivateKeyCmdVars struct {
flagUsername string
flagPrivateKey string
flagUsername string
flagPrivateKey string
flagPrivateKeyPassphrase string
}
func extraSshPrivateKeyActionsFlagsMapFuncImpl() map[string][]string {
@@ -25,6 +30,7 @@ func extraSshPrivateKeyActionsFlagsMapFuncImpl() map[string][]string {
"create": {
usernameFlagName,
privateKeyFlagName,
privateKeyPassphraseFlagName,
},
}
flags["update"] = flags["create"]
@@ -46,7 +52,13 @@ func extraSshPrivateKeyFlagsFuncImpl(c *SshPrivateKeyCommand, set *base.FlagSets
f.StringVar(&base.StringVar{
Name: privateKeyFlagName,
Target: &c.flagPrivateKey,
Usage: "The SSH private key associated with the credential. This can be the value itself, refer to a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read.",
Usage: "The SSH private key associated with the credential. This can refer to a file on disk (file://) from which the value will be read or an env var (env://) from which the value will be read.",
})
case privateKeyPassphraseFlagName:
f.StringVar(&base.StringVar{
Name: privateKeyPassphraseFlagName,
Target: &c.flagPrivateKeyPassphrase,
Usage: "The passphrase associated with the SSH private key. This value is ignored if the private key does not require a passphrase or if no private key is supplied. This can refer to a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read. Or, if left empty, if the key requires a passphrase it can be entered manually.",
})
}
}
@@ -58,22 +70,79 @@ func extraSshPrivateKeyFlagHandlingFuncImpl(c *SshPrivateKeyCommand, _ *base.Fla
default:
*opts = append(*opts, credentials.WithSshPrivateKeyCredentialUsername(c.flagUsername))
}
switch c.flagPrivateKey {
case "":
// If private key not set (e.g. just a username update) then don't check
// either private key or passphrase
if c.flagPrivateKey == "" {
return true
}
// First parse private key
privateKey, err := parseutil.MustParsePath(c.flagPrivateKey)
switch {
case err == nil:
case errors.Is(err, parseutil.ErrNotParsed):
c.UI.Error("Private key flag must be used with env:// or file:// syntax")
return false
default:
privateKey, err := parseutil.MustParsePath(c.flagPrivateKey)
c.UI.Error(fmt.Sprintf("Error parsing private key flag: %v", err))
return false
}
// Now validate it
_, err = ssh.ParsePrivateKey([]byte(privateKey))
// If nil, parsed successfully without passphrase, ignore passphrase flag
if err == nil {
if c.flagPrivateKeyPassphrase != "" {
c.UI.Warn("Ignoring private key passphrase as private key is not encrypted.")
}
*opts = append(*opts, credentials.WithSshPrivateKeyCredentialPrivateKey(privateKey))
return true
}
// If an error but it's not a passphrase missing error, send the error back
if err.Error() != (&ssh.PassphraseMissingError{}).Error() {
c.UI.Error(fmt.Sprintf("Error parsing private key: %v", err))
return false
}
// Passphrase required
var privateKeyPassphrase string
switch c.flagPrivateKeyPassphrase {
case "":
fmt.Print("Please enter the passphrase for the private key (it will be hidden): ")
privateKeyPassphrase, err = password.Read(os.Stdin)
fmt.Print("\n")
if err != nil {
c.UI.Error(fmt.Sprintf("An error occurred attempting to read the passphrase. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error()))
return false
}
privateKeyPassphrase = strings.TrimSpace(privateKeyPassphrase)
default:
privateKeyPassphrase, err = parseutil.MustParsePath(c.flagPrivateKeyPassphrase)
switch {
case err == nil:
case errors.Is(err, parseutil.ErrNotParsed):
c.UI.Error("Private key flag must be used with env:// or file:// syntax")
c.UI.Error("Private key passphrase flag must be used with env:// or file:// syntax")
return false
default:
c.UI.Error(fmt.Sprintf("Error parsing private key flag: %v", err))
c.UI.Error(fmt.Sprintf("Error parsing private key passphrase flag: %v", err))
return false
}
*opts = append(*opts, credentials.WithSshPrivateKeyCredentialPrivateKey(privateKey))
}
if _, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(privateKeyPassphrase)); err != nil {
c.UI.Error(fmt.Sprintf("Error parsing private key passphrase: %v", err))
return false
}
*opts = append(*opts,
credentials.WithSshPrivateKeyCredentialPrivateKey(privateKey),
credentials.WithSshPrivateKeyCredentialPrivateKeyPassphrase(privateKeyPassphrase),
)
return true
}