21702: Merge branch 'main' into 21702-keep-web-replace_files
[arvados.git] / lib / service / tls.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package service
6
7 import (
8         "context"
9         "crypto/tls"
10         "errors"
11         "fmt"
12         "os"
13         "os/signal"
14         "strings"
15         "syscall"
16         "time"
17
18         "git.arvados.org/arvados.git/sdk/go/arvados"
19         "github.com/sirupsen/logrus"
20         "golang.org/x/crypto/acme/autocert"
21 )
22
23 func makeTLSConfig(cluster *arvados.Cluster, logger logrus.FieldLogger) (*tls.Config, error) {
24         if cluster.TLS.ACME.Server != "" {
25                 return makeAutocertConfig(cluster, logger)
26         } else {
27                 return makeFileLoaderConfig(cluster, logger)
28         }
29 }
30
31 var errCertUnavailable = errors.New("certificate unavailable, waiting for supervisor to update cache")
32
33 type readonlyDirCache autocert.DirCache
34
35 func (c readonlyDirCache) Get(ctx context.Context, name string) ([]byte, error) {
36         data, err := autocert.DirCache(c).Get(ctx, name)
37         if err != nil {
38                 // Returning an error other than autocert.ErrCacheMiss
39                 // causes GetCertificate() to fail early instead of
40                 // trying to obtain a certificate itself (which
41                 // wouldn't work because we're not in a position to
42                 // answer challenges).
43                 return nil, errCertUnavailable
44         }
45         return data, nil
46 }
47
48 func (c readonlyDirCache) Put(ctx context.Context, name string, data []byte) error {
49         return fmt.Errorf("(bug?) (readonlyDirCache)Put(%s) called", name)
50 }
51
52 func (c readonlyDirCache) Delete(ctx context.Context, name string) error {
53         return nil
54 }
55
56 func makeAutocertConfig(cluster *arvados.Cluster, logger logrus.FieldLogger) (*tls.Config, error) {
57         mgr := &autocert.Manager{
58                 Cache:  readonlyDirCache("/var/lib/arvados/tmp/autocert"),
59                 Prompt: autocert.AcceptTOS,
60                 // HostPolicy accepts all names because this Manager
61                 // doesn't request certs. Whoever writes certs to our
62                 // cache is effectively responsible for HostPolicy.
63                 HostPolicy: func(ctx context.Context, host string) error { return nil },
64                 // Keep using whatever's in the cache as long as
65                 // possible. Assume some other process (see lib/boot)
66                 // handles renewals.
67                 RenewBefore: time.Second,
68         }
69         return mgr.TLSConfig(), nil
70 }
71
72 func makeFileLoaderConfig(cluster *arvados.Cluster, logger logrus.FieldLogger) (*tls.Config, error) {
73         currentCert := make(chan *tls.Certificate, 1)
74         loaded := false
75
76         key := strings.TrimPrefix(cluster.TLS.Key, "file://")
77         cert := strings.TrimPrefix(cluster.TLS.Certificate, "file://")
78
79         update := func() error {
80                 cert, err := tls.LoadX509KeyPair(cert, key)
81                 if err != nil {
82                         return fmt.Errorf("error loading X509 key pair: %s", err)
83                 }
84                 if loaded {
85                         // Throw away old cert
86                         <-currentCert
87                 }
88                 currentCert <- &cert
89                 loaded = true
90                 return nil
91         }
92         err := update()
93         if err != nil {
94                 return nil, err
95         }
96
97         reload := make(chan os.Signal, 1)
98         signal.Notify(reload, syscall.SIGHUP)
99         go func() {
100                 for range time.NewTicker(time.Hour).C {
101                         reload <- nil
102                 }
103         }()
104         go func() {
105                 for range reload {
106                         err := update()
107                         if err != nil {
108                                 logger.WithError(err).Warn("error updating TLS certificate")
109                         }
110                 }
111         }()
112
113         // https://blog.gopheracademy.com/advent-2016/exposing-go-on-the-internet/
114         return &tls.Config{
115                 PreferServerCipherSuites: true,
116                 CurvePreferences: []tls.CurveID{
117                         tls.CurveP256,
118                         tls.X25519,
119                 },
120                 MinVersion: tls.VersionTLS12,
121                 CipherSuites: []uint16{
122                         tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
123                         tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
124                         tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
125                         tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
126                         tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
127                         tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
128                 },
129                 GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
130                         cert := <-currentCert
131                         currentCert <- cert
132                         return cert, nil
133                 },
134         }, nil
135 }