@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 250 lines 7.14 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 // Package distributedstek provides TLS session ticket ephemeral
16 // keys (STEKs) in a distributed fashion by utilizing configured
17 // storage for locking and key sharing. This allows a cluster of
18 // machines to optimally resume TLS sessions in a load-balanced
19 // environment without any hassle. This is similar to what
20 // Twitter does, but without needing to rely on SSH, as it is
21 // built into the web server this way:
22 // https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html
23 package distributedstek
24
25 import (
26 "bytes"
27 "encoding/gob"
28 "encoding/json"
29 "errors"
30 "fmt"
31 "io/fs"
32 "log"
33 "runtime/debug"
34 "time"
35
36 "github.com/caddyserver/certmagic"
37
38 "github.com/caddyserver/caddy/v2"
39 "github.com/caddyserver/caddy/v2/modules/caddytls"
40 )
41
42 func init() {
43 caddy.RegisterModule(Provider{})
44 }
45
46 // Provider implements a distributed STEK provider. This
47 // module will obtain STEKs from a storage module instead
48 // of generating STEKs internally. This allows STEKs to be
49 // coordinated, improving TLS session resumption in a cluster.
50 type Provider struct {
51 // The storage module wherein to store and obtain session
52 // ticket keys. If unset, Caddy's default/global-configured
53 // storage module will be used.
54 Storage json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
55
56 storage certmagic.Storage
57 stekConfig *caddytls.SessionTicketService
58 timer *time.Timer
59 ctx caddy.Context
60 }
61
62 // CaddyModule returns the Caddy module information.
63 func (Provider) CaddyModule() caddy.ModuleInfo {
64 return caddy.ModuleInfo{
65 ID: "tls.stek.distributed",
66 New: func() caddy.Module { return new(Provider) },
67 }
68 }
69
70 // Provision provisions s.
71 func (s *Provider) Provision(ctx caddy.Context) error {
72 s.ctx = ctx
73
74 // unpack the storage module to use, if different from the default
75 if s.Storage != nil {
76 val, err := ctx.LoadModule(s, "Storage")
77 if err != nil {
78 return fmt.Errorf("loading TLS storage module: %s", err)
79 }
80 cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
81 if err != nil {
82 return fmt.Errorf("creating TLS storage configuration: %v", err)
83 }
84 s.storage = cmStorage
85 }
86
87 // otherwise, use default storage
88 if s.storage == nil {
89 s.storage = ctx.Storage()
90 }
91
92 return nil
93 }
94
95 // Initialize sets the configuration for s and returns the starting keys.
96 func (s *Provider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
97 // keep a reference to the config; we'll need it when rotating keys
98 s.stekConfig = config
99
100 dstek, err := s.getSTEK()
101 if err != nil {
102 return nil, err
103 }
104
105 // create timer for the remaining time on the interval;
106 // this timer is cleaned up only when rotate() returns
107 s.timer = time.NewTimer(time.Until(dstek.NextRotation))
108
109 return dstek.Keys, nil
110 }
111
112 // Next returns a channel which transmits the latest session ticket keys.
113 func (s *Provider) Next(doneChan <-chan struct{}) <-chan [][32]byte {
114 keysChan := make(chan [][32]byte)
115 go s.rotate(doneChan, keysChan)
116 return keysChan
117 }
118
119 func (s *Provider) loadSTEK() (distributedSTEK, error) {
120 var sg distributedSTEK
121 gobBytes, err := s.storage.Load(s.ctx, stekFileName)
122 if err != nil {
123 return sg, err // don't wrap, in case error is certmagic.ErrNotExist
124 }
125 dec := gob.NewDecoder(bytes.NewReader(gobBytes))
126 err = dec.Decode(&sg)
127 if err != nil {
128 return sg, fmt.Errorf("STEK gob corrupted: %v", err)
129 }
130 return sg, nil
131 }
132
133 func (s *Provider) storeSTEK(dstek distributedSTEK) error {
134 var buf bytes.Buffer
135 err := gob.NewEncoder(&buf).Encode(dstek)
136 if err != nil {
137 return fmt.Errorf("encoding STEK gob: %v", err)
138 }
139 err = s.storage.Store(s.ctx, stekFileName, buf.Bytes())
140 if err != nil {
141 return fmt.Errorf("storing STEK gob: %v", err)
142 }
143 return nil
144 }
145
146 // getSTEK locks and loads the current STEK from storage. If none
147 // currently exists, a new STEK is created and persisted. If the
148 // current STEK is outdated (NextRotation time is in the past),
149 // then it is rotated and persisted. The resulting STEK is returned.
150 func (s *Provider) getSTEK() (distributedSTEK, error) {
151 err := s.storage.Lock(s.ctx, stekLockName)
152 if err != nil {
153 return distributedSTEK{}, fmt.Errorf("failed to acquire storage lock: %v", err)
154 }
155
156 //nolint:errcheck
157 defer s.storage.Unlock(s.ctx, stekLockName)
158
159 // load the current STEKs from storage
160 dstek, err := s.loadSTEK()
161 if errors.Is(err, fs.ErrNotExist) {
162 // if there is none, then make some right away
163 dstek, err = s.rotateKeys(dstek)
164 if err != nil {
165 return dstek, fmt.Errorf("creating new STEK: %v", err)
166 }
167 } else if err != nil {
168 // some other error, that's a problem
169 return dstek, fmt.Errorf("loading STEK: %v", err)
170 } else if time.Now().After(dstek.NextRotation) {
171 // if current STEKs are outdated, rotate them
172 dstek, err = s.rotateKeys(dstek)
173 if err != nil {
174 return dstek, fmt.Errorf("rotating keys: %v", err)
175 }
176 }
177
178 return dstek, nil
179 }
180
181 // rotateKeys rotates the keys of oldSTEK and returns the new distributedSTEK
182 // with updated keys and timestamps. It stores the returned STEK in storage,
183 // so this function must only be called in a storage-provided lock.
184 func (s *Provider) rotateKeys(oldSTEK distributedSTEK) (distributedSTEK, error) {
185 var newSTEK distributedSTEK
186 var err error
187
188 newSTEK.Keys, err = s.stekConfig.RotateSTEKs(oldSTEK.Keys)
189 if err != nil {
190 return newSTEK, err
191 }
192
193 now := time.Now()
194 newSTEK.LastRotation = now
195 newSTEK.NextRotation = now.Add(time.Duration(s.stekConfig.RotationInterval))
196
197 err = s.storeSTEK(newSTEK)
198 if err != nil {
199 return newSTEK, err
200 }
201
202 return newSTEK, nil
203 }
204
205 // rotate rotates keys on a regular basis, sending each updated set of
206 // keys down keysChan, until doneChan is closed.
207 func (s *Provider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) {
208 defer func() {
209 if err := recover(); err != nil {
210 log.Printf("[PANIC] distributed STEK rotation: %v\n%s", err, debug.Stack())
211 }
212 }()
213 for {
214 select {
215 case <-s.timer.C:
216 dstek, err := s.getSTEK()
217 if err != nil {
218 // TODO: improve this handling
219 log.Printf("[ERROR] Loading STEK: %v", err)
220 continue
221 }
222
223 // send the updated keys to the service
224 keysChan <- dstek.Keys
225
226 // timer channel is already drained, so reset directly (see godoc)
227 s.timer.Reset(time.Until(dstek.NextRotation))
228
229 case <-doneChan:
230 // again, see godocs for why timer is stopped this way
231 if !s.timer.Stop() {
232 <-s.timer.C
233 }
234 return
235 }
236 }
237 }
238
239 type distributedSTEK struct {
240 Keys [][32]byte
241 LastRotation, NextRotation time.Time
242 }
243
244 const (
245 stekLockName = "stek_check"
246 stekFileName = "stek/stek.bin"
247 )
248
249 // Interface guard
250 var _ caddytls.STEKProvider = (*Provider)(nil)