diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go
index ff93b1150..ec3740c41 100644
--- a/caddyconfig/httpcaddyfile/builtins.go
+++ b/caddyconfig/httpcaddyfile/builtins.go
@@ -86,6 +86,14 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
 	var mgr caddytls.ACMEManagerMaker
 	var off bool
 
+	// fill in global defaults, if configured
+	if email := h.Option("email"); email != nil {
+		mgr.Email = email.(string)
+	}
+	if acmeCA := h.Option("acme_ca"); acmeCA != nil {
+		mgr.CA = acmeCA.(string)
+	}
+
 	for h.Next() {
 		// file certificate loader
 		firstLine := h.RemainingArgs()
@@ -112,7 +120,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
 			hasBlock = true
 
 			switch h.Val() {
-
 			// connection policy
 			case "protocols":
 				args := h.RemainingArgs()
@@ -164,7 +171,8 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
 				}
 				mgr.CA = arg[0]
 
-				// TODO: other properties for automation manager
+			default:
+				return nil, h.Errf("unknown subdirective: %s", h.Val())
 			}
 		}
 
diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go
index 2f89f6db7..0d7e0e4ca 100644
--- a/caddyconfig/httpcaddyfile/directives.go
+++ b/caddyconfig/httpcaddyfile/directives.go
@@ -80,11 +80,17 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
 // Caddyfile tokens.
 type Helper struct {
 	*caddyfile.Dispenser
+	options     map[string]interface{}
 	warnings    *[]caddyconfig.Warning
 	matcherDefs map[string]map[string]json.RawMessage
 	parentBlock caddyfile.ServerBlock
 }
 
+// Option gets the option keyed by name.
+func (h Helper) Option(name string) interface{} {
+	return h.options[name]
+}
+
 // Caddyfiles returns the list of config files from
 // which tokens in the current server block were loaded.
 func (h Helper) Caddyfiles() []string {
diff --git a/caddyconfig/httpcaddyfile/handlers.go b/caddyconfig/httpcaddyfile/handlers.go
deleted file mode 100644
index e13302863..000000000
--- a/caddyconfig/httpcaddyfile/handlers.go
+++ /dev/null
@@ -1,56 +0,0 @@
-// 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 httpcaddyfile
-
-import (
-	"encoding/json"
-	"fmt"
-
-	"github.com/caddyserver/caddy/v2"
-	"github.com/caddyserver/caddy/v2/caddyconfig"
-	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
-	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
-)
-
-func (st *ServerType) parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) {
-	matchers := make(map[string]map[string]json.RawMessage)
-	for d.Next() {
-		definitionName := d.Val()
-		for nesting := d.Nesting(); d.NextBlock(nesting); {
-			matcherName := d.Val()
-			mod, err := caddy.GetModule("http.matchers." + matcherName)
-			if err != nil {
-				return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
-			}
-			unm, ok := mod.New().(caddyfile.Unmarshaler)
-			if !ok {
-				return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
-			}
-			err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
-			if err != nil {
-				return nil, err
-			}
-			rm, ok := unm.(caddyhttp.RequestMatcher)
-			if !ok {
-				return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
-			}
-			if _, ok := matchers[definitionName]; !ok {
-				matchers[definitionName] = make(map[string]json.RawMessage)
-			}
-			matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
-		}
-	}
-	return matchers, nil
-}
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index 265cf73f5..794919c6a 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -58,6 +58,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
 			var val interface{}
 			var err error
 			disp := caddyfile.NewDispenser(segment)
+			// TODO: make this switch into a map
 			switch dir {
 			case "http_port":
 				val, err = parseOptHTTPPort(disp)
@@ -69,6 +70,10 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
 				val, err = parseOptExperimentalHTTP3(disp)
 			case "storage":
 				val, err = parseOptStorage(disp)
+			case "acme_ca":
+				val, err = parseOptACMECA(disp)
+			case "email":
+				val, err = parseOptEmail(disp)
 			default:
 				return nil, warnings, fmt.Errorf("unrecognized parameter name: %s", dir)
 			}
@@ -108,7 +113,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
 
 		// extract matcher definitions
 		d := sb.block.DispenseDirective("matcher")
-		matcherDefs, err := st.parseMatcherDefinitions(d)
+		matcherDefs, err := parseMatcherDefinitions(d)
 		if err != nil {
 			return nil, warnings, err
 		}
