caddy/modules/caddytls/distributedstek/distributedstek.go
Matt Holt 3c90e370a4
v2: Module documentation; refactor LoadModule(); new caddy struct tags (#2924)
This commit goes a long way toward making automated documentation of
Caddy config and Caddy modules possible. It's a broad, sweeping change,
but mostly internal. It allows us to automatically generate docs for all
Caddy modules (including future third-party ones) and make them viewable
on a web page; it also doubles as godoc comments.

As such, this commit makes significant progress in migrating the docs
from our temporary wiki page toward our new website which is still under
construction.

With this change, all host modules will use ctx.LoadModule() and pass in
both the struct pointer and the field name as a string. This allows the
reflect package to read the struct tag from that field so that it can
get the necessary information like the module namespace and the inline
key.

This has the nice side-effect of unifying the code and documentation. It
also simplifies module loading, and handles several variations on field
types for raw module fields (i.e. variations on json.RawMessage, such as
arrays and maps).

I also renamed ModuleInfo.Name -> ModuleInfo.ID, to make it clear that
the ID is the "full name" which includes both the module namespace and
the name. This clarity is helpful when describing module hierarchy.

As of this change, Caddy modules are no longer an experimental design.
I think the architecture is good enough to go forward.
2019-12-10 13:36:46 -07:00

234 lines
6.8 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package distributedstek provides TLS session ticket ephemeral
// keys (STEKs) in a distributed fashion by utilizing configured
// storage for locking and key sharing. This allows a cluster of
// machines to optimally resume TLS sessions in a load-balanced
// environment without any hassle. This is similar to what
// Twitter does, but without needing to rely on SSH, as it is
// built into the web server this way:
// https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html
package distributedstek
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"log"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/mholt/certmagic"
)
func init() {
caddy.RegisterModule(Provider{})
}
// Provider implements a distributed STEK provider. This
// module will obtain STEKs from a storage module instead
// of generating STEKs internally. This allows STEKs to be
// coordinated, improving TLS session resumption in a cluster.
type Provider struct {
// The storage module wherein to store and obtain session
// ticket keys. If unset, Caddy's default/global-configured
// storage module will be used.
Storage json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
storage certmagic.Storage
stekConfig *caddytls.SessionTicketService
timer *time.Timer
}
// CaddyModule returns the Caddy module information.
func (Provider) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "tls.stek.distributed",
New: func() caddy.Module { return new(Provider) },
}
}
// Provision provisions s.
func (s *Provider) Provision(ctx caddy.Context) error {
// unpack the storage module to use, if different from the default
if s.Storage != nil {
val, err := ctx.LoadModule(s, "Storage")
if err != nil {
return fmt.Errorf("loading TLS storage module: %s", err)
}
cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
if err != nil {
return fmt.Errorf("creating TLS storage configuration: %v", err)
}
s.storage = cmStorage
}
// otherwise, use default storage
if s.storage == nil {
s.storage = ctx.Storage()
}
return nil
}
// Initialize sets the configuration for s and returns the starting keys.
func (s *Provider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
// keep a reference to the config; we'll need it when rotating keys
s.stekConfig = config
dstek, err := s.getSTEK()
if err != nil {
return nil, err
}
// create timer for the remaining time on the interval;
// this timer is cleaned up only when rotate() returns
s.timer = time.NewTimer(time.Until(dstek.NextRotation))
return dstek.Keys, nil
}
// Next returns a channel which transmits the latest session ticket keys.
func (s *Provider) Next(doneChan <-chan struct{}) <-chan [][32]byte {
keysChan := make(chan [][32]byte)
go s.rotate(doneChan, keysChan)
return keysChan
}
func (s *Provider) loadSTEK() (distributedSTEK, error) {
var sg distributedSTEK
gobBytes, err := s.storage.Load(stekFileName)
if err != nil {
return sg, err // don't wrap, in case error is certmagic.ErrNotExist
}
dec := gob.NewDecoder(bytes.NewReader(gobBytes))
err = dec.Decode(&sg)
if err != nil {
return sg, fmt.Errorf("STEK gob corrupted: %v", err)
}
return sg, nil
}
func (s *Provider) storeSTEK(dstek distributedSTEK) error {
var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(dstek)
if err != nil {
return fmt.Errorf("encoding STEK gob: %v", err)
}
err = s.storage.Store(stekFileName, buf.Bytes())
if err != nil {
return fmt.Errorf("storing STEK gob: %v", err)
}
return nil
}
// getSTEK locks and loads the current STEK from storage. If none
// currently exists, a new STEK is created and persisted. If the
// current STEK is outdated (NextRotation time is in the past),
// then it is rotated and persisted. The resulting STEK is returned.
func (s *Provider) getSTEK() (distributedSTEK, error) {
s.storage.Lock(stekLockName)
defer s.storage.Unlock(stekLockName)
// load the current STEKs from storage
dstek, err := s.loadSTEK()
if _, isNotExist := err.(certmagic.ErrNotExist); isNotExist {
// if there is none, then make some right away
dstek, err = s.rotateKeys(dstek)
if err != nil {
return dstek, fmt.Errorf("creating new STEK: %v", err)
}
} else if err != nil {
// some other error, that's a problem
return dstek, fmt.Errorf("loading STEK: %v", err)
} else if time.Now().After(dstek.NextRotation) {
// if current STEKs are outdated, rotate them
dstek, err = s.rotateKeys(dstek)
if err != nil {
return dstek, fmt.Errorf("rotating keys: %v", err)
}
}
return dstek, nil
}
// rotateKeys rotates the keys of oldSTEK and returns the new distributedSTEK
// with updated keys and timestamps. It stores the returned STEK in storage,
// so this function must only be called in a storage-provided lock.
func (s *Provider) rotateKeys(oldSTEK distributedSTEK) (distributedSTEK, error) {
var newSTEK distributedSTEK
var err error
newSTEK.Keys, err = s.stekConfig.RotateSTEKs(oldSTEK.Keys)
if err != nil {
return newSTEK, err
}
now := time.Now()
newSTEK.LastRotation = now
newSTEK.NextRotation = now.Add(time.Duration(s.stekConfig.RotationInterval))
err = s.storeSTEK(newSTEK)
if err != nil {
return newSTEK, err
}
return newSTEK, nil
}
// rotate rotates keys on a regular basis, sending each updated set of
// keys down keysChan, until doneChan is closed.
func (s *Provider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) {
for {
select {
case <-s.timer.C:
dstek, err := s.getSTEK()
if err != nil {
// TODO: improve this handling
log.Printf("[ERROR] Loading STEK: %v", err)
continue
}
// send the updated keys to the service
keysChan <- dstek.Keys
// timer channel is already drained, so reset directly (see godoc)
s.timer.Reset(time.Until(dstek.NextRotation))
case <-doneChan:
// again, see godocs for why timer is stopped this way
if !s.timer.Stop() {
<-s.timer.C
}
return
}
}
}
type distributedSTEK struct {
Keys [][32]byte
LastRotation, NextRotation time.Time
}
const (
stekLockName = "stek_check"
stekFileName = "stek/stek.bin"
)
// Interface guard
var _ caddytls.STEKProvider = (*Provider)(nil)