Repository: caddyserver/caddy (2026-03-13T00:23:10Z)

Issue:
Regression for wildcard `tls get_certificate http` in v2.11
### Issue Details

As discussed on Discord: https://discord.com/channels/569842713239879680/569842713239879682/1480691506837717144

A setup like this no longer works after upgrading to v2.11, stating that no matching certs could be found. Replacing `get_certificate http` with a more conventional `tls crt.pem key.pem` works.

This is on latest version v2.11.2, but I am unfortunately not sure what I upgraded from. Most likely v2.9.

```Caddyfile
*.mydomain.app {
        tls {
                get_certificate http http://localhost:9000/certs
        }
        abort
}

wiki.mydomain.app {
        reverse_proxy localhost:3000
}
```

```jsonl
{"level":"debug","ts":1773094478.2185016,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"wiki.mydomain.app"}
{"level":"debug","ts":1773094478.2185636,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"*.mydomain.app"}
{"level":"debug","ts":1773094478.2185688,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"*.*.app"}
{"level":"debug","ts":1773094478.2185729,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"*.*.*"}
```


### Assistance Disclosure

_No response_

### If AI was used, describe the extent to which it was used.

_No response_


Discussion:
Hm, Caddy should be provisioning a certificate for `wiki.mydomain.app`. What are the full logs?

It's not immediately clear to me what is going on, so I'll need your help to understand it better.

Ideally, we need to be able to reproduce the bug _in the most minimal way possible_ **using the latest version of Caddy.** This allows us to write regression tests to verify the fix is working. If we can't reproduce it, then you'll have to test our changes for us until it's fixed -- and then we can't add test cases, either.

I've attached a template below that will help make this easier and faster! **This will require some effort on your part -- please understand that we will be dedicating time to fix the bug you are reporting if you can just help us understand it and reproduce it easily.**

This template will ask for some information you've already provided; that's OK, just fill it out the best you can. :+1: I've also included some helpful tips below the template. Feel free to let me know if you have any questions!

Thank you again for your report, we look forward to resolving it!

## Template

<pre><code>## 1. Environment

### 1a. Operating system and version

```
paste here
```


### 1b. Caddy version (run `caddy version` or paste commit SHA)

This should be the latest version of Caddy:

```
paste here
```


## 2. Description

### 2a. What happens (briefly explain what is wrong)




### 2b. Why it's a bug (if it's not obvious)




### 2c. Log output

```
paste terminal output or logs here
```



### 2d. Workaround(s)




### 2e. Relevant links




## 3. Tutorial (minimal steps to reproduce the bug)




</code></pre>

## Instructions -- please heed otherwise we cannot help you (help us help you!)

1. **Environment**: Please fill out your OS and Caddy versions, even if you don't think they are relevant. (They are _always_ relevant.) If you built Caddy from source, provide the commit SHA and specify your exact Go version.

2. **Description:** Describe at a high level what the bug is. What happens? Why is it a bug? Not all bugs are obvious, so convince readers that it's actually a bug.
	- **2c) Log output:** Paste terminal output and/or complete logs in a code block. [DO NOT REDACT INFORMATION except for credentials.](https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222) Please enable debug and access logs.
	- **2d) Workaround:** What are you doing to work around the problem in the meantime? This can help others who encounter the same problem, until we implement a fix.
	- **2e) Relevant links:** Please link to any related issues, pull requests, docs, and/or discussion. This can add crucial context to your report.

3. **Tutorial:** What are the _minimum required specific steps_ someone needs to take in order to experience the same bug? Your goal here is to make sure that anyone else can have the same experience with the bug as you do. You are writing a tutorial, so make sure to carry it out yourself before posting it. Please:
	- Start with an empty config. Add _only_ the lines/parameters that are _absolutely required_ to reproduce the bug.
	- Do not run Caddy inside containers.
	- Run Caddy manually in your terminal; do not use systemd or other init systems.
	- If making HTTP requests, avoid web browsers. Use a simpler HTTP client instead, like `curl`.
	- Do not redact any information from your config (except credentials). Domain names are public knowledge and [often necessary for quick resolution of an issue](https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222)!
	- Note that ignoring this advice may result in delays, or even in your issue being closed. 😞 Only actionable issues are kept open, and if there is not enough information or clarity to reproduce the bug, then the report is not actionable.

