Skip to content

Commit 3793a2f

Browse files
authored
cloud: use unique SSH keys per VM (#340)
When the gateway creates a new VM for a user, it also generates an ed25519 key pair that the client can use to login. This change updates the client to save the key pair in `~/.config/devbox/ssh/keys` and configures SSH to use it when connecting to the VM. - Consolidate `cloud/sshclient` and `cloud/openssh` into `cloud/openssh` to make it clearer that they manipulate the OpenSSH command. - Remove `openssh.Client.Port` and instead parse the port from the hostname. This allows the port to be overridden via `DEVBOX_GATEWAY`. - Add `openssh.AddVMKey(host, key)` that adds a new key to `~/.config/devbox/ssh/keys`. - Atomically edit `~/.ssh/config` and `~/.config/devbox/ssh/*` files so that partial edits don't break the user's SSH. - Add a bunch of tests that check that the client correctly modifies the user's `~/.ssh/config` and `~/.config/devbox` files. - Misc. bug fixes related to SSH configs: - Quote paths in ~/.ssh/config to handle paths with spaces. - Make the devbox include regex case-insensitive and detect when the include is commented out. - Set recommended permissions on the various SSH directories and files.
1 parent 2b0904a commit 3793a2f

File tree

21 files changed

+702
-188
lines changed

21 files changed

+702
-188
lines changed

cloud/cloud.go

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,27 @@ package cloud
55

66
import (
77
"encoding/json"
8+
"errors"
89
"fmt"
910
"log"
1011
"os"
12+
"os/exec"
1113
"path/filepath"
1214
"strings"
1315
"time"
1416

1517
"github.com/AlecAivazis/survey/v2"
1618
"github.com/fatih/color"
1719
"go.jetpack.io/devbox/cloud/mutagen"
18-
"go.jetpack.io/devbox/cloud/sshclient"
19-
"go.jetpack.io/devbox/cloud/sshconfig"
20+
"go.jetpack.io/devbox/cloud/openssh"
2021
"go.jetpack.io/devbox/cloud/stepper"
2122
"go.jetpack.io/devbox/debug"
2223
)
2324

2425
func Shell(configDir string) error {
25-
setupSSHConfig()
26+
if err := openssh.SetupDevbox(); err != nil {
27+
return err
28+
}
2629

2730
c := color.New(color.FgMagenta).Add(color.Bold)
2831
c.Println("Devbox Cloud")
@@ -58,12 +61,6 @@ func Shell(configDir string) error {
5861
return shell(username, vmHostname, configDir)
5962
}
6063

61-
func setupSSHConfig() {
62-
if err := sshconfig.Setup(); err != nil {
63-
log.Fatal(err)
64-
}
65-
}
66-
6764
func promptUsername() string {
6865
username := ""
6966
prompt := &survey.Input{
@@ -77,33 +74,49 @@ func promptUsername() string {
7774
return username
7875
}
7976

80-
type authResponse struct {
81-
VMHostname string `json:"vm_host"`
77+
type vm struct {
78+
JumpHost string `json:"jump_host"`
79+
JumpHostPort int `json:"jump_host_port"`
80+
VMHost string `json:"vm_host"`
81+
VMHostPort int `json:"vm_host_port"`
82+
VMRegion string `json:"vm_region"`
83+
VMPublicKey string `json:"vm_public_key"`
84+
VMPrivateKey string `json:"vm_private_key"`
8285
}
8386

8487
func getVirtualMachine(username string) string {
88+
client := openssh.Client{
89+
Username: username,
90+
Hostname: "gateway.devbox.sh",
91+
}
92+
8593
// When developing we can use this env variable to point
8694
// to a different gateway
87-
hostname := os.Getenv("DEVBOX_GATEWAY")
88-
if hostname == "" {
89-
hostname = "gateway.devbox.sh"
90-
}
91-
client := sshclient.Client{
92-
Username: username,
93-
Hostname: hostname,
95+
if envGateway := os.Getenv("DEVBOX_GATEWAY"); envGateway != "" {
96+
client.Hostname = envGateway
9497
}
9598
bytes, err := client.Exec("auth")
9699
if err != nil {
97-
log.Fatal(err)
100+
log.Println(err)
101+
var exitErr *exec.ExitError
102+
if errors.As(err, &exitErr) {
103+
log.Printf("ssh %s stderr:\n%s", client.Hostname, string(exitErr.Stderr))
104+
}
105+
os.Exit(1)
98106
}
99-
debug.Log("gateway.devbox.sh auth response: %s", string(bytes))
100-
resp := &authResponse{}
107+
debug.Log("ssh %s stdout:\n%s", client.Hostname, string(bytes))
108+
resp := &vm{}
101109
err = json.Unmarshal(bytes, resp)
102110
if err != nil {
103111
log.Fatal(err)
104112
}
105-
106-
return resp.VMHostname
113+
if resp.VMPrivateKey != "" {
114+
err := openssh.AddVMKey(resp.VMHost, resp.VMPrivateKey)
115+
if err != nil {
116+
log.Fatal(err)
117+
}
118+
}
119+
return resp.VMHost
107120
}
108121

109122
func syncFiles(username, hostname, configDir string) error {
@@ -135,7 +148,7 @@ func syncFiles(username, hostname, configDir string) error {
135148
}
136149

137150
func shell(username, hostname, configDir string) error {
138-
client := &sshclient.Client{
151+
client := &openssh.Client{
139152
Username: username,
140153
Hostname: hostname,
141154
ProjectDirName: projectDirName(configDir),

cloud/sshclient/sshclient.go renamed to cloud/openssh/client.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
package sshclient
1+
// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package openssh
25

36
import (
4-
"errors"
57
"fmt"
8+
"net"
69
"os"
710
"os/exec"
811
"strconv"
@@ -13,7 +16,6 @@ import (
1316
type Client struct {
1417
Username string
1518
Hostname string
16-
Port int
1719
ProjectDirName string
1820
}
1921

@@ -33,30 +35,36 @@ func (c *Client) Shell() error {
3335

3436
func (c *Client) Exec(remoteCmd string) ([]byte, error) {
3537
sshCmd := c.cmd()
36-
3738
sshCmd.Args = append(sshCmd.Args, remoteCmd)
38-
bytes, err := sshCmd.Output()
39-
var exerr *exec.ExitError
40-
if errors.As(err, &exerr) {
41-
// Ignore exit code errors and just return the output
42-
return bytes, nil
43-
}
44-
return bytes, err
39+
debug.Log("cmd/exec: %s", sshCmd)
40+
return sshCmd.Output()
4541
}
4642

4743
func (c *Client) cmd(sshArgs ...string) *exec.Cmd {
48-
44+
host, port := c.hostPort()
4945
cmd := exec.Command("ssh", sshArgs...)
50-
cmd.Args = append(cmd.Args, destination(c.Username, c.Hostname))
46+
cmd.Args = append(cmd.Args, destination(c.Username, host))
5147

5248
// Add any necessary flags:
53-
if c.Port != 0 {
54-
cmd.Args = append(cmd.Args, "-p", strconv.Itoa(c.Port))
49+
if port != 0 && port != 22 {
50+
cmd.Args = append(cmd.Args, "-p", strconv.Itoa(port))
5551
}
5652

5753
return cmd
5854
}
5955

56+
func (c *Client) hostPort() (host string, port int) {
57+
host, portStr, err := net.SplitHostPort(c.Hostname)
58+
if err != nil {
59+
return c.Hostname, 22
60+
}
61+
port, err = net.LookupPort("tcp", portStr)
62+
if err != nil {
63+
return host, 22
64+
}
65+
return host, port
66+
}
67+
6068
func destination(username, hostname string) string {
6169
result := hostname
6270
if username != "" {

0 commit comments

Comments
 (0)