@marcel91200 / caddy Mirror · read-only / commits / dedcfd4e
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 14, 05:15 UTC

tls: Add distributed_stek module

This migrates a feature that was previously reserved for enterprise users, according to https://github.com/caddyserver/caddy/issues/2786. TLS session ticket keys are sensitive, so they should be rotated on a regular basis. Only Caddy does this by default. However, a cluster of servers that rotate keys without synchronization will lose the benefits of having sessions in the first place if the client is routed to a different backend. This module coordinates STEK rotation in a fleet so the same keys are used, and rotated, across the whole cluster. No other server does this, but Twitter wrote about how they hacked together a solution a few years ago: https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html

Matthew Holt committed Oct 9, 2019 at 19:38 UTC dedcfd4e3da1297cda2d09776e7c8135e0d960ce
1 file changed +228
modules/caddytls/distributedstek/distributedstek.go new
+228
@@ -0,0 +1,228 @@
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 + "fmt"
30 + "log"
31 + "time"
32 +
33 + "github.com/caddyserver/caddy/v2"
34 + "github.com/caddyserver/caddy/v2/modules/caddytls"
35 + "github.com/mholt/certmagic"
36 +)
37 +
38 +func init() {
39 + caddy.RegisterModule(Provider{})
40 +}
41 +
42 +// Provider implements a distributed STEK provider.
43 +type Provider struct {
44 + Storage json.RawMessage `json:"storage,omitempty"`
45 +
46 + storage certmagic.Storage
47 + stekConfig *caddytls.SessionTicketService
48 + timer *time.Timer
49 +}
50 +
51 +// CaddyModule returns the Caddy module information.
52 +func (Provider) CaddyModule() caddy.ModuleInfo {
53 + return caddy.ModuleInfo{
54 + Name: "tls.stek.distributed",
55 + New: func() caddy.Module { return new(Provider) },
56 + }
57 +}
58 +
59 +// Provision provisions s.
60 +func (s *Provider) Provision(ctx caddy.Context) error {
61 + // unpack the storage module to use, if different from the default
62 + if s.Storage != nil {
63 + val, err := ctx.LoadModuleInline("module", "caddy.storage", s.Storage)
64 + if err != nil {
65 + return fmt.Errorf("loading TLS storage module: %s", err)
66 + }
67 + cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
68 + if err != nil {
69 + return fmt.Errorf("creating TLS storage configuration: %v", err)
70 + }
71 + s.storage = cmStorage
72 + s.Storage = nil // allow GC to deallocate
73 + }
74 +
75 + // otherwise, use default storage
76 + if s.storage == nil {
77 + s.storage = ctx.Storage()
78 + }
79 +
80 + return nil
81 +}
82 +
83 +// Initialize sets the configuration for s and returns the starting keys.
84 +func (s *Provider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
85 + // keep a reference to the config; we'll need it when rotating keys
86 + s.stekConfig = config
87 +
88 + dstek, err := s.getSTEK()
89 + if err != nil {
90 + return nil, err
91 + }
92 +
93 + // create timer for the remaining time on the interval;
94 + // this timer is cleaned up only when rotate() returns
95 + s.timer = time.NewTimer(time.Until(dstek.NextRotation))
96 +
97 + return dstek.Keys, nil
98 +}
99 +
100 +// Next returns a channel which transmits the latest session ticket keys.
101 +func (s *Provider) Next(doneChan <-chan struct{}) <-chan [][32]byte {
102 + keysChan := make(chan [][32]byte)
103 + go s.rotate(doneChan, keysChan)
104 + return keysChan
105 +}
106 +
107 +func (s *Provider) loadSTEK() (distributedSTEK, error) {
108 + var sg distributedSTEK
109 + gobBytes, err := s.storage.Load(stekFileName)
110 + if err != nil {
111 + return sg, err // don't wrap, in case error is certmagic.ErrNotExist
112 + }
113 + dec := gob.NewDecoder(bytes.NewReader(gobBytes))
114 + err = dec.Decode(&sg)
115 + if err != nil {
116 + return sg, fmt.Errorf("STEK gob corrupted: %v", err)
117 + }
118 + return sg, nil
119 +}
120 +
121 +func (s *Provider) storeSTEK(dstek distributedSTEK) error {
122 + var buf bytes.Buffer
123 + err := gob.NewEncoder(&buf).Encode(dstek)
124 + if err != nil {
125 + return fmt.Errorf("encoding STEK gob: %v", err)
126 + }
127 + err = s.storage.Store(stekFileName, buf.Bytes())
128 + if err != nil {
129 + return fmt.Errorf("storing STEK gob: %v", err)
130 + }
131 + return nil
132 +}
133 +
134 +// getSTEK locks and loads the current STEK from storage. If none
135 +// currently exists, a new STEK is created and persisted. If the
136 +// current STEK is outdated (NextRotation time is in the past),
137 +// then it is rotated and persisted. The resulting STEK is returned.
138 +func (s *Provider) getSTEK() (distributedSTEK, error) {
139 + s.storage.Lock(stekLockName)
140 + defer s.storage.Unlock(stekLockName)
141 +
142 + // load the current STEKs from storage
143 + dstek, err := s.loadSTEK()
144 + if _, isNotExist := err.(certmagic.ErrNotExist); isNotExist {
145 + // if there is none, then make some right away
146 + dstek, err = s.rotateKeys(dstek)
147 + if err != nil {
148 + return dstek, fmt.Errorf("creating new STEK: %v", err)
149 + }
150 + } else if err != nil {
151 + // some other error, that's a problem
152 + return dstek, fmt.Errorf("loading STEK: %v", err)
153 + } else if time.Now().After(dstek.NextRotation) {
154 + // if current STEKs are outdated, rotate them
155 + dstek, err = s.rotateKeys(dstek)
156 + if err != nil {
157 + return dstek, fmt.Errorf("rotating keys: %v", err)
158 + }
159 + }
160 +
161 + return dstek, nil
162 +}
163 +
164 +// rotateKeys rotates the keys of oldSTEK and returns the new distributedSTEK
165 +// with updated keys and timestamps. It stores the returned STEK in storage,
166 +// so this function must only be called in a storage-provided lock.
167 +func (s *Provider) rotateKeys(oldSTEK distributedSTEK) (distributedSTEK, error) {
168 + var newSTEK distributedSTEK
169 + var err error
170 +
171 + newSTEK.Keys, err = s.stekConfig.RotateSTEKs(oldSTEK.Keys)
172 + if err != nil {
173 + return newSTEK, err
174 + }
175 +
176 + now := time.Now()
177 + newSTEK.LastRotation = now
178 + newSTEK.NextRotation = now.Add(time.Duration(s.stekConfig.RotationInterval))
179 +
180 + err = s.storeSTEK(newSTEK)
181 + if err != nil {
182 + return newSTEK, err
183 + }
184 +
185 + return newSTEK, nil
186 +}
187 +
188 +// rotate rotates keys on a regular basis, sending each updated set of
189 +// keys down keysChan, until doneChan is closed.
190 +func (s *Provider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) {
191 + for {
192 + select {
193 + case <-s.timer.C:
194 + dstek, err := s.getSTEK()
195 + if err != nil {
196 + // TODO: improve this handling
197 + log.Printf("[ERROR] Loading STEK: %v", err)
198 + continue
199 + }
200 +
201 + // send the updated keys to the service
202 + keysChan <- dstek.Keys
203 +
204 + // timer channel is already drained, so reset directly (see godoc)
205 + s.timer.Reset(time.Until(dstek.NextRotation))
206 +
207 + case <-doneChan:
208 + // again, see godocs for why timer is stopped this way
209 + if !s.timer.Stop() {
210 + <-s.timer.C
211 + }
212 + return
213 + }
214 + }
215 +}
216 +
217 +type distributedSTEK struct {
218 + Keys [][32]byte
219 + LastRotation, NextRotation time.Time
220 +}
221 +
222 +const (
223 + stekLockName = "stek_check"
224 + stekFileName = "stek/stek.bin"
225 +)
226 +
227 +// Interface guard
228 +var _ caddytls.STEKProvider = (*Provider)(nil)