This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / spindle / engines / microvm / debugssh.go
4.8 kB 171 lines
1package microvm 2 3import ( 4 "context" 5 "crypto/ed25519" 6 "crypto/rand" 7 "encoding/pem" 8 "fmt" 9 "io" 10 "net/http" 11 "os" 12 "path/filepath" 13 "time" 14 15 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 16 "github.com/gliderlabs/ssh" 17 gossh "golang.org/x/crypto/ssh" 18 "tangled.org/core/api/tangled" 19 "tangled.org/core/hostutil" 20) 21 22// debug ssh: terminates ssh here, bridges a pty into a live (failed) microVM 23// over the guest's agent conn. the guest stays keyless. access mirrors git 24// push: the offered key goes to the repo's knot 25// (sh.tangled.repo.checkPushAllowed) and is accepted only if it can push to the 26// job's repo. 27 28const debugAuthTimeout = 5 * time.Second 29 30func (e *Engine) serveDebugSSH(ctx context.Context) { 31 dbg := e.cfg.MicroVMPipelines.DebugSSH 32 if !dbg.Enabled || dbg.ListenAddr == "" { 33 return 34 } 35 addr := dbg.ListenAddr 36 httpc := &http.Client{Timeout: debugAuthTimeout} 37 38 srv := &ssh.Server{ 39 Addr: addr, 40 Handler: e.debugHandle, 41 PublicKeyHandler: func(c ssh.Context, key ssh.PublicKey) bool { 42 return e.checkDebugAuth(c, c.User(), key, httpc) 43 }, 44 } 45 keyPath := e.cfg.MicroVMPipelines.DebugSSH.HostKeyPath 46 if keyPath == "" { 47 keyPath = filepath.Join(filepath.Dir(e.cfg.Server.DBPath), "debug_ssh_host_key") 48 } 49 if err := ensureDebugHostKey(keyPath); err != nil { 50 e.l.Error("debug ssh: ensure host key", "path", keyPath, "err", err) 51 return 52 } 53 if err := srv.SetOption(ssh.HostKeyFile(keyPath)); err != nil { 54 e.l.Error("debug ssh: load host key", "path", keyPath, "err", err) 55 return 56 } 57 58 go func() { 59 <-ctx.Done() 60 _ = srv.Close() 61 }() 62 63 e.l.Info("starting debug ssh server", "address", addr) 64 if err := srv.ListenAndServe(); err != nil && err != ssh.ErrServerClosed { 65 e.l.Error("debug ssh server stopped", "err", err) 66 } 67} 68 69func ensureDebugHostKey(path string) error { 70 if _, err := os.Stat(path); err == nil { 71 return nil 72 } else if !os.IsNotExist(err) { 73 return fmt.Errorf("stat host key: %w", err) 74 } 75 76 _, priv, err := ed25519.GenerateKey(rand.Reader) 77 if err != nil { 78 return fmt.Errorf("generate host key: %w", err) 79 } 80 block, err := gossh.MarshalPrivateKey(priv, "") 81 if err != nil { 82 return fmt.Errorf("marshal host key: %w", err) 83 } 84 85 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { 86 return fmt.Errorf("create host key dir: %w", err) 87 } 88 if err := os.WriteFile(path, pem.EncodeToMemory(block), 0o600); err != nil { 89 return fmt.Errorf("write host key: %w", err) 90 } 91 return nil 92} 93 94func (e *Engine) debugHandle(sess ssh.Session) { 95 ptyReq, winCh, isPty := sess.Pty() 96 if !isPty { 97 io.WriteString(sess.Stderr(), "error: no terminal allocated; use `ssh -t`\n") 98 _ = sess.Exit(1) 99 return 100 } 101 102 jobID := sess.User() 103 l := e.l.With("component", "debugssh", "job", jobID) 104 105 debug, err := e.OpenDebugSession(sess.Context(), jobID, ptyReq.Term, ptyReq.Window.Height, ptyReq.Window.Width) 106 if err != nil { 107 fmt.Fprintf(sess.Stderr(), "error: %v\n", err) 108 _ = sess.Exit(1) 109 return 110 } 111 defer debug.Close() 112 l.Info("debug shell opened") 113 114 go func() { 115 for win := range winCh { 116 if err := debug.Resize(win.Height, win.Width); err != nil { 117 l.Debug("debug ssh resize failed", "error", err) 118 } 119 } 120 }() 121 122 // keyboard -> shell, runs until the client hangs up 123 go func() { _, _ = io.Copy(debug, sess) }() 124 // shell -> client, returns when the shell exits (Read hits EOF) 125 _, _ = io.Copy(sess, debug) 126 127 code := debug.ExitCode() 128 l.Info("debug shell closed", "exitCode", code) 129 // the user is done; let retention tear the VM down now instead of waiting 130 // out the rest of the grace period 131 e.releaseDebugTarget(jobID) 132 _ = sess.Exit(code) 133} 134 135func (e *Engine) checkDebugAuth(ctx context.Context, jobID string, key ssh.PublicKey, httpc *http.Client) bool { 136 l := e.l.With("component", "debugssh", "job", jobID, "keyType", key.Type()) 137 138 knot, repoDid, ok := e.RepoForJob(jobID) 139 if !ok { 140 l.Warn("debug ssh: no live job / unknown repo") 141 return false 142 } 143 144 host, noSSL, err := hostutil.ParseHostname(knot) 145 if err != nil { 146 l.Error("debug ssh: bad knot host", "knot", knot, "error", err) 147 return false 148 } 149 scheme := "https" 150 if noSSL { 151 scheme = "http" 152 } 153 xc := &indigoxrpc.Client{Host: fmt.Sprintf("%s://%s", scheme, host), Client: httpc} 154 155 reqCtx, cancel := context.WithTimeout(ctx, debugAuthTimeout) 156 defer cancel() 157 158 out, err := tangled.RepoCheckPushAllowed(reqCtx, xc, string(gossh.MarshalAuthorizedKey(key)), repoDid) 159 if err != nil { 160 l.Error("debug ssh: push-allowed check failed", "knot", knot, "repo", repoDid, "error", err) 161 return false 162 } 163 if !out.Allowed { 164 l.Warn("debug ssh: key not allowed to push", "knot", knot, "repo", repoDid) 165 return false 166 } 167 if out.Did != nil { 168 l.Info("debug ssh: authorized", "did", *out.Did, "knot", knot, "repo", repoDid) 169 } 170 return true 171}