@@ -6,6 +6,10 @@ package bastion
66
77import (
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+
315389func (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
504578func (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-
531591func 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