caddyserver/caddy · error

automation policy %d: cannot apply more than one automation

Error message

automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)

What it means

Policy matching is first-match-wins, so a host may only appear as a subject in one automation policy; a second policy listing the same host is silently ineffective. Validation builds a host->policy-index map and rejects duplicates, naming both the offending policy index and the earlier policy that already claimed the host.

Source

Thrown at modules/caddytls/tls.go:386

// Validate validates t's configuration.
func (t *TLS) Validate() error {
	if t.Automation != nil {
		// ensure that host aren't repeated; since only the first
		// automation policy is used, repeating a host in the lists
		// isn't useful and is probably a mistake; same for two
		// catch-all/default policies
		var hasDefault bool
		hostSet := make(map[string]int)
		for i, ap := range t.Automation.Policies {
			if len(ap.subjects) == 0 {
				if hasDefault {
					return fmt.Errorf("automation policy %d is the second policy that acts as default/catch-all, but will never be used", i)
				}
				hasDefault = true
			}
			for _, h := range ap.subjects {
				if first, ok := hostSet[h]; ok {
					return fmt.Errorf("automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)", i, h, first)
				}
				hostSet[h] = i
			}
		}
	}
	if t.Cache != nil {
		if t.Cache.Capacity < 0 {
			return fmt.Errorf("cache capacity must be >= 0")
		}
	}
	return nil
}

// Start activates the TLS module.
func (t *TLS) Start() error {
	// warn if on-demand TLS is enabled but no restrictions are in place
	if t.Automation.OnDemand == nil || (t.Automation.OnDemand.Ask == "" && t.Automation.OnDemand.permission == nil) {
		for _, ap := range t.Automation.Policies {

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Keep each subject in exactly one policy; consolidate the settings for the duplicate host into the first policy that should own it
  2. If different issuers are intended per-subdomain, split subjects precisely (e.g. move only www.example.com to the second policy)
  3. Use 'caddy adapt' + inspect the JSON to see the final policy subjects after snippet imports

Example fix

// before
"policies": [
  {"subjects": ["example.com"], "issuers": [{"module": "acme"}]},
  {"subjects": ["example.com", "api.example.com"], "issuers": [{"module": "internal"}]}
]
// after
"policies": [
  {"subjects": ["example.com"], "issuers": [{"module": "acme"}]},
  {"subjects": ["api.example.com"], "issuers": [{"module": "internal"}]}
]
Defensive patterns

Strategy: validation

Validate before calling

seen := map[string]int{}
for i, p := range tlsApp.Automation.Policies {
	for _, h := range p.SubjectsRaw {
		if first, dup := seen[h]; dup {
			return fmt.Errorf("host %s in policies %d and %d", h, first, i)
		}
		seen[h] = i
	}
}

Prevention

When it happens

Trigger: automation.policies = [{subjects: ["example.com"], ...}, {subjects: ["example.com", "www.example.com"], ...}] — example.com is claimed by policy 0, so policy 1 triggers the error.

Common situations: Splitting config snippets that each manage overlapping domains; merging site configs into policies without deduplicating; wildcard policies that also explicitly list a covered name in another policy.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/506015bc07428e3e. Report an issue: GitHub.