Skip to content

Commit

Permalink
tls: Restructure and improve certificate management
Browse files Browse the repository at this point in the history
- Expose the list of Caddy instances through caddy.Instances()

- Added arbitrary storage to caddy.Instance

- The cache of loaded certificates is no longer global; now scoped
  per-instance, meaning upon reload (like SIGUSR1) the old cert cache
  will be discarded entirely, whereas before, aggressively reloading
  config that added and removed lots of sites would cause unnecessary
  build-up in the cache over time.

- Key certificates in the cache by their SHA-256 hash instead of
  by their names. This means certificates will not be duplicated in
  memory (within each instance), making Caddy much more memory-efficient
  for large-scale deployments with thousands of sites sharing certs.

- Perform name-to-certificate lookups scoped per caddytls.Config instead
  of a single global lookup. This prevents certificates from stepping on
  each other when they overlap in their names.

- Do not allow TLS configurations keyed by the same hostname to be
  different; this now throws an error.

- Updated relevant tests, with a stark awareness that more tests are
  needed.

- Change the NewContext function signature to include an *Instance.

- Strongly recommend (basically require) use of caddytls.NewConfig()
  to create a new *caddytls.Config, to ensure pointers to the instance
  certificate cache are initialized properly.

- Update the TLS-SNI challenge solver (even though TLS-SNI is disabled
  currently on the CA side). Store temporary challenge cert in instance
  cache, but do so directly by the ACME challenge name, not the hash.
  Modified the getCertificate function to check the cache directly for
  a name match if one isn't found otherwise. This will allow any
  caddytls.Config to be able to help solve a TLS-SNI challenge, with one
  extra side-effect that might actually be kind of interesting (and
  useless): clients could send a certificate's hash as the SNI and
  Caddy would be able to serve that certificate for the handshake.

- Do not attempt to match a "default" (random) certificate when SNI
  is present but unrecognized; return no certificate so a TLS alert
  happens instead.

- Store an Instance in the list of instances even while the instance
  is still starting up (this allows access to the cert cache for
  performing renewals at startup, etc). Will be removed from list again
  if instance startup fails.

- Laid groundwork for ACMEv2 and Let's Encrypt wildcard support.

Server type plugins will need to be updated slightly to accommodate
minor adjustments to their API (like passing in an Instance). This
commit includes the changes for the HTTP server.

Certain Caddyfile configurations might error out with this change, if
they configured different TLS settings for the same hostname.

This change trades some complexity for other complexity, but ultimately
this new complexity is more correct and robust than earlier logic.

Fixes #1991
Fixes #1994
Fixes #1303
  • Loading branch information
mholt committed Feb 4, 2018
1 parent 9619fe2 commit fc2ff91
Show file tree
Hide file tree
Showing 17 changed files with 794 additions and 422 deletions.
56 changes: 45 additions & 11 deletions caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ var (

// Instance contains the state of servers created as a result of
// calling Start and can be used to access or control those servers.
// It is literally an instance of a server type. Instance values
// should NOT be copied. Use *Instance for safety.
type Instance struct {
// serverType is the name of the instance's server type
serverType string
Expand All @@ -89,10 +91,11 @@ type Instance struct {
// wg is used to wait for all servers to shut down
wg *sync.WaitGroup

// context is the context created for this instance.
// context is the context created for this instance,
// used to coordinate the setting up of the server type
context Context

// servers is the list of servers with their listeners.
// servers is the list of servers with their listeners
servers []ServerListener

// these callbacks execute when certain events occur
Expand All @@ -101,6 +104,18 @@ type Instance struct {
onRestart []func() error // before restart commences
onShutdown []func() error // stopping, even as part of a restart
onFinalShutdown []func() error // stopping, not as part of a restart

// storing values on an instance is preferable to
// global state because these will get garbage-
// collected after in-process reloads when the
// old instances are destroyed; use StorageMu
// to access this value safely
Storage map[interface{}]interface{}
StorageMu sync.RWMutex
}

func Instances() []*Instance {
return instances
}

// Servers returns the ServerListeners in i.
Expand Down Expand Up @@ -196,7 +211,7 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
}

// create new instance; if the restart fails, it is simply discarded
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg}
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg, Storage: make(map[interface{}]interface{})}

// attempt to start new instance
err := startWithListenerFds(newCaddyfile, newInst, restartFds)
Expand Down Expand Up @@ -455,7 +470,7 @@ func (i *Instance) Caddyfile() Input {
//
// This function blocks until all the servers are listening.
func Start(cdyfile Input) (*Instance, error) {
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup)}
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
err := startWithListenerFds(cdyfile, inst, nil)
if err != nil {
return inst, err
Expand All @@ -468,11 +483,34 @@ func Start(cdyfile Input) (*Instance, error) {
}

func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error {
// save this instance in the list now so that
// plugins can access it if need be, for example
// the caddytls package, so it can perform cert
// renewals while starting up; we just have to
// remove the instance from the list later if
// it fails
instancesMu.Lock()
instances = append(instances, inst)
instancesMu.Unlock()
var err error
defer func() {
if err != nil {
instancesMu.Lock()
for i, otherInst := range instances {
if otherInst == inst {
instances = append(instances[:i], instances[i+1:]...)
break
}
}
instancesMu.Unlock()
}
}()

if cdyfile == nil {
cdyfile = CaddyfileInput{}
}

err := ValidateAndExecuteDirectives(cdyfile, inst, false)
err = ValidateAndExecuteDirectives(cdyfile, inst, false)
if err != nil {
return err
}
Expand Down Expand Up @@ -504,10 +542,6 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
return err
}

instancesMu.Lock()
instances = append(instances, inst)
instancesMu.Unlock()

// run any AfterStartup callbacks if this is not
// part of a restart; then show file descriptor notice
if restartFds == nil {
Expand Down Expand Up @@ -546,7 +580,7 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bool) error {
// If parsing only inst will be nil, create an instance for this function call only.
if justValidate {
inst = &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup)}
inst = &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
}

