diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go
index 1efe5accc..8137ce2ae 100644
--- a/caddyconfig/httpcaddyfile/builtins.go
+++ b/caddyconfig/httpcaddyfile/builtins.go
@@ -211,12 +211,13 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
 				provName := h.Val()
 				if acmeIssuer.Challenges == nil {
 					acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
+					acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
 				}
 				dnsProvModule, err := caddy.GetModule("tls.dns." + provName)
 				if err != nil {
 					return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
 				}
-				acmeIssuer.Challenges.DNSRaw = caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, h.warnings)
+				acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, h.warnings)
 
 			case "ca_root":
 				arg := h.RemainingArgs()
diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go
index f66fcae71..88c126071 100644
--- a/caddyconfig/httpcaddyfile/tlsapp.go
+++ b/caddyconfig/httpcaddyfile/tlsapp.go
@@ -388,7 +388,9 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
 				return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
 			}
 			mgr.Challenges = &caddytls.ChallengesConfig{
-				DNSRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, &warnings),
+				DNS: &caddytls.DNSChallengeConfig{
+					ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, &warnings),
+				},
 			}
 		}
 		if acmeCARoot != nil {
diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go
index 854e6d507..7296d28f8 100644
--- a/modules/caddytls/acmeissuer.go
+++ b/modules/caddytls/acmeissuer.go
@@ -86,8 +86,8 @@ func (ACMEIssuer) CaddyModule() caddy.ModuleInfo {
 // Provision sets up m.
 func (m *ACMEIssuer) Provision(ctx caddy.Context) error {
 	// DNS providers
-	if m.Challenges != nil && m.Challenges.DNSRaw != nil {
-		val, err := ctx.LoadModule(m.Challenges, "DNSRaw")
+	if m.Challenges != nil && m.Challenges.DNS != nil && m.Challenges.DNS.ProviderRaw != nil {
+		val, err := ctx.LoadModule(m.Challenges.DNS, "ProviderRaw")
 		if err != nil {
 			return fmt.Errorf("loading DNS provider module: %v", err)
 		}
@@ -95,7 +95,7 @@ func (m *ACMEIssuer) Provision(ctx caddy.Context) error {
 		if err != nil {
 			return fmt.Errorf("making DNS provider: %v", err)
 		}
-		m.Challenges.DNS = prov
+		m.Challenges.DNS.provider = prov
 	}
 
 	// add any custom CAs to trust store
@@ -152,7 +152,9 @@ func (m *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEManager, error) {
 			template.DisableTLSALPNChallenge = m.Challenges.TLSALPN.Disabled
 			template.AltTLSALPNPort = m.Challenges.TLSALPN.AlternatePort
 		}
-		template.DNSProvider = m.Challenges.DNS
+		if m.Challenges.DNS != nil {
+			template.DNSProvider = m.Challenges.DNS.provider
+		}
 		template.ListenHost = m.Challenges.BindHost
 	}
 
diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go
index df76fd921..5d96c4f88 100644
--- a/modules/caddytls/automation.go
+++ b/modules/caddytls/automation.go
@@ -35,8 +35,15 @@ type AutomationConfig struct {
 
 	// On-Demand TLS defers certificate operations to the
 	// moment they are needed, e.g. during a TLS handshake.
-	// Useful when you don't know all the hostnames up front.
-	// Caddy was the first web server to deploy this technology.
+	// Useful when you don't know all the hostnames at
+	// config-time, or when you are not in control of the
+	// domain names you are managing certificates for.
+	// In 2015, Caddy became the first web server to
+	// implement this experimental technology.
+	//
+	// Note that this field does not enable on-demand TLS,
+	// it only configures it for when it is used. To enable
+	// it, create an automation policy with `on_demand`.
 	OnDemand *OnDemandConfig `json:"on_demand,omitempty"`
 
 	// Caddy staples OCSP (and caches the response) for all
@@ -239,13 +246,14 @@ type ChallengesConfig struct {
 	// not enabled by default. This is the only challenge
 	// type which does not require a direct connection
 	// to Caddy from an external server.
-	DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=tls.dns inline_key=provider"`
+	// NOTE: DNS providers are currently being upgraded,
+	// and this API is subject to change, but should be
+	// stabilized soon.
+	DNS *DNSChallengeConfig `json:"dns,omitempty"`
 
 	// Optionally customize the host to which a listener
 	// is bound if required for solving a challenge.
 	BindHost string `json:"bind_host,omitempty"`
-
-	DNS challenge.Provider `json:"-"`
 }
 
 // HTTPChallengeConfig configures the ACME HTTP challenge.
@@ -274,12 +282,25 @@ type TLSALPNChallengeConfig struct {
 	AlternatePort int `json:"alternate_port,omitempty"`
 }
 
+// DNSChallengeConfig configures the ACME DNS challenge.
+// NOTE: This API is still experimental and is subject to change.
+type DNSChallengeConfig struct {
+	// The DNS provider module to use which will manage
+	// the DNS records relevant to the ACME challenge.
+	ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=tls.dns inline_key=name"`
+
+	// The TTL of the TXT record used for the DNS challenge.
+	TTL caddy.Duration `json:"ttl,omitempty"`
+
+	provider challenge.Provider
+}
+
 // OnDemandConfig configures on-demand TLS, for obtaining
 // needed certificates at handshake-time. Because this
-// feature can easily be abused, you should set up rate
-// limits and/or an internal endpoint that Caddy can
-// "ask" if it should be allowed to manage certificates
-// for a given hostname.
+// feature can easily be abused, you should use this to
+// establish rate limits and/or an internal endpoint that
+// Caddy can "ask" if it should be allowed to manage
+// certificates for a given hostname.
 type OnDemandConfig struct {
 	// An optional rate limit to throttle the
 	// issuance of certificates from handshakes.