{"record":{"id":"4b4279633cbe800c","repo":"caddyserver/caddy","slug":"on-demand-tls-cannot-be-enabled-without-a-permissi","errorCode":null,"errorMessage":"on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details","messagePattern":"on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"modules/caddytls/automation.go","lineNumber":307,"sourceCode":"\tif storage == nil {\n\t\tstorage = tlsApp.ctx.Storage()\n\t}\n\n\t// on-demand TLS\n\tvar ond *certmagic.OnDemandConfig\n\tif ap.OnDemand || len(ap.Managers) > 0 {\n\t\t// permission module is now required after a number of negligence cases that allowed abuse;\n\t\t// but it may still be optional for explicit subjects (bounded, non-wildcard), for the\n\t\t// internal issuer since it doesn't cause public PKI pressure on ACME servers; subtly, it\n\t\t// is useful to allow on-demand TLS to be enabled so Managers can be used, but to still\n\t\t// prevent issuance from Issuers (when Managers don't provide a certificate) if there's no\n\t\t// permission module configured\n\t\tnoProtections := ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil)\n\t\tfailClosed := noProtections && !ap.hadExplicitManagers // don't allow on-demand issuance (other than implicit managers) if no managers have been explicitly configured\n\t\tif noProtections {\n\t\t\tif !ap.hadExplicitManagers {\n\t\t\t\t// no managers, no explicitly-configured permission module, this is a config error\n\t\t\t\treturn certmagic.Config{}, fmt.Errorf(\"on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details\")\n\t\t\t}\n\t\t\t// allow on-demand to be enabled but only for the purpose of the Managers; issuance won't be allowed from Issuers\n\t\t\ttlsApp.logger.Warn(\"on-demand TLS can only get certificates from the configured external manager(s) because no ask endpoint / permission module is specified\")\n\t\t}\n\t\tond = &certmagic.OnDemandConfig{\n\t\t\tDecisionFunc: func(ctx context.Context, name string) error {\n\t\t\t\tif failClosed {\n\t\t\t\t\treturn fmt.Errorf(\"no permission module configured; certificates not allowed except from external Managers\")\n\t\t\t\t}\n\t\t\t\tif tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\t// logging the remote IP can be useful for servers that want to count\n\t\t\t\t// attempts from clients to detect patterns of abuse -- it should NOT be\n\t\t\t\t// used solely for decision making, however\n\t\t\t\tvar remoteIP string\n\t\t\t\tif hello, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo); ok && hello != nil {","sourceCodeStart":289,"sourceCodeEnd":325,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/modules/caddytls/automation.go#L289-L325","documentation":"On-demand TLS was enabled for an automation policy that covers wildcard/default subjects without any permission module (ask endpoint / http directive in on_demand_tls, or a permission module), no explicitly-configured managers, and not solely the internal issuer. Because unbounded on-demand issuance has been abused to hammer ACME CAs, Caddy fails closed at config provisioning time and requires an explicit abuse-prevention mechanism.","triggerScenarios":"Configuring on_demand_tls without permission (e.g. missing ask URL), or tls { on_demand } on a catch-all/wildcard site without an ask endpoint and without get_certificate managers. The check noProtections = isWildcardOrDefault() && !onlyInternalIssuer() && no permission module, combined with no explicit managers, triggers the error.","commonSituations":"Upgrading from older Caddy versions where on_demand without ask was allowed; enabling on-demand TLS for dynamic tenant subdomains and forgetting the ask endpoint; using the internal issuer alongside a wildcard but with a non-internal issuer also present.","solutions":["Add a permission module: in the Caddyfile global options set on_demand_tls { ask <endpoint> } pointing to a 2xx-returning endpoint that approves names, or configure a permission module in JSON under apps.tls.automation.on_demand.permission.","Or add an explicit certificate manager (get_certificate) so issuance is bounded by the manager, if managers fit the use case.","Or restrict the policy to explicit, non-wildcard subjects and/or use only the internal issuer, which are exempt.","As a last resort, disable on_demand and list subjects explicitly."],"exampleFix":"# before\n{\n\ton_demand_tls {\n\t\t# no ask / permission\n\t}\n}\nexample.com {\n\ttls {\n\t\ton_demand\n\t}\n}\n\n# after\n{\n\ton_demand_tls {\n\t\task http://localhost:5555/check\n\t}\n}\nexample.com {\n\ttls {\n\t\ton_demand\n\t}\n}","handlingStrategy":"validation","validationCode":"// Config-time check mirroring Caddy's rule.\nneedsPermission := policy.isWildcardOrDefault && !onlyInternalIssuer(policy)\nhasPermission := tlsApp.Automation != nil && tlsApp.Automation.OnDemand != nil && tlsApp.Automation.OnDemand.permission != nil\nif (policy.OnDemand || len(policy.Managers) > 0) && needsPermission && !hasPermission && !hadExplicitManagers {\n    return errors.New(\"on-demand requires an ask endpoint / permission module\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pair on_demand with on_demand_tls { ask ... } in configs.","Have the ask endpoint return non-2xx for unknown names to bound issuance.","Run `caddy validate` on configs after every Caddy upgrade; on-demand rules tightened over time."],"tags":["tls","on-demand","abuse-prevention","configuration","security"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}