1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
20 "git.arvados.org/arvados.git/sdk/go/arvados"
21 "git.arvados.org/arvados.git/sdk/go/auth"
22 "git.arvados.org/arvados.git/sdk/go/ctxlog"
23 "git.arvados.org/arvados.git/sdk/go/httpserver"
26 // ContainerSSH returns a connection to the SSH server in the
27 // appropriate crunch-run process on the worker node where the
28 // specified container is running.
30 // If the returned error is nil, the caller is responsible for closing
32 func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOptions) (sshconn arvados.ContainerSSHConnection, err error) {
33 user, err := conn.railsProxy.UserGetCurrent(ctx, arvados.GetOptions{})
37 ctr, err := conn.railsProxy.ContainerGet(ctx, arvados.GetOptions{UUID: opts.UUID})
41 ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{conn.cluster.SystemRootToken}})
42 if !user.IsAdmin || !conn.cluster.Containers.ShellAccess.Admin {
43 if !conn.cluster.Containers.ShellAccess.User {
44 err = httpserver.ErrorWithStatus(errors.New("shell access is disabled in config"), http.StatusServiceUnavailable)
47 var crs arvados.ContainerRequestList
48 crs, err = conn.railsProxy.ContainerRequestList(ctxRoot, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"container_uuid", "=", opts.UUID}}})
52 for _, cr := range crs.Items {
53 if cr.ModifiedByUserUUID != user.UUID {
54 err = httpserver.ErrorWithStatus(errors.New("permission denied: container is associated with requests submitted by other users"), http.StatusForbidden)
58 if crs.ItemsAvailable != len(crs.Items) {
59 err = httpserver.ErrorWithStatus(errors.New("incomplete response while checking permission"), http.StatusInternalServerError)
65 case arvados.ContainerStateQueued, arvados.ContainerStateLocked:
66 err = httpserver.ErrorWithStatus(fmt.Errorf("container is not running yet (state is %q)", ctr.State), http.StatusServiceUnavailable)
68 case arvados.ContainerStateRunning:
69 if ctr.GatewayAddress == "" {
70 err = httpserver.ErrorWithStatus(errors.New("container is running but gateway is not available -- installation problem or feature not supported"), http.StatusServiceUnavailable)
74 err = httpserver.ErrorWithStatus(fmt.Errorf("container has ended (state is %q)", ctr.State), http.StatusGone)
77 // crunch-run uses a self-signed / unverifiable TLS
78 // certificate, so we use the following scheme to ensure we're
79 // not talking to a MITM.
81 // 1. Compute ctrKey = HMAC-SHA256(sysRootToken,ctrUUID) --
82 // this will be the same ctrKey that a-d-c supplied to
83 // crunch-run in the GatewayAuthSecret env var.
85 // 2. Compute requestAuth = HMAC-SHA256(ctrKey,serverCert) and
86 // send it to crunch-run as the X-Arvados-Authorization
87 // header, proving that we know ctrKey. (Note a MITM cannot
88 // replay the proof to a real crunch-run server, because the
89 // real crunch-run server would have a different cert.)
91 // 3. Compute respondAuth = HMAC-SHA256(ctrKey,requestAuth)
92 // and ensure the server returns it in the
93 // X-Arvados-Authorization-Response header, proving that the
94 // server knows ctrKey.
95 var requestAuth, respondAuth string
96 netconn, err := tls.Dial("tcp", ctr.GatewayAddress, &tls.Config{
97 InsecureSkipVerify: true,
98 VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
99 if len(rawCerts) == 0 {
100 return errors.New("no certificate received, cannot compute authorization header")
102 h := hmac.New(sha256.New, []byte(conn.cluster.SystemRootToken))
103 fmt.Fprint(h, opts.UUID)
104 authKey := fmt.Sprintf("%x", h.Sum(nil))
105 h = hmac.New(sha256.New, []byte(authKey))
107 requestAuth = fmt.Sprintf("%x", h.Sum(nil))
109 h.Write([]byte(requestAuth))
110 respondAuth = fmt.Sprintf("%x", h.Sum(nil))
115 err = httpserver.ErrorWithStatus(err, http.StatusBadGateway)
118 if respondAuth == "" {
119 err = httpserver.ErrorWithStatus(errors.New("BUG: no respondAuth"), http.StatusInternalServerError)
122 bufr := bufio.NewReader(netconn)
123 bufw := bufio.NewWriter(netconn)
127 Host: ctr.GatewayAddress,
130 bufw.WriteString("GET " + u.String() + " HTTP/1.1\r\n")
131 bufw.WriteString("Host: " + u.Host + "\r\n")
132 bufw.WriteString("Upgrade: ssh\r\n")
133 bufw.WriteString("X-Arvados-Target-Uuid: " + opts.UUID + "\r\n")
134 bufw.WriteString("X-Arvados-Authorization: " + requestAuth + "\r\n")
135 bufw.WriteString("X-Arvados-Detach-Keys: " + opts.DetachKeys + "\r\n")
136 bufw.WriteString("X-Arvados-Login-Username: " + opts.LoginUsername + "\r\n")
137 bufw.WriteString("\r\n")
139 resp, err := http.ReadResponse(bufr, &http.Request{Method: "GET"})
141 err = httpserver.ErrorWithStatus(fmt.Errorf("error reading http response from gateway: %w", err), http.StatusBadGateway)
145 if resp.Header.Get("X-Arvados-Authorization-Response") != respondAuth {
146 err = httpserver.ErrorWithStatus(errors.New("bad X-Arvados-Authorization-Response header"), http.StatusBadGateway)
150 if strings.ToLower(resp.Header.Get("Upgrade")) != "ssh" ||
151 strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
152 err = httpserver.ErrorWithStatus(errors.New("bad upgrade"), http.StatusBadGateway)
157 if !ctr.InteractiveSessionStarted {
158 _, err = conn.railsProxy.ContainerUpdate(ctxRoot, arvados.UpdateOptions{
160 Attrs: map[string]interface{}{
161 "interactive_session_started": true,
170 sshconn.Conn = netconn
171 sshconn.Bufrw = &bufio.ReadWriter{Reader: bufr, Writer: bufw}
172 sshconn.Logger = ctxlog.FromContext(ctx)