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:
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user