Very weird issue regarding on demand TLS and unconsistent behavior

1. The problem I’m having:

I want to use caddy for its reverse proxy and TLS issuing capabilites. Specifically the on demand tls feature. It is used for issuing certificates with the local CA, no public access. The server offers services reachable via its IPs, hostnames and also .local domains that are published via mDNS in the network. My config creates the following inconsistent behavior:

Curl to hostname

curl -k -v -L "http://server01"
*   Trying 192.168.60.38:80...
* Connected to server01 (192.168.60.38) port 80 (#0)
> GET / HTTP/1.1
> Host: server01
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Alt-Svc: h3=":443"; ma=2592000
< Content-Type: text/html
< Server: Caddy
< Date: Mon, 28 Apr 2025 23:17:20 GMT
< Content-Length: 49
< 
* Connection #0 to host server left intact
<html><body><h1>Great success!</h1></body></html>
  1. The redirect from HTTP to HTTPS does not work
  2. I get the response body

Curl to *.local domain

curl -k -v -L "http://server01.local"
*   Trying 192.168.60.38:80...
* Connected to server01.local (192.168.60.38) port 80 (#0)
> GET / HTTP/1.1
> Host: server01.local
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 308 Permanent Redirect
< Alt-Svc: h3=":443"; ma=2592000
< Connection: close
< Location: https://server01.local/
< Server: Caddy
< Date: Mon, 28 Apr 2025 23:19:36 GMT
< Content-Length: 0
< 
* Closing connection 0
* Clear auth, redirects to port from 80 to 443
* Issue another request to this URL: 'https://server01.local/'
*   Trying 192.168.60.38:443...
* Connected to server01.local (192.168.60.38) port 443 (#1)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
...
* Using Stream ID: 1 (easy handle 0x62a3f1bf99f0)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET / HTTP/2
> Host: server01.local
> user-agent: curl/7.81.0
> accept: */*
> 
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 200 
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Mon, 28 Apr 2025 23:19:36 GMT
< 
* Connection #1 to host server01.local left intact
  1. I get a redirect from HTTP to HTTPS as it should be
  2. I get no response body

Curl to IP

curl -k -v -L "http://192.168.60.38" → Same output as curl to *.local

  1. I get a redirect from HTTP to HTTPS as it should be
  2. I get no response body

2. Error messages and/or full log output:

Here is the log for first a request to a IP, then to the *.local domain. You will laugh, but for just the hostname I don’t get log output.

{"level":"info","ts":1745883674.2854295,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}
{"level":"info","ts":1745883674.2857394,"logger":"http.log","msg":"server running","name":"main","protocols":["h1","h2","h3"]}
{"level":"info","ts":1745883674.2857552,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["*"]}
{"level":"warn","ts":1745883674.2857654,"logger":"tls","msg":"YOUR SERVER MAY BE VULNERABLE TO ABUSE: on-demand TLS is enabled, but no protections are in place","docs":"https://caddyserver.com/docs/automatic-https#on-demand-tls"}
{"level":"warn","ts":1745883674.2864292,"logger":"pki.ca.local","msg":"installing root certificate (you might be prompted for password)","path":"storage:pki/authorities/local/root.crt"}
{"level":"info","ts":1745883674.2906094,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/root/.local/share/caddy","instance":"a4f5efb8-93e8-4340-bde9-cea6d52c68eb","try_again":1745970074.2906065,"try_again_in":86399.999999566}
{"level":"info","ts":1745883674.2907023,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"error","ts":1745883674.2954905,"logger":"pki.ca.local","msg":"failed to install root certificate","error":"failed to execute sudo: exit status 1","certificate_file":"storage:pki/authorities/local/root.crt"}
{"level":"debug","ts":1745883674.2955394,"logger":"events","msg":"event","name":"started","id":"9e5b14b5-3b44-4e70-b6ce-3cabce46b5d3","origin":"","data":null}
{"level":"info","ts":1745883674.2956579,"msg":"autosaved config (load with --resume flag)","file":"/root/.config/caddy/autosave.json"}
{"level":"debug","ts":1745883683.069651,"logger":"events","msg":"event","name":"tls_get_certificate","id":"7ed40528-4925-4353-a036-ad539f428955","origin":"tls","data":{"client_hello":{"CipherSuites":[4866,4867,4865,49196,49200,159,52393,52392,52394,49195,49199,158,49188,49192,107,49187,49191,103,49162,49172,57,49161,49171,51,157,156,61,60,53,47,255],"ServerName":"","SupportedCurves":[29,23,30,25,24,256,257,258,259,260],"SupportedPoints":"AAEC","SignatureSchemes":[1027,1283,1539,2055,2056,2057,2058,2059,2052,2053,2054,1025,1281,1537,771,769,770,1026,1282,1538],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771],"RemoteAddr":{"IP":"192.168.60.68","Port":36620,"Zone":""},"LocalAddr":{"IP":"192.168.60.38","Port":443,"Zone":""}}}}
{"level":"debug","ts":1745883683.0698292,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"192.168.60.38"}
{"level":"debug","ts":1745883683.0698707,"logger":"tls.handshake","msg":"all external certificate managers yielded no certificates and no errors","remote_ip":"192.168.60.68","remote_port":"36620","sni":""}
{"level":"debug","ts":1745883683.070528,"logger":"tls.cache","msg":"added certificate to cache","subjects":["192.168.60.38"],"expiration":1745924011,"managed":true,"issuer_key":"local","hash":"0863b520452fa56ba4d025178fce814340eb32891427aab6cfbc1e5e790f555f","cache_size":1,"cache_capacity":10000}
{"level":"debug","ts":1745883683.070587,"logger":"events","msg":"event","name":"cached_managed_cert","id":"3ff6716e-f3d7-45b4-bd11-19e8d54bda8f","origin":"tls","data":{"sans":["192.168.60.38"]}}
{"level":"debug","ts":1745883683.0706265,"logger":"tls.handshake","msg":"loaded certificate from storage","remote_ip":"192.168.60.68","remote_port":"36620","subjects":["192.168.60.38"],"managed":true,"expiration":1745924011,"hash":"0863b520452fa56ba4d025178fce814340eb32891427aab6cfbc1e5e790f555f"}
{"level":"debug","ts":1745883691.9273942,"logger":"events","msg":"event","name":"tls_get_certificate","id":"a43af01b-7755-467d-91e4-6d4d2719a57c","origin":"tls","data":{"client_hello":{"CipherSuites":[4866,4867,4865,49196,49200,159,52393,52392,52394,49195,49199,158,49188,49192,107,49187,49191,103,49162,49172,57,49161,49171,51,157,156,61,60,53,47,255],"ServerName":"server01.local","SupportedCurves":[29,23,30,25,24,256,257,258,259,260],"SupportedPoints":"AAEC","SignatureSchemes":[1027,1283,1539,2055,2056,2057,2058,2059,2052,2053,2054,1025,1281,1537,771,769,770,1026,1282,1538],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771],"RemoteAddr":{"IP":"192.168.60.68","Port":48458,"Zone":""},"LocalAddr":{"IP":"192.168.60.38","Port":443,"Zone":""}}}}
{"level":"debug","ts":1745883691.9275274,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"server01.local"}
{"level":"debug","ts":1745883691.9275544,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"*.local"}
{"level":"debug","ts":1745883691.9275672,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"*.*"}
{"level":"debug","ts":1745883691.927588,"logger":"tls.handshake","msg":"all external certificate managers yielded no certificates and no errors","remote_ip":"192.168.60.68","remote_port":"48458","sni":"server01.local"}
{"level":"debug","ts":1745883691.9283504,"logger":"tls.cache","msg":"added certificate to cache","subjects":["server01.local"],"expiration":1745924028,"managed":true,"issuer_key":"local","hash":"5e07af611f7cc108fd6ba2aae2ec52e5ca4c180ba6dffe60c995065949cee2b3","cache_size":2,"cache_capacity":10000}
{"level":"debug","ts":1745883691.9284291,"logger":"events","msg":"event","name":"cached_managed_cert","id":"997358c9-2290-44a7-88dc-cf6caeb0c4fb","origin":"tls","data":{"sans":["server01.local"]}}
{"level":"debug","ts":1745883691.928465,"logger":"tls.handshake","msg":"loaded certificate from storage","remote_ip":"192.168.60.68","remote_port":"48458","subjects":["server01.local"],"managed":true,"expiration":1745924028,"hash":"5e07af611f7cc108fd6ba2aae2ec52e5ca4c180ba6dffe60c995065949cee2b3"}

3. Caddy version:

caddy --version
v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=

4. How I installed and ran Caddy:

sudo apt install caddy

a. System environment:

Ubuntu 24.04.2

b. Command:

n.A., I use the systemd service

c. Service/unit/compose file:

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=root
Group=root
ExecStart=/usr/bin/caddy run --environ --resume
ExecReload=/usr/bin/caddy run --resume
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

d. My complete Caddy config:

{
    "admin": {
        "disabled": false
    },
    "apps": {
        "tls": {
            "automation": {
                "policies": [
                    {
                        "issuers": [
                            {
                                "module": "internal",
                                "sign_with_root": false
                            }
                        ],
                        "on_demand": true
                    }
                ]
            }
        },
        "http": {
            "servers": {
                "main": {
                    "listen": [
                        ":80",
                        ":443"
                    ],
                    "routes": [
                        {
                            "match": [
                                {
                                    "host": [
                                        "*"
                                    ]
                                }
                            ],
                            "handle": [
                                {
                                    "handler": "static_response",
                                    "body": "<html><body><h1>Great success!</h1></body></html>",
                                    "headers": {
                                        "Content-Type": [
                                            "text/html"
                                        ]
                                    }
                                }
                            ]
                        }
                    ]
                }
            }
        }
    }
}

5. Links to relevant resources:

n.A.

"host": ["*"] will only match single-label domains like localhost, not server01.local. You’d need *.* for that, or *.local, etc. See the docs for the host matcher: