Skip to content

Commit

Permalink
fix(ssh): add support for interactive ssh keys when adding to SSH agent
Browse files Browse the repository at this point in the history
Separates between SSH keys that can be used as-in and the ones that
require additional input from the user like a passphrase.
In the latter case we now check if Stdin is interactive and prompt users
for input if it is.
  • Loading branch information
pascalbreuninger committed Jan 13, 2025
1 parent a2bf7f7 commit 5361694
Showing 1 changed file with 45 additions and 11 deletions.
56 changes: 45 additions & 11 deletions pkg/ssh/ssh_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/loft-sh/devpod/pkg/util"
"github.com/loft-sh/log"
"golang.org/x/crypto/ssh"
"golang.org/x/term"
)

func AddPrivateKeysToAgent(ctx context.Context, log log.Logger) error {
Expand All @@ -22,25 +23,28 @@ func AddPrivateKeysToAgent(ctx context.Context, log log.Logger) error {
return fmt.Errorf("ssh-add couldn't be found")
}

privateKeys, err := FindPrivateKeys()
privateKeys, err := findPrivateKeys()
if err != nil {
return err
}

for _, privateKey := range privateKeys {
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second*2)
log.Debugf("Run ssh-add %s", privateKey)
out, err := exec.CommandContext(timeoutCtx, "ssh-add", privateKey).CombinedOutput()
cancel()
log.Debugf("Adding key to SSH Agent: %s", privateKey.path)
err := addKeyToAgent(ctx, privateKey)
if err != nil {
log.Debugf("Error adding key %s to agent: %v", privateKey, command.WrapCommandError(out, err))
log.Debugf("%v", err)
}
}

return nil
}

func FindPrivateKeys() ([]string, error) {
type privateKey struct {
path string
requiresPassphrase bool
}

func findPrivateKeys() ([]privateKey, error) {
homeDir, err := util.UserHomeDir()
if err != nil {
return nil, err
Expand All @@ -52,21 +56,51 @@ func FindPrivateKeys() ([]string, error) {
return nil, err
}

privateKeys := []string{}
keys := []privateKey{}
passphraseMissingErr := &ssh.PassphraseMissingError{}
for _, entry := range entries {
if entry.IsDir() {
continue
}

keyPath := filepath.Join(sshDir, entry.Name())
out, err := os.ReadFile(keyPath)
if err == nil {
_, err = ssh.ParsePrivateKey(out)
if err == nil {
privateKeys = append(privateKeys, keyPath)
keys = append(keys, privateKey{path: keyPath})
} else if err.Error() == passphraseMissingErr.Error() {
// we can check for the passphrase later
keys = append(keys, privateKey{path: keyPath, requiresPassphrase: true})
}
}
}

return privateKeys, nil
return keys, nil
}

func addKeyToAgent(ctx context.Context, privateKey privateKey) error {
timeoutCtx, cancel := context.WithTimeout(ctx, time.Minute*5)
defer cancel()

// Let users enter the passphrase if the key requires it and we're in an interactive session
if privateKey.requiresPassphrase && term.IsTerminal(int(os.Stdin.Fd())) {
cmd := exec.CommandContext(timeoutCtx, "ssh-add", privateKey.path)
cmd.Stdin = os.Stdin
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("Add key %s to agent: %v", privateKey.path, command.WrapCommandError(out, err))

Check failure on line 91 in pkg/ssh/ssh_add.go

View workflow job for this annotation

GitHub Actions / lint

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}

return nil
}

// Normal non-interactive mode
timeoutCtx, cancel = context.WithTimeout(ctx, time.Second*1)
defer cancel()
out, err := exec.CommandContext(timeoutCtx, "ssh-add", privateKey.path).CombinedOutput()
if err != nil {
return fmt.Errorf("Add key %s to agent: %v", privateKey.path, command.WrapCommandError(out, err))

Check failure on line 102 in pkg/ssh/ssh_add.go

View workflow job for this annotation

GitHub Actions / lint

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}

return nil
}

0 comments on commit 5361694

Please sign in to comment.