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 git@github.com 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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy