envoy: support autocert (#695)

* envoy: support autocert

* envoy: fallback to http host routing if sni fails to match

* update comment

* envoy: renew certs when necessary

* fix tests
This commit is contained in:
Caleb Doxsey 2020-05-13 13:07:04 -06:00 committed by Travis Groth
parent 0c1ac5a575
commit dccec1e646
18 changed files with 689 additions and 391 deletions

View file

@ -1,15 +1,16 @@
package config
import (
"bytes"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
@ -68,10 +69,6 @@ type Options struct {
// and renewal from LetsEncrypt. Must be used in conjunction with AutoCertFolder.
AutoCert bool `mapstructure:"autocert" yaml:"autocert,omitempty"`
// AutoCertHandler is the HTTP challenge handler used in a http-01 acme
// https://letsencrypt.org/docs/challenge-types/#http-01-challenge
AutoCertHandler func(h http.Handler) http.Handler `hash:"ignore"`
// AutoCertFolder specifies the location to store, and load autocert managed
// TLS certificates.
// defaults to $XDG_DATA_HOME/pomerium
@ -83,7 +80,7 @@ type Options struct {
// https://letsencrypt.org/docs/staging-environment/
AutoCertUseStaging bool `mapstructure:"autocert_use_staging" yaml:"autocert_use_staging,omitempty"`
Certificates []certificateFilePair `mapstructure:"certificates" yaml:"certificates,omitempty"`
CertificateFiles []certificateFilePair `mapstructure:"certificates" yaml:"certificates,omitempty"`
// Cert and Key is the x509 certificate used to create the HTTPS server.
Cert string `mapstructure:"certificate" yaml:"certificate,omitempty"`
@ -93,7 +90,7 @@ type Options struct {
CertFile string `mapstructure:"certificate_file" yaml:"certificate_file,omitempty"`
KeyFile string `mapstructure:"certificate_key_file" yaml:"certificate_key_file,omitempty"`
TLSConfig *tls.Config `hash:"ignore"`
Certificates []tls.Certificate `yaml:"-"`
// HttpRedirectAddr, if set, specifies the host and port to run the HTTP
// to HTTPS redirect server on. If empty, no redirect server is started.
@ -534,38 +531,42 @@ func (o *Options) Validate() error {
}
if o.Cert != "" || o.Key != "" {
o.TLSConfig, err = cryptutil.TLSConfigFromBase64(o.TLSConfig, o.Cert, o.Key)
cert, err := cryptutil.CertificateFromBase64(o.Cert, o.Key)
if err != nil {
return fmt.Errorf("config: bad cert base64 %w", err)
}
o.Certificates = append(o.Certificates, *cert)
}
if len(o.Certificates) != 0 {
for _, c := range o.Certificates {
o.TLSConfig, err = cryptutil.TLSConfigFromFile(o.TLSConfig, c.CertFile, c.KeyFile)
if err != nil {
return fmt.Errorf("config: bad cert file %w", err)
}
}
}
if o.CertFile != "" || o.KeyFile != "" {
o.TLSConfig, err = cryptutil.TLSConfigFromFile(o.TLSConfig, o.CertFile, o.KeyFile)
for _, c := range o.CertificateFiles {
cert, err := cryptutil.CertificateFromFile(c.CertFile, c.KeyFile)
if err != nil {
return fmt.Errorf("config: bad cert file %w", err)
}
o.Certificates = append(o.Certificates, *cert)
}
if o.AutoCert {
o.TLSConfig, o.AutoCertHandler, err = cryptutil.NewAutocert(
o.TLSConfig,
o.sourceHostnames(),
o.AutoCertUseStaging,
o.AutoCertFolder)
if o.CertFile != "" || o.KeyFile != "" {
cert, err := cryptutil.CertificateFromFile(o.CertFile, o.KeyFile)
if err != nil {
return fmt.Errorf("config: autocert failed %w", err)
return fmt.Errorf("config: bad cert file %w", err)
}
o.Certificates = append(o.Certificates, *cert)
}
if !o.InsecureServer && o.TLSConfig == nil {
RedirectAndAutocertServer.update(o)
err = AutocertManager.update(o)
if err != nil {
return fmt.Errorf("config: failed to setup autocert: %w", err)
}
// sort the certificates so we get a consistent hash
sort.Slice(o.Certificates, func(i, j int) bool {
return compareByteSliceSlice(o.Certificates[i].Certificate, o.Certificates[j].Certificate) < 0
})
if !o.InsecureServer && len(o.Certificates) == 0 {
return fmt.Errorf("config: server must be run with `autocert`, " +
"`insecure_server` or manually provided certificates to start")
}
@ -576,13 +577,21 @@ func (o *Options) sourceHostnames() []string {
if len(o.Policies) == 0 {
return nil
}
var h []string
dedupe := map[string]struct{}{}
for _, p := range o.Policies {
h = append(h, p.Source.Hostname())
dedupe[p.Source.Hostname()] = struct{}{}
}
if o.AuthenticateURL != nil {
h = append(h, o.AuthenticateURL.Hostname())
dedupe[o.AuthenticateURL.Hostname()] = struct{}{}
}
var h []string
for k := range dedupe {
h = append(h, k)
}
sort.Strings(h)
return h
}
@ -601,10 +610,37 @@ func (o *Options) Checksum() uint64 {
return hash
}
// HandleConfigUpdate takes configuration file, an existing options struct, and
// WatchChanges takes a configuration file, an existing options struct, and
// updates each service in the services slice OptionsUpdater with a new set
// of options if any change is detected. It also periodically rechecks if
// any computed properties have changed.
func WatchChanges(configFile string, opt *Options, services []OptionsUpdater) {
onchange := make(chan struct{}, 1)
ticker := time.NewTicker(10 * time.Minute) // force check every 10 minutes
defer ticker.Stop()
opt.OnConfigChange(func(fs fsnotify.Event) {
log.Info().Str("file", fs.Name).Msg("config: file changed")
select {
case onchange <- struct{}{}:
default:
}
})
for {
select {
case <-onchange:
case <-ticker.C:
}
opt = handleConfigUpdate(configFile, opt, services)
}
}
// handleConfigUpdate takes configuration file, an existing options struct, and
// updates each service in the services slice OptionsUpdater with a new set of
// options if any change is detected.
func HandleConfigUpdate(configFile string, opt *Options, services []OptionsUpdater) *Options {
func handleConfigUpdate(configFile string, opt *Options, services []OptionsUpdater) *Options {
newOpt, err := NewOptionsFromConfig(configFile)
if err != nil {
log.Error().Err(err).Msg("config: could not reload configuration")
@ -648,3 +684,31 @@ func dataDir() string {
}
return filepath.Join(baseDir, "pomerium")
}
func compareByteSliceSlice(a, b [][]byte) int {
sz := min(len(a), len(b))
for i := 0; i < sz; i++ {
switch bytes.Compare(a[i], b[i]) {
case -1:
return -1
case 1:
return 1
}
}
switch {
case len(a) < len(b):
return -1
case len(b) < len(a):
return 1
default:
return 0
}
}
func min(x, y int) int {
if x < y {
return x
}
return y
}