Skip to content

Commit d0b02e5

Browse files
feat: Improve experience with local SSH keys (#3835)
* 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 * fix: Ensure gitssh cleans up temporary file on interrupt Co-authored-by: Dean Sheather <dean@deansheather.com>
1 parent 66ad86a commit d0b02e5

File tree

2 files changed

+352
-81
lines changed

2 files changed

+352
-81
lines changed

cli/gitssh.go

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

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

915
"github.com/spf13/cobra"
@@ -13,16 +19,30 @@ import (
1319
)
1420

1521
func gitssh() *cobra.Command {
16-
return &cobra.Command{
22+
cmd := &cobra.Command{
1723
Use: "gitssh",
1824
Hidden: true,
1925
Short: `Wraps the "ssh" command and uses the coder gitssh key for authentication`,
2026
RunE: func(cmd *cobra.Command, args []string) error {
27+
ctx := cmd.Context()
28+
env := os.Environ()
29+
30+
// Catch interrupt signals to ensure the temporary private
31+
// key file is cleaned up on most cases.
32+
ctx, stop := signal.NotifyContext(ctx, interruptSignals...)
33+
defer stop()
34+
35+
// Early check so errors are reported immediately.
36+
identityFiles, err := parseIdentityFilesForHost(ctx, args, env)
37+
if err != nil {
38+
return err
39+
}
40+
2141
client, err := createAgentClient(cmd)
2242
if err != nil {
2343
return xerrors.Errorf("create agent client: %w", err)
2444
}
25-
key, err := client.AgentGitSSHKey(cmd.Context())
45+
key, err := client.AgentGitSSHKey(ctx)
2646
if err != nil {
2747
return xerrors.Errorf("get agent git ssh token: %w", err)
2848
}
@@ -44,8 +64,23 @@ func gitssh() *cobra.Command {
4464
return xerrors.Errorf("close temp gitsshkey file: %w", err)
4565
}
4666

47-
args = append([]string{"-i", privateKeyFile.Name()}, args...)
48-
c := exec.CommandContext(cmd.Context(), "ssh", 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...)
82+
c := exec.CommandContext(ctx, "ssh", args...)
83+
c.Env = append(c.Env, env...)
4984
c.Stderr = cmd.ErrOrStderr()
5085
c.Stdout = cmd.OutOrStdout()
5186
c.Stdin = cmd.InOrStdin()
@@ -69,4 +104,86 @@ func gitssh() *cobra.Command {
69104
return nil
70105
},
71106
}
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+
// parseIdentityFilesForHost 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 invocation:
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 parseIdentityFilesForHost(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 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 = io.Discard
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
72189
}

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