stypeName := cdyfile.ServerType()
Expand All @@ -563,7 +597,7 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
return err
}

inst.context = stype.NewContext()
inst.context = stype.NewContext(inst)
if inst.context == nil {
return fmt.Errorf("server type %s produced a nil Context", stypeName)
}
Expand Down
7 changes: 5 additions & 2 deletions caddyhttp/httpserver/https.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func activateHTTPS(cctx caddy.Context) error {
operatorPresent := !caddy.Started()

if !caddy.Quiet && operatorPresent {
fmt.Print("Activating privacy features...")
fmt.Print("Activating privacy features... ")
}

ctx := cctx.(*httpContext)
Expand Down Expand Up @@ -69,7 +69,7 @@ func activateHTTPS(cctx caddy.Context) error {
}

if !caddy.Quiet && operatorPresent {
fmt.Println(" done.")
fmt.Println("done.")
}

return nil
Expand Down Expand Up @@ -163,6 +163,7 @@ func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
if redirPort == DefaultHTTPSPort {
redirPort = "" // default port is redundant
}

redirMiddleware := func(next Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
// Construct the URL to which to redirect. Note that the Host in a request might
Expand All @@ -184,9 +185,11 @@ func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
return 0, nil
})
}

host := cfg.Addr.Host
port := HTTPPort
addr := net.JoinHostPort(host, port)

return &SiteConfig{
Addr: Address{Original: addr, Host: host, Port: port},
ListenHost: cfg.ListenHost,
Expand Down
24 changes: 15 additions & 9 deletions caddyhttp/httpserver/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,13 @@ func hideCaddyfile(cctx caddy.Context) error {
return nil
}

func newContext() caddy.Context {
return &httpContext{keysToSiteConfigs: make(map[string]*SiteConfig)}
func newContext(inst *caddy.Instance) caddy.Context {
return &httpContext{instance: inst, keysToSiteConfigs: make(map[string]*SiteConfig)}
}

type httpContext struct {
instance *caddy.Instance

// keysToSiteConfigs maps an address at the top of a
// server block (a "key") to its SiteConfig. Not all
// SiteConfigs will be represented here, only ones
Expand Down Expand Up @@ -146,15 +148,19 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
altTLSSNIPort = HTTPSPort
}

// Make our caddytls.Config, which has a pointer to the
// instance's certificate cache and enough information
// to use automatic HTTPS when the time comes
caddytlsConfig := caddytls.NewConfig(h.instance)
caddytlsConfig.Hostname = addr.Host
caddytlsConfig.AltHTTPPort = altHTTPPort
caddytlsConfig.AltTLSSNIPort = altTLSSNIPort

// Save the config to our master list, and key it for lookups
cfg := &SiteConfig{
Addr: addr,
Root: Root,
TLS: &caddytls.Config{
Hostname: addr.Host,
AltHTTPPort: altHTTPPort,
AltTLSSNIPort: altTLSSNIPort,
},
Addr: addr,
Root: Root,
TLS: caddytlsConfig,
originCaddyfile: sourceFile,
IndexPages: staticfiles.DefaultIndexPages,
}
Expand Down
8 changes: 4 additions & 4 deletions caddyhttp/httpserver/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestAddressString(t *testing.T) {
func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
Port = "9999"
filename := "Testfile"
ctx := newContext().(*httpContext)
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
input := strings.NewReader(`localhost`)
sblocks, err := caddyfile.Parse(filename, input, nil)
if err != nil {
Expand All @@ -155,7 +155,7 @@ func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {

func TestInspectServerBlocksCaseInsensitiveKey(t *testing.T) {
filename := "Testfile"
ctx := newContext().(*httpContext)
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
input := strings.NewReader("localhost {\n}\nLOCALHOST {\n}")
sblocks, err := caddyfile.Parse(filename, input, nil)
if err != nil {
Expand Down Expand Up @@ -207,7 +207,7 @@ func TestDirectivesList(t *testing.T) {
}

func TestContextSaveConfig(t *testing.T) {
ctx := newContext().(*httpContext)
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
ctx.saveConfig("foo", new(SiteConfig))
if _, ok := ctx.keysToSiteConfigs["foo"]; !ok {
t.Error("Expected config to be saved, but it wasn't")
Expand All @@ -226,7 +226,7 @@ func TestContextSaveConfig(t *testing.T) {

// Test to make sure we are correctly hiding the Caddyfile
func TestHideCaddyfile(t *testing.T) {
ctx := newContext().(*httpContext)
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
ctx.saveConfig("test", &SiteConfig{
Root: Root,
originCaddyfile: "Testfile",
Expand Down
Loading

0 comments on commit fc2ff91

Please sign in to comment.