Example of a tutorial:

<blockquote>Create a config file:

```
{ ... }
```

Open terminal and run Caddy:

```
$ caddy ...
```

Make an HTTP request:

```
$ curl ...
```

Notice that the result is ___ but it should be ___.</blockquote>

@mholt I made a repro (with the help of Opus 4.6) here: https://github.com/filiabel/caddy-repro-7559

I found this to be the easiest way to replicate the behaviour I observed, let me know if it looks OK! Logs and test script output are in repo, so you shouldn't have to run it yourself.
Thanks, this does appear to be a regression. Would love help with a fix! Maybe the smart LLM can make a smart PR? :)
I gave it a try with AI, and the fix seems reasonable to me at least. Feel free to close, modify or whatever else 😀
Thanks -- worth a shot. I think it's a bit too narrow-scoped, or too specific.

My patch is in #7567 which I think is a better fit, if it works for you.
Thanks, Matt! I have no opinion on how to fix this really, so I am sure your manually crafted patch will be more in line with Caddy overall. 

I did my runtime tests on https://github.com/caddyserver/caddy/pull/7567, and issue seems to remain?

```
➜  caddy git:(claude/fix-issue-7559) ✗ ./test.sh
=== Building caddy from local repo ===
=== Starting cert-server ===
=== Running local caddy with /Users/fga/Developer/personal/caddy/Caddyfile.2.11 ===
  wiki.example.com  → 200
  other.example.com → 200
  cert-server was called for:
    wiki.example.com
    other.example.com
  caddy tls handshake log:
    wiki.example.com → using externally-managed certificate
    other.example.com → using externally-managed certificate

➜  caddy git:(fix-ap-consolidation) ✗ ./test.sh
=== Building caddy from local repo ===
=== Starting cert-server ===
=== Running local caddy with /Users/fga/Developer/personal/caddy/Caddyfile.2.11 ===
  wiki.example.com  → 000
  other.example.com → 200
  cert-server was called for:
    other.example.com
  caddy tls handshake log:
    no certificate available for wiki.example.com
    other.example.com → using externally-managed certificate
```
Are you _sure_? I ran your config and added a test and they both confirmed it fixed it for me (I was able to repro without the fix, problem goes away with the fix).

Are you sure you're running the right binary?
Oh you know, I think it's my bad -- I only tested your AI's patch with the end-to-end HTTPS request, whereas my fix is strictly a Caddyfile adapter (Caddyfile -> JSON) fix. So the JSON now looks correct, but the behavior is indeed still the same... very odd, I am now investigating the auto-HTTPS logic.
Ohhh okay, so I think this is working as expected (with my patch at least), since I remember now the new behavior (as of a few versions ago) is for wildcards to cover specific subdomains. (For better privacy.) So like it expects a cert for `*.example.com` to be used for `foo.example.com`.

Since `*.example.com` has a certificate from a certificate manager, it uses the certificate manager for `wiki.example.com`.

What does your certificate manager return for `wiki.example.com`?
I have configured my manager to always return the wildcard certificate for a domain. So in this case it would return `*.example.com`.

So I did indeed expect wiki to be covered in wildcard, just like when `prefer_wilcard` was configurable.
So what we really need to decide is, should every site block that is expected to use the manager have the `tls` directive block in it? It's a bit weird translating this from Caddyfile to JSON (native) config.

Option 1:

```
*.test.com {
	tls {
		get_certificate http http://cert-server:9000/certs
	}
	respond "wildcard"
}

wiki.test.com {
	tls {
		get_certificate http http://cert-server:9000/certs
	}
	respond "wiki"
}
```

This is clear, explicit, obvious. Both use the "manager" to get the cert.

Option 2:

```
*.test.com {
	tls {
		get_certificate http http://cert-server:9000/certs
	}
	respond "wildcard"
}

wiki.test.com {
	respond "wiki"
}
```

