17170: Use TLS for controller->crunch-run traffic.
[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/ctxlog"
22         "git.arvados.org/arvados.git/sdk/go/httpserver"
23 )
24
25 // ContainerSSH returns a connection to the SSH server in the
26 // appropriate crunch-run process on the worker node where the
27 // specified container is running.
28 //
29 // If the returned error is nil, the caller is responsible for closing
30 // sshconn.Conn.
31 func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOptions) (sshconn arvados.ContainerSSHConnection, err error) {
32         ctr, err := conn.railsProxy.ContainerGet(ctx, arvados.GetOptions{UUID: opts.UUID})
33         if err != nil {
34                 return
35         }
36         if ctr.GatewayAddress == "" || ctr.State != arvados.ContainerStateRunning {
37                 err = httpserver.ErrorWithStatus(fmt.Errorf("gateway is not available, container is %s", strings.ToLower(string(ctr.State))), http.StatusBadGateway)
38                 return
39         }
40         // crunch-run uses a self-signed / unverifiable TLS
41         // certificate, so we use the following scheme to ensure we're
42         // not talking to a MITM.
43         //
44         // 1. Compute ctrKey = HMAC-SHA256(sysRootToken,ctrUUID) --
45         // this will be the same ctrKey that a-d-c supplied to
46         // crunch-run in the GatewayAuthSecret env var.
47         //
48         // 2. Compute requestAuth = HMAC-SHA256(ctrKey,serverCert) and
49         // send it to crunch-run as the X-Arvados-Authorization
50         // header, proving that we know ctrKey. (Note a MITM cannot
51         // replay the proof to a real crunch-run server, because the
52         // real crunch-run server would have a different cert.)
53         //
54         // 3. Compute respondAuth = HMAC-SHA256(ctrKey,requestAuth)
55         // and ensure the server returns it in the
56         // X-Arvados-Authorization-Response header, proving that the
57         // server knows ctrKey.
58         var requestAuth, respondAuth string
59         netconn, err := tls.Dial("tcp", ctr.GatewayAddress, &tls.Config{
60                 InsecureSkipVerify: true,
61                 VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
62                         if len(rawCerts) == 0 {
63                                 return errors.New("no certificate received, cannot compute authorization header")
64                         }
65                         h := hmac.New(sha256.New, []byte(conn.cluster.SystemRootToken))
66                         fmt.Fprint(h, "%s", opts.UUID)
67                         authKey := fmt.Sprintf("%x", h.Sum(nil))
68                         h = hmac.New(sha256.New, []byte(authKey))
69                         h.Write(rawCerts[0])
70                         requestAuth = fmt.Sprintf("%x", h.Sum(nil))
71                         h.Reset()
72                         h.Write([]byte(requestAuth))
73                         respondAuth = fmt.Sprintf("%x", h.Sum(nil))
74                         return nil
75                 },
76         })
77         if err != nil {
78                 return
79         }
80         if respondAuth == "" {
81                 err = httpserver.ErrorWithStatus(errors.New("BUG: no respondAuth"), http.StatusInternalServerError)
82                 return
83         }
84         bufr := bufio.NewReader(netconn)
85         bufw := bufio.NewWriter(netconn)
86
87         u := url.URL{
88                 Scheme: "http",
89                 Host:   ctr.GatewayAddress,
90                 Path:   "/ssh",
91         }
92         bufw.WriteString("GET " + u.String() + " HTTP/1.1\r\n")
93         bufw.WriteString("Host: " + u.Host + "\r\n")
94         bufw.WriteString("Upgrade: ssh\r\n")
95         bufw.WriteString("X-Arvados-Target-Uuid: " + opts.UUID + "\r\n")
96         bufw.WriteString("X-Arvados-Authorization: " + requestAuth + "\r\n")
97         bufw.WriteString("X-Arvados-Detach-Keys: " + opts.DetachKeys + "\r\n")
98         bufw.WriteString("X-Arvados-Login-Username: " + opts.LoginUsername + "\r\n")
99         bufw.WriteString("\r\n")
100         bufw.Flush()
101         resp, err := http.ReadResponse(bufr, &http.Request{Method: "GET"})
102         if err != nil {
103                 err = httpserver.ErrorWithStatus(fmt.Errorf("error reading http response from gateway: %w", err), http.StatusBadGateway)
104                 netconn.Close()
105                 return
106         }
107         if resp.Header.Get("X-Arvados-Authorization-Response") != respondAuth {
108                 err = httpserver.ErrorWithStatus(errors.New("bad X-Arvados-Authorization-Response header"), http.StatusBadGateway)
109                 netconn.Close()
110                 return
111         }
112         if strings.ToLower(resp.Header.Get("Upgrade")) != "ssh" ||
113                 strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
114                 err = httpserver.ErrorWithStatus(errors.New("bad upgrade"), http.StatusBadGateway)
115                 netconn.Close()
116                 return
117         }
118         sshconn.Conn = netconn
119         sshconn.Bufrw = &bufio.ReadWriter{Reader: bufr, Writer: bufw}
120         sshconn.Logger = ctxlog.FromContext(ctx)
121         return
122 }