caddyserver/caddy · error

automation policy from site block is also default/catch-all

Error message

automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v

What it means

When a site block's address has no hostname (e.g. ':443'), its automation policy doubles as the catch-all policy for all sites. If another such site block already established a different issuer list (ap.Issuers vs issuers), the two catch-all policies conflict and adaptation aborts rather than silently picking one. The source comment shows the canonical repro: two ':443' blocks, one with 'tls { issuer acme }' and one without.

Source

Thrown at caddyconfig/httpcaddyfile/tlsapp.go:172

				var issuers []certmagic.Issuer
				for _, issuerVal := range issuerVals {
					issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
				}
				if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
					// this more correctly implements an error check that was removed
					// below; try it with this config:
					//
					// :443 {
					// 	bind 127.0.0.1
					// }
					//
					// :443 {
					// 	bind ::1
					// 	tls {
					// 		issuer acme
					// 	}
					// }
					return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
				}
				ap.Issuers = issuers
			}

			// certificate managers
			if certManagerVals, ok := sblock.pile["tls.cert_manager"]; ok {
				for _, certManager := range certManagerVals {
					certGetterName := certManager.Value.(caddy.Module).CaddyModule().ID.Name()
					ap.ManagersRaw = append(ap.ManagersRaw, caddyconfig.JSONModuleObject(certManager.Value, "via", certGetterName, &warnings))
				}
			}
			// custom bind host
			for _, cfgVal := range sblock.pile["bind"] {
				for _, iss := range ap.Issuers {
					// if an issuer was already configured and it is NOT an ACME issuer,
					// skip, since we intend to adjust only ACME issuers; ensure we
					// include any issuer that embeds/wraps an underlying ACME issuer
					var acmeIssuer *caddytls.ACMEIssuer

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Give at least one block a hostname so its policy is no longer catch-all (e.g. 'example.com:443 { ... }'), as the source comment implies.
  2. Make the issuer configuration identical across the conflicting catch-all blocks, or move issuer config to one place only.
  3. Use distinct ports for the two catch-all sites so they do not merge into one policy scope.

Example fix

# before
:443 {
  bind 127.0.0.1
}
:443 {
  bind ::1
  tls {
    issuer acme
  }
}

# after
localhost:443 {
  bind 127.0.0.1
}
[::1]:443 {
  bind ::1
  tls {
    issuer acme
  }
}
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Two site blocks keyed by hostname-less addresses (':443', 'https://:443') where global issuers were captured first and the second block's site-level tls issuers differ — e.g. one block sets 'tls { issuer acme }' (or internal) and the other leaves defaults.

Common situations: Splitting a config into ':443' blocks bound to different interfaces with distinct TLS settings; adding a new catch-all site with different issuer config next to an existing one.

Related errors


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