21cd3e2ac25d8030e766d0a7e572a4de50209db1
[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.Automatic {
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         if !strings.HasPrefix(key, "file://") || !strings.HasPrefix(cert, "file://") {
79         }
80         key, cert = key[7:], cert[7:]
81
82         update := func() error {
83                 cert, err := tls.LoadX509KeyPair(cert, key)
84                 if err != nil {
85                         return fmt.Errorf("error loading X509 key pair: %s", err)
86                 }
87                 if loaded {
88                         // Throw away old cert
89                         <-currentCert
90                 }
91                 currentCert <- &cert
92                 loaded = true
93                 return nil
94         }
95         err := update()
96         if err != nil {
97                 return nil, err
98         }
99
100         reload := make(chan os.Signal, 1)
101         signal.Notify(reload, syscall.SIGHUP)
102         go func() {
103                 for range time.NewTicker(time.Hour).C {
104                         reload <- nil
105                 }
106         }()
107         go func() {
108                 for range reload {
109                         err := update()
110                         if err != nil {
111                                 logger.WithError(err).Warn("error updating TLS certificate")
112                         }
113                 }
114         }()
115
116         // https://blog.gopheracademy.com/advent-2016/exposing-go-on-the-internet/
117         return &tls.Config{
118                 PreferServerCipherSuites: true,
119                 CurvePreferences: []tls.CurveID{
120                         tls.CurveP256,
121                         tls.X25519,
122                 },
123                 MinVersion: tls.VersionTLS12,
124                 CipherSuites: []uint16{
125                         tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
126                         tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
127                         tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
128                         tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
129                         tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
130                         tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
131                 },
132                 GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
133                         cert := <-currentCert
134                         currentCert <- cert
135                         return cert, nil
136                 },
137         }, nil
138 }