Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b6efca5

Browse files
committed
feat: Improve experience with local SSH keys
This change means that users can place SSH keys in the default locations for OpenSSH, like `~/.ssh/id_rsa` and it will be automatically picked up (as per a default OpenSSH experience). Fixes #3126
1 parent edd595c commit b6efca5

File tree

2 files changed

+343
-81
lines changed

2 files changed

+343
-81
lines changed

cli/gitssh.go

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package cli
22

33
import (
4+
"bufio"
5+
"bytes"
6+
"context"
47
"fmt"
8+
"io"
59
"os"
610
"os/exec"
711
"os/signal"
12+
"path/filepath"
813
"strings"
914

1015
"github.com/spf13/cobra"
@@ -14,18 +19,25 @@ import (
1419
)
1520

1621
func gitssh() *cobra.Command {
17-
return &cobra.Command{
22+
cmd := &cobra.Command{
1823
Use: "gitssh",
1924
Hidden: true,
2025
Short: `Wraps the "ssh" command and uses the coder gitssh key for authentication`,
2126
RunE: func(cmd *cobra.Command, args []string) error {
2227
ctx := cmd.Context()
28+
env := os.Environ()
2329

24-
// Catch interrupt signals as a best-effort attempt to clean
25-
// up the temporary key file.
30+
// Catch interrupt signals to ensure the temporary private
31+
// key file is cleaned up on most cases.
2632
ctx, stop := signal.NotifyContext(ctx, interruptSignals...)
2733
defer stop()
2834

35+
// Early check so errors are reported immediately.
36+
identityFiles, err := praseIdentityFilesForHost(ctx, args, env)
37+
if err != nil {
38+
return err
39+
}
40+
2941
client, err := createAgentClient(cmd)
3042
if err != nil {
3143
return xerrors.Errorf("create agent client: %w", err)
@@ -52,8 +64,23 @@ func gitssh() *cobra.Command {
5264
return xerrors.Errorf("close temp gitsshkey file: %w", err)
5365
}
5466

55-
args = append([]string{"-i", privateKeyFile.Name()}, args...)
67+
// Append our key, giving precedence to user keys. Note that
68+
// OpenSSH server are typically configured with MaxAuthTries
69+
// set to the default value of 6. This means that only the 6
70+
// first keys can be tried. However, we will assume that if
71+
// a user has configured 6+ keys for a host, they know what
72+
// they're doing. This behavior is critical if a server has
73+
// been configured with MaxAuthTries set to 1.
74+
identityFiles = append(identityFiles, privateKeyFile.Name())
75+
76+
var identityArgs []string
77+
for _, id := range identityFiles {
78+
identityArgs = append(identityArgs, "-i", id)
79+
}
80+
81+
args = append(identityArgs, args...)
5682
c := exec.CommandContext(ctx, "ssh", args...)
83+
c.Env = append(c.Env, env...)
5784
c.Stderr = cmd.ErrOrStderr()
5885
c.Stdout = cmd.OutOrStdout()
5986
c.Stdin = cmd.InOrStdin()
@@ -77,4 +104,86 @@ func gitssh() *cobra.Command {
77104
return nil
78105
},
79106
}
107+
108+
return cmd
109+
}
110+
111+
// fallbackIdentityFiles is the list of identity files SSH tries when
112+
// none have been defined for a host.
113+
var fallbackIdentityFiles = strings.Join([]string{
114+
"identityfile ~/.ssh/id_rsa",
115+
"identityfile ~/.ssh/id_dsa",
116+
"identityfile ~/.ssh/id_ecdsa",
117+
"identityfile ~/.ssh/id_ecdsa_sk",
118+
"identityfile ~/.ssh/id_ed25519",
119+
"identityfile ~/.ssh/id_ed25519_sk",
120+
"identityfile ~/.ssh/id_xmss",
121+
}, "\n")
122+
123+
// praseIdentityFilesForHost uses ssh -G to discern what SSH keys have
124+
// been enabled for the host (via the users SSH config) and returns a
125+
// list of existing identity files.
126+
//
127+
// We do this because when no keys are defined for a host, SSH uses
128+
// fallback keys (see above). However, by passing `-i` to attach our
129+
// private key, we're effectively disabling the fallback keys.
130+
//
131+
// Example invokation:
132+
//
133+
// ssh -G -o SendEnv=GIT_PROTOCOL [email protected] git-upload-pack 'coder/coder'
134+
//
135+
// The extra arguments work without issue and lets us run the command
136+
// as-is without stripping out the excess (git-upload-pack 'coder/coder').
137+
func praseIdentityFilesForHost(ctx context.Context, args, env []string) (identityFiles []string, error error) {
138+
home, err := os.UserHomeDir()
139+
if err != nil {
140+
return nil, xerrors.Errorf("get user home dir failed: %w", err)
141+
}
142+
143+
var outBuf, errBuf bytes.Buffer
144+
var r io.Reader = &outBuf
145+
146+
args = append([]string{"-G"}, args...)
147+
cmd := exec.CommandContext(ctx, "ssh", args...)
148+
cmd.Env = append(cmd.Env, env...)
149+
cmd.Stdout = &outBuf
150+
cmd.Stderr = &errBuf
151+
err = cmd.Run()
152+
if err != nil {
153+
// If ssh -G failed, the SSH version is likely too old, fallback
154+
// to using the default identity files.
155+
r = strings.NewReader(fallbackIdentityFiles)
156+
}
157+
158+
s := bufio.NewScanner(r)
159+
for s.Scan() {
160+
line := s.Text()
161+
if strings.HasPrefix(line, "identityfile ") {
162+
id := strings.TrimPrefix(line, "identityfile ")
163+
if strings.HasPrefix(id, "~/") {
164+
id = home + id[1:]
165+
}
166+
// OpenSSH on Windows is weird, it supports using (and does
167+
// use) mixed \ and / in paths.
168+
//
169+
// Example: C:\Users\ZeroCool/.ssh/known_hosts
170+
//
171+
// To check the file existence in Go, though, we want to use
172+
// proper Windows paths.
173+
// OpenSSH is amazing, this will work on Windows too:
174+
// C:\Users\ZeroCool/.ssh/id_rsa
175+
id = filepath.FromSlash(id)
176+
177+
// Only include the identity file if it exists.
178+
if _, err := os.Stat(id); err == nil {
179+
identityFiles = append(identityFiles, id)
180+
}
181+
}
182+
}
183+
if err := s.Err(); err != nil {
184+
// This should never happen, the check is for completeness.
185+
return nil, xerrors.Errorf("scan ssh output: %w", err)
186+
}
187+
188+
return identityFiles, nil
80189
}

0 commit comments

Comments
 (0)