e509278773ae288f9368e78e98ee059f656560a8
[arvados.git] / lib / controller / localdb / container_gateway.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package localdb
6
7 import (
8         "bufio"
9         "context"
10         "crypto/hmac"
11         "crypto/sha256"
12         "crypto/tls"
13         "crypto/x509"
14         "errors"
15         "fmt"
16         "net/http"
17         "net/url"
18         "strings"
19
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"
24 )
25
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.
29 //
30 // If the returned error is nil, the caller is responsible for closing
31 // sshconn.Conn.
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{})
34         if err != nil {
35                 return
36         }
37         ctr, err := conn.railsProxy.ContainerGet(ctx, arvados.GetOptions{UUID: opts.UUID})
38         if err != nil {
39                 return
40         }
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)
45                         return
46                 }
47                 var crs arvados.ContainerRequestList
48                 crs, err = conn.railsProxy.ContainerRequestList(ctxRoot, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"container_uuid", "=", opts.UUID}}})
49                 if err != nil {
50                         return
51                 }
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)
55                                 return
56                         }
57                 }
58                 if crs.ItemsAvailable != len(crs.Items) {
59                         err = httpserver.ErrorWithStatus(errors.New("incomplete response while checking permission"), http.StatusInternalServerError)
60                         return
61                 }
62         }
63
64         switch ctr.State {
65         case arvados.ContainerStateQueued, arvados.ContainerStateLocked:
66                 err = httpserver.ErrorWithStatus(fmt.Errorf("gateway is not available, container is %s", strings.ToLower(string(ctr.State))), http.StatusServiceUnavailable)
67                 return
68         case arvados.ContainerStateRunning:
69                 if ctr.GatewayAddress == "" {
70                         err = httpserver.ErrorWithStatus(errors.New("container is running but gateway is not available"), http.StatusServiceUnavailable)
71                         return
72                 }
73         default:
74                 err = httpserver.ErrorWithStatus(fmt.Errorf("gateway is not available, container is %s", strings.ToLower(string(ctr.State))), http.StatusGone)
75                 return
76         }
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.
80         //
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.
84         //
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.)
90         //
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")
101                         }
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))
106                         h.Write(rawCerts[0])
107                         requestAuth = fmt.Sprintf("%x", h.Sum(nil))
108                         h.Reset()
109                         h.Write([]byte(requestAuth))
110                         respondAuth = fmt.Sprintf("%x", h.Sum(nil))
111                         return nil
112                 },
113         })
114         if err != nil {
115                 err = httpserver.ErrorWithStatus(err, http.StatusBadGateway)
116                 return
117         }
118         if respondAuth == "" {
119                 err = httpserver.ErrorWithStatus(errors.New("BUG: no respondAuth"), http.StatusInternalServerError)
120                 return
121         }
122         bufr := bufio.NewReader(netconn)
123         bufw := bufio.NewWriter(netconn)
124
125         u := url.URL{
126                 Scheme: "http",
127                 Host:   ctr.GatewayAddress,
128                 Path:   "/ssh",
129         }
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")
138         bufw.Flush()
139         resp, err := http.ReadResponse(bufr, &http.Request{Method: "GET"})
140         if err != nil {
141                 err = httpserver.ErrorWithStatus(fmt.Errorf("error reading http response from gateway: %w", err), http.StatusBadGateway)
142                 netconn.Close()
143                 return
144         }
145         if resp.Header.Get("X-Arvados-Authorization-Response") != respondAuth {
146                 err = httpserver.ErrorWithStatus(errors.New("bad X-Arvados-Authorization-Response header"), http.StatusBadGateway)
147                 netconn.Close()
148                 return
149         }
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)
153                 netconn.Close()
154                 return
155         }
156
157         if !ctr.InteractiveSessionStarted {
158                 _, err = conn.railsProxy.ContainerUpdate(ctxRoot, arvados.UpdateOptions{
159                         UUID: opts.UUID,
160                         Attrs: map[string]interface{}{
161                                 "interactive_session_started": true,
162                         },
163                 })
164                 if err != nil {
165                         netconn.Close()
166                         return
167                 }
168         }
169
170         sshconn.Conn = netconn
171         sshconn.Bufrw = &bufio.ReadWriter{Reader: bufr, Writer: bufw}
172         sshconn.Logger = ctxlog.FromContext(ctx)
173         return
174 }