Admittedly, this looks like Caddy should manage the cert for `wiki.test.com`. But according to the new wildcard rules, the manager defined for the wildcard domain, `*.test.com`, should cover that. This is not obvious because Caddyfile conflates TLS and HTTP into a single site block structure in the config. That's by design for simplicity in some cases, but it's bad in cases like this, where it's unclear what the correct behavior should be.

Anyway, in this example, should it behave like Option 1, or should it behave differently where Caddy manages the cert?
I think Option 2 matches the otherwise current behaviour where wildcard is preferred unless specified. If a different cert is wanted then `tls force_automate` can be used as an escape hatch.
Ok, I'll proceed with exploring option 2.
I pushed a commit that implements the logic, as correctly as I am able to fathom, for option 2.

I worry it might be a little surprising but I think it does make sense. It implements the "wildcards cover specific subdomains, when configured" default we now have.



Patch:
diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go
index ddec0b941d7..22bc2281639 100644
--- a/caddyconfig/httpcaddyfile/tlsapp.go
+++ b/caddyconfig/httpcaddyfile/tlsapp.go
@@ -698,14 +698,31 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
 	emptyAPCount := 0
 	origLenAPs := len(aps)
 	// compute the number of empty policies (disregarding subjects) - see #4128
+	// while we're at it,
 	emptyAP := new(caddytls.AutomationPolicy)
 	for i := 0; i < len(aps); i++ {
 		emptyAP.SubjectsRaw = aps[i].SubjectsRaw
+		emptyAP.ManagersRaw = nil
 		if reflect.DeepEqual(aps[i], emptyAP) {
+			// AP is empty
 			emptyAPCount++
-			if !automationPolicyHasAllPublicNames(aps[i]) {
-				// if this automation policy has internal names, we might as well remove it
-				// so auto-https can implicitly use the internal issuer
+
+			// see if this AP shadows something later
+			shadowIdx := automationPolicyShadows(i, aps)
+			emptyAP.SubjectsRaw = nil
+			if shadowIdx >= 0 {
+				emptyAP.SubjectsRaw = aps[shadowIdx].SubjectsRaw
+				// allow the later policy, which is likely for a wildcard, to have cert
+				// managers ("get_certificate"), since wildcards now cover specific
+				// subdomains by default, when configured (see discussion in #7559)
+				emptyAP.ManagersRaw = aps[shadowIdx].ManagersRaw
+			}
+
+			// if this is the last AP, we can delete it, since auto-https should
+			// pick it up; if it shadows something later that is also empty, we
+			// can similarly delete this; but if it shadows something that is NOT
+			// empty, we must not delete it since the shadowing has a purpose
+			if i == len(aps)-1 || (shadowIdx >= 0 && reflect.DeepEqual(aps[shadowIdx], emptyAP)) {
 				aps = slices.Delete(aps, i, i+1)
 				i--
 			}


Test patch:
diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest
index 9cdfd120033..75a9deb2c7e 100644
--- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest
@@ -54,11 +54,6 @@ b.com {
 								"via": "http"
 							}
 						]
-					},
-					{
-						"subjects": [
-							"b.com"
-						]
 					}
 				]
 			}
diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_12.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_12.caddyfiletest
new file mode 100644
index 00000000000..b79d2974699
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_12.caddyfiletest
@@ -0,0 +1,96 @@
+# example from https://github.com/caddyserver/caddy/issues/7559
+*.test.local {
+	tls {
+		get_certificate http http://cert-server:9000/certs
+	}
+	respond "wildcard"
+}
+
+# certificate for this subdomain is covered by wildcard above
+subdomain.test.local {
+	respond "subdomain"
+}
+
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":443"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"subdomain.test.local"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"body": "subdomain",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							],
+							"terminal": true
+						},
+						{
+							"match": [
+								{
+									"host": [
+										"*.test.local"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"body": "wildcard",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							],
+							"terminal": true
+						}
+					]
+				}
+			}
+		},
+		"tls": {
+			"automation": {
+				"policies": [
+					{
+						"subjects": [
+							"*.test.local"
+						],
+						"get_certificate": [
+							{
+								"url": "http://cert-server:9000/certs",
+								"via": "http"
+							}
+						]
+					}
+				]
+			}
+		}
+	}
+}
\ No newline at end of file