@@ -122,6 +127,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
 			if dirFunc, ok := registeredDirectives[dir]; ok {
 				results, err := dirFunc(Helper{
 					Dispenser:   caddyfile.NewDispenser(segment),
+					options:     options,
 					warnings:    &warnings,
 					matcherDefs: matcherDefs,
 					parentBlock: sb.block,
@@ -166,7 +172,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
 	// now for the TLS app! (TODO: refactor into own func)
 	tlsApp := caddytls.TLS{Certificates: make(map[string]json.RawMessage)}
 	for _, p := range pairings {
-		for _, sblock := range p.serverBlocks {
+		for i, sblock := range p.serverBlocks {
 			// tls automation policies
 			if mmVals, ok := sblock.pile["tls.automation_manager"]; ok {
 				for _, mmVal := range mmVals {
@@ -175,10 +181,16 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
 					if err != nil {
 						return nil, warnings, err
 					}
-					tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
-						Hosts:         sblockHosts,
-						ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID(), &warnings),
-					})
+					if len(sblockHosts) > 0 {
+						tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
+							Hosts:         sblockHosts,
+							ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID(), &warnings),
+						})
+					} else {
+						warnings = append(warnings, caddyconfig.Warning{
+							Message: fmt.Sprintf("Server block %d %v has no names that qualify for automatic HTTPS, so no TLS automation policy will be added.", i, sblock.block.Keys),
+						})
+					}
 				}
 			}
 
@@ -192,8 +204,25 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
 			}
 		}
 	}
-	// consolidate automation policies that are the exact same
-	tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
+	// if global ACME CA or email were set, append a catch-all automation
+	// policy that ensures they will be used if no tls directive was used
+	acmeCA, hasACMECA := options["acme_ca"]
+	email, hasEmail := options["email"]
+	if hasACMECA || hasEmail {
+		if tlsApp.Automation == nil {
+			tlsApp.Automation = new(caddytls.AutomationConfig)
+		}
+		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
+			ManagementRaw: caddyconfig.JSONModuleObject(caddytls.ACMEManagerMaker{
+				CA:    acmeCA.(string),
+				Email: email.(string),
+			}, "module", "acme", &warnings),
+		})
+	}
+	if tlsApp.Automation != nil {
+		// consolidate automation policies that are the exact same
+		tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
+	}
 
 	// if experimental HTTP/3 is enabled, enable it on each server
 	if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
@@ -207,7 +236,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
 	if !reflect.DeepEqual(httpApp, caddyhttp.App{}) {
 		cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
 	}
-	if !reflect.DeepEqual(tlsApp, caddytls.TLS{}) {
+	if !reflect.DeepEqual(tlsApp, caddytls.TLS{Certificates: make(map[string]json.RawMessage)}) {
 		cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
 	}
 	if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
@@ -415,10 +444,10 @@ func consolidateAutomationPolicies(aps []caddytls.AutomationPolicy) []caddytls.A
 			}
 			if reflect.DeepEqual(aps[i].ManagementRaw, aps[j].ManagementRaw) {
 				aps[i].Hosts = append(aps[i].Hosts, aps[j].Hosts...)
+				aps = append(aps[:j], aps[j+1:]...)
+				i--
+				break
 			}
-			aps = append(aps[:j], aps[j+1:]...)
-			i--
-			break
 		}
 	}
 	return aps
@@ -531,6 +560,37 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([
 	return matcherSetsEnc, nil
 }
 
+func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) {
+	matchers := make(map[string]map[string]json.RawMessage)
+	for d.Next() {
+		definitionName := d.Val()
+		for nesting := d.Nesting(); d.NextBlock(nesting); {
+			matcherName := d.Val()
+			mod, err := caddy.GetModule("http.matchers." + matcherName)
+			if err != nil {
+				return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
+			}
+			unm, ok := mod.New().(caddyfile.Unmarshaler)
+			if !ok {
+				return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
+			}
+			err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
+			if err != nil {
+				return nil, err
+			}
+			rm, ok := unm.(caddyhttp.RequestMatcher)
+			if !ok {
+				return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
+			}
+			if _, ok := matchers[definitionName]; !ok {
+				matchers[definitionName] = make(map[string]json.RawMessage)
+			}
+			matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
+		}
+	}
+	return matchers, nil
+}
+
 func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string]json.RawMessage, error) {
 	msEncoded := make(map[string]json.RawMessage)
 	for matcherName, val := range matchers {
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index dadde288d..a60d060a8 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -108,3 +108,27 @@ func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) {
 	}
 	return storage, nil
 }
+
+func parseOptACMECA(d *caddyfile.Dispenser) (string, error) {
+	d.Next() // consume parameter name
+	if !d.Next() {
+		return "", d.ArgErr()
+	}
+	val := d.Val()
+	if d.Next() {
+		return "", d.ArgErr()
+	}
+	return val, nil
+}
+
+func parseOptEmail(d *caddyfile.Dispenser) (string, error) {
+	d.Next() // consume parameter name
+	if !d.Next() {
+		return "", d.ArgErr()
+	}
+	val := d.Val()
+	if d.Next() {
+		return "", d.ArgErr()
+	}
+	return val, nil
+}