Skip to content

Commit dd3d9de

Browse files
committed
[local-app] Produce per-workspace SSH keypair
1 parent 3853d34 commit dd3d9de

File tree

1 file changed

+125
-32
lines changed

1 file changed

+125
-32
lines changed

components/local-app/pkg/bastion/bastion.go

Lines changed: 125 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ package bastion
66

77
import (
88
"context"
9+
"crypto/rand"
10+
"crypto/rsa"
11+
"crypto/x509"
12+
"encoding/pem"
913
"errors"
1014
"fmt"
1115
"io"
@@ -24,7 +28,6 @@ import (
2428
supervisor "github.com/gitpod-io/gitpod/supervisor/api"
2529
"github.com/google/uuid"
2630
"github.com/kevinburke/ssh_config"
27-
"github.com/prometheus/common/log"
2831
"golang.org/x/crypto/ssh"
2932
"google.golang.org/grpc"
3033
"google.golang.org/protobuf/proto"
@@ -78,6 +81,8 @@ type Workspace struct {
7881
tunnelListeners map[uint32]*TunnelListener
7982

8083
localSSHListener *TunnelListener
84+
SSHPrivateFN string
85+
SSHPublicKey string
8186

8287
ctx context.Context
8388
cancel context.CancelFunc
@@ -125,14 +130,17 @@ func (s *SSHConfigWritingCallback) InstanceUpdate(w *Workspace) {
125130
}
126131
if w.localSSHListener == nil || w.Phase == "stopping" {
127132
delete(s.workspaces, w.WorkspaceID)
128-
} else {
133+
} else if _, exists := s.workspaces[w.WorkspaceID]; !exists {
129134
s.workspaces[w.WorkspaceID] = w
130135
}
131136

132137
var cfg ssh_config.Config
133138
for _, ws := range s.workspaces {
134-
// TODO(cw): don't ignore error
135-
p, _ := ssh_config.NewPattern(ws.WorkspaceID)
139+
p, err := ssh_config.NewPattern(ws.WorkspaceID)
140+
if err != nil {
141+
logrus.WithError(err).Warn("cannot produce ssh_config entry")
142+
continue
143+
}
136144

137145
host, port, _ := net.SplitHostPort(ws.localSSHListener.LocalAddr)
138146
cfg.Hosts = append(cfg.Hosts, &ssh_config.Host{
@@ -141,6 +149,7 @@ func (s *SSHConfigWritingCallback) InstanceUpdate(w *Workspace) {
141149
&ssh_config.KV{Key: "HostName", Value: host},
142150
&ssh_config.KV{Key: "User", Value: "gitpod"},
143151
&ssh_config.KV{Key: "Port", Value: port},
152+
&ssh_config.KV{Key: "IdentityFile", Value: ws.SSHPrivateFN},
144153
},
145154
})
146155
}
@@ -294,11 +303,19 @@ func (b *Bastion) handleUpdate(u *gitpod.WorkspaceInstance) {
294303
}
295304

296305
if ws.localSSHListener == nil && ws.supervisorClient != nil {
297-
var err error
298-
ws.localSSHListener, err = b.establishSSHTunnel(ws)
299-
if err != nil {
300-
logrus.WithError(err).Error("cannot establish SSH tunnel")
301-
}
306+
func() {
307+
var err error
308+
ws.SSHPrivateFN, ws.SSHPublicKey, err = generateSSHKeys(ws.InstanceID)
309+
if err != nil {
310+
logrus.WithError(err).WithField("workspaceInstanceID", ws.InstanceID).Error("cannot produce SSH keypair")
311+
return
312+
}
313+
314+
ws.localSSHListener, err = b.establishSSHTunnel(ws)
315+
if err != nil {
316+
logrus.WithError(err).Error("cannot establish SSH tunnel")
317+
}
318+
}()
302319
}
303320

304321
case "stopping", "stopped":
@@ -312,6 +329,63 @@ func (b *Bastion) handleUpdate(u *gitpod.WorkspaceInstance) {
312329
b.Callbacks.InstanceUpdate(ws)
313330
}
314331

332+
func generateSSHKeys(instanceID string) (privateKeyFN string, publicKey string, err error) {
333+
privateKeyFN = filepath.Join(os.TempDir(), fmt.Sprintf("gitpod_%s_id_rsa", instanceID))
334+
useRrandomFile := func() {
335+
var tmpf *os.File
336+
tmpf, err = ioutil.TempFile("", "gitpod_*_id_rsa")
337+
if err != nil {
338+
return
339+
}
340+
tmpf.Close()
341+
privateKeyFN = tmpf.Name()
342+
}
343+
if stat, serr := os.Stat(privateKeyFN); serr == nil && stat.IsDir() {
344+
useRrandomFile()
345+
} else if serr == nil {
346+
var publicKeyRaw []byte
347+
publicKeyRaw, err = ioutil.ReadFile(privateKeyFN + ".pub")
348+
publicKey = string(publicKeyRaw)
349+
if err == nil {
350+
// we've loaded a pre-existing key - all is well
351+
return
352+
}
353+
354+
logrus.WithError(err).WithField("instance", instanceID).WithField("privateKeyFN", privateKeyFN).Warn("cannot load public SSH key for this workspace")
355+
useRrandomFile()
356+
}
357+
358+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
359+
if err != nil {
360+
return
361+
}
362+
err = privateKey.Validate()
363+
if err != nil {
364+
return
365+
}
366+
367+
privDER := x509.MarshalPKCS1PrivateKey(privateKey)
368+
privBlock := pem.Block{
369+
Type: "RSA PRIVATE KEY",
370+
Headers: nil,
371+
Bytes: privDER,
372+
}
373+
privatePEM := pem.EncodeToMemory(&privBlock)
374+
err = ioutil.WriteFile(privateKeyFN, privatePEM, 0600)
375+
if err != nil {
376+
return
377+
}
378+
379+
publicRsaKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
380+
if err != nil {
381+
return
382+
}
383+
publicKey = string(ssh.MarshalAuthorizedKey(publicRsaKey))
384+
_ = ioutil.WriteFile(privateKeyFN+".pub", []byte(publicKey), 0644)
385+
386+
return
387+
}
388+
315389
func (b *Bastion) connectTunnelClient(ctx context.Context, ws *Workspace) error {
316390
if ws.URL == "" {
317391
return fmt.Errorf("IDE URL is empty")
@@ -502,32 +576,18 @@ func (b *Bastion) establishTunnel(ctx context.Context, ws *Workspace, logprefix
502576
}
503577

504578
func (b *Bastion) establishSSHTunnel(ws *Workspace) (listener *TunnelListener, err error) {
505-
key, err := readPublicSSHKey()
506-
if err != nil {
507-
// TODO(cw): surface to the user and ask them to run ssh-keygen
508-
logrus.WithError(err).Warn("no id_rsa.pub file found - will not be able to login via SSH")
579+
if ws.SSHPublicKey == "" {
580+
return nil, fmt.Errorf("no public key generated")
509581
}
510-
err = installSSHAuthorizedKey(ws, key)
582+
583+
err = installSSHAuthorizedKey(ws, ws.SSHPublicKey)
511584
if err != nil {
512-
// TODO(cw): surface to the user and ask them install the key manually
513-
logrus.WithError(err).Warn("cannot install authorized key")
585+
return nil, fmt.Errorf("cannot install authorized key: %w", err)
514586
}
515587
listener, err = b.establishTunnel(ws.ctx, ws, "ssh", 23001, 0, supervisor.TunnelVisiblity_host)
516588
return listener, err
517589
}
518590

519-
func readPublicSSHKey() (key string, err error) {
520-
home, err := os.UserHomeDir()
521-
if err != nil {
522-
return "", err
523-
}
524-
res, err := ioutil.ReadFile(filepath.Join(home, ".ssh", "id_rsa.pub"))
525-
if err != nil {
526-
return "", err
527-
}
528-
return strings.TrimSpace(string(res)), nil
529-
}
530-
531591
func installSSHAuthorizedKey(ws *Workspace, key string) error {
532592
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
533593
defer cancel()
@@ -539,17 +599,50 @@ func installSSHAuthorizedKey(ws *Workspace, key string) error {
539599
//nolint:errcheck
540600
defer term.Shutdown(ctx, &supervisor.ShutdownTerminalRequest{Alias: tres.Terminal.Alias})
541601

602+
done := make(chan bool, 1)
603+
recv, err := term.Listen(ctx, &supervisor.ListenTerminalRequest{Alias: tres.Terminal.Alias})
604+
if err != nil {
605+
return err
606+
}
607+
608+
go func() {
609+
defer close(done)
610+
for {
611+
resp, err := recv.Recv()
612+
if err != nil {
613+
return
614+
}
615+
if resp.Output == nil {
616+
continue
617+
}
618+
out, ok := resp.Output.(*supervisor.ListenTerminalResponse_Data)
619+
if !ok {
620+
continue
621+
}
622+
c := strings.TrimSpace(string(out.Data))
623+
if strings.HasPrefix(c, "write done") {
624+
done <- true
625+
return
626+
}
627+
}
628+
}()
542629
_, err = term.Write(ctx, &supervisor.WriteTerminalRequest{
543630
Alias: tres.Terminal.Alias,
544-
Stdin: []byte(fmt.Sprintf("mkdir -p ~/.ssh; echo %s >> ~/.ssh/authorized_keys\n", key)),
631+
Stdin: []byte(fmt.Sprintf("mkdir -p ~/.ssh; echo %s >> ~/.ssh/authorized_keys; echo write done\r\n", strings.TrimSpace(key))),
545632
})
546633
if err != nil {
547634
return err
548635
}
549636

550637
// give the command some time to execute
551-
// TODO(cw): synchronize this properly
552-
time.Sleep(500 * time.Millisecond)
638+
select {
639+
case <-ctx.Done():
640+
return ctx.Err()
641+
case success := <-done:
642+
if !success {
643+
return fmt.Errorf("unable to upload SSH key")
644+
}
645+
}
553646

554647
return nil
555648
}
@@ -659,7 +752,7 @@ func (b *Bastion) notify(ws *Workspace) {
659752
select {
660753
case sub.updates <- status:
661754
case <-time.After(5 * time.Second):
662-
log.Error("ports subscription dropped out")
755+
logrus.Error("ports subscription dropped out")
663756
sub.Close()
664757
}
665758
}

0 commit comments

Comments
 (0)