caddyserver/caddy · error

on-demand TLS config conflict: both 'ask' endpoint and a 'pe

Error message

on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module

What it means

On-demand TLS supports two permission mechanisms: the deprecated 'ask' endpoint URL and the newer 'permission' module. When tls.automation.on_demand sets both 'ask' and a 'permission' module, Caddy refuses to guess which applies and aborts provisioning. 'ask' is deprecated in favor of the http permission module, which supersedes it.

Source

Thrown at modules/caddytls/tls.go:280

	certCacheMu.RUnlock()
	for _, loader := range t.certificateLoaders {
		certs, err := loader.LoadCertificates()
		if err != nil {
			return fmt.Errorf("loading certificates: %v", err)
		}
		for _, cert := range certs {
			hash, err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags)
			if err != nil {
				return fmt.Errorf("caching unmanaged certificate: %v", err)
			}
			t.loaded[hash] = ""
		}
	}

	// on-demand permission module
	if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.PermissionRaw != nil {
		if t.Automation.OnDemand.Ask != "" {
			return fmt.Errorf("on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module")
		}
		val, err := ctx.LoadModule(t.Automation.OnDemand, "PermissionRaw")
		if err != nil {
			return fmt.Errorf("loading on-demand TLS permission module: %v", err)
		}
		t.Automation.OnDemand.permission = val.(OnDemandPermission)
	}

	// automation/management policies
	if t.Automation == nil {
		t.Automation = new(AutomationConfig)
	}
	t.Automation.defaultPublicAutomationPolicy = new(AutomationPolicy)
	err = t.Automation.defaultPublicAutomationPolicy.Provision(t)
	if err != nil {
		return fmt.Errorf("provisioning default public automation policy: %v", err)
	}
	for n := range t.automateNames {

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Delete the 'ask' entry and keep only the permission module (e.g. permission module 'http' with endpoint set to the old ask URL)
  2. If you must keep 'ask' temporarily, remove the 'permission' block entirely
  3. Run 'caddy validate' after editing to confirm the conflict is gone

Example fix

// before
"on_demand": {"ask": "http://localhost:5555/check", "permission": {"module": "http", "endpoint": "http://localhost:5555/check"}}
// after
"on_demand": {"permission": {"module": "http", "endpoint": "http://localhost:5555/check"}}
Defensive patterns

Strategy: validation

Validate before calling

jq '.apps.tls.automation.on_demand // {} | select(.ask != null and .permission != null)' Caddyfile.json
# non-empty output means the conflict will abort provisioning

Prevention

When it happens

Trigger: JSON config containing {"on_demand": {"ask": "http://...", "permission": {...}}} — both keys non-empty under tls.automation.on_demand. In Caddyfile terms, combining the legacy 'ask' global option with an 'on_demand_tls permission' block.

Common situations: Configs migrated from older Caddy versions that already had 'ask' and then added a permission module per new docs; copy-pasting examples from different documentation eras into one config.

Understand the failure class

Related errors


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