Accessing HTTP-only upstream app on a HSTS domain through Caddy

1. The problem I’m having:

I have a Minecraft server on a domain with the .app extension, which is a strict transport security domain that only allows HTTPS connections. The Minecraft server runs the Dynmap plugin, which is a browser app that serves all content through HTTP. The domain, play.minecraftserver.app has an A record pointing to the server IP, 123.45.67.89. I can connect to the server with the domain and access the Dynmap through the server’s IP like http://123.45.67.89:8123. But when I configured Caddy with TLS and reverse_proxy 123.45.67.89:8123 for the /map handle path, https://play.minecraftserver.app/map just returns an empty page. I believe that the problem is caused by the upstream app only giving HTTP responses which are getting blocked by the HSTS domain, so I want to know how to configure Caddy to display the Dynmap on https://play.minecraftserver.app/map or a subdomain.

Log information about the request from the Caddy container:

{"level":"debug","ts":1683912268.6151292,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"123.45.67.89:8123","total_upstreams":1}
{"level":"debug","ts":1683912268.618348,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"123.45.67.89:8123","duration":0.003144619,"request":{"remote_ip":"123.45.67.89","remote_port":"36136","proto":"HTTP/2.0","method":"GET","host":"play.minecraftserver.app","uri":"/","headers":{"User-Agent":["curl/7.81.0"],"Accept":["*/*"],"X-Forwarded-For":["123.45.67.89"],"X-Forwarded-Proto":["https"],"X-Forwarded-Host":["play.minecraftserver.app"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"play.minecraftserver.app"}},"error":"tls: first record does not look like a TLS handshake"}
{"level":"error","ts":1683912268.6187713,"logger":"http.log.error","msg":"tls: first record does not look like a TLS handshake","request":{"remote_ip":"123.45.67.89","remote_port":"36136","proto":"HTTP/2.0","method":"GET","host":"play.minecraftserver.app","uri":"/map","headers":{"User-Agent":["curl/7.81.0"],"Accept":["*/*"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"play.minecraftserver.app"}},"duration":0.003718811,"status":502,"err_id":"g84qsjact","err_trace":"reverseproxy.statusError (reverseproxy.go:1272)"}

2. Error messages and/or full log output:

Response of curl -vL https://play.minecraftserver.app/map:

*   Trying 123.45.67.89:443...
* Connected to play.minecraftserver.app (123.45.67.89) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=play.minecraftserver.app
*  start date: May  8 09:02:31 2023 GMT
*  expire date: Aug  6 09:02:30 2023 GMT
*  subjectAltName: host "play.minecraftserver.app" matched cert's "play.minecraftserver.app"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x55f643f4fe80)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /map HTTP/2
> Host: play.minecraftserver.app
> 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 502
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Fri, 12 May 2023 17:24:28 GMT
<
* Connection #0 to host play.minecraftserver.app left intact

3. Caddy version:

2.6.4

4. How I installed and ran Caddy:

Official Docker image

a. System environment:

Ubuntu 22.04

b. Docker Compose file, command and Caddyfile:

version: "3.7"

services:
  caddy:
    image: caddy:2
    volumes:
      - ./caddy:/data/caddy
    command:
      - /bin/sh
      - -c
      - |
        cat <<EOF > /etc/caddy/Caddyfile && caddy run --config /etc/caddy/Caddyfile
        
        {
          debug
        }
        
        (hsts) {
          header Strict-Transport-Security max-age=63072000
        }
        
        play.minecraftserver.app {
          import hsts
          log
          handle_path /map {
            reverse_proxy 123.45.67.89:8123 {
              transport http {
                tls_insecure_skip_verify
              }
            }
          }
          tls {
            on_demand
          }
        }
        EOF
    network_mode: "host"
      
  minecraft:
    image: itzg/minecraft-server
    ports:
      - 8123:8123
      - 25565:25565
    environment:
      EULA: "TRUE"
    networks:
      minecraft-network:
        ipv4_address: 172.59.0.100
    tty: true
    stdin_open: true
    restart: unless-stopped
    volumes:
      - ./minecraft:/data
  
networks:
  minecraft-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.59.0.0/16

5. What I already tried:

Placing the Dynmap under a subdomain rather than a subdirectory to avoid the subfolder problem still causes the same error.

play.minecraftserver.app {
  import hsts
  log
  reverse_proxy 123.45.67.89:8123 {
    transport http {
      tls_insecure_skip_verify
    }
  }
  tls {
	on_demand
  }
}

curl -vL play.minecraftserver.app:8132 returns “Connection refused”.

6. Links to relevant resources:

None

Path matching is exact in Caddy. So /map will only match exactly /map and not /map/foo. Make sure to use /map* or /map/* instead if you need to match more.

Why are you turning on on_demand? Only do so if you have a specific reason to use it.

With this config, Caddy won’t send SNI to the upstream. You should use tls_server_name maybe. Are you sure that upstream is HTTPS, or is it HTTP? If it’s HTTP then remove tls_insecure_skip_verify because that’s implicitly enabling TLS.

That wouldn’t be going through Caddy at all, Caddy is only listening on ports 80 and 443. If you don’t have port 8123 forwarded, then it wouldn’t reach your minecraft server either.

Keep in mind that your config only handles requests to /map and does nothing else, so it’ll respond with an empty body and status 200 otherwise. You should add something to handle other requests, e.g. with a handle block with no matcher, to catch anything else.

I followed your suggestions and updated the Caddyfile so it looks like this now. Unfortunately, it’s still returning the 502 error and an empty page after I used docker compose up -d to recreate the container with the updated Caddyfile.

{
  debug
}

(hsts) {
  header Strict-Transport-Security max-age=63072000
}

play.minecraftserver.app {
  import hsts
  log
  handle_path /map* {
	reverse_proxy 123.45.67.89:8123 {
	  transport http {
		tls_server_name play.minecraftserver.app
	  }
	}
  }
  handle * {
	reverse_proxy 123.45.67.89:8123 {
	  transport http {
		tls_server_name play.minecraftserver.app
	  }
	}
  }
}

The upstream is HTTP and port 8123 is open on the Minecraft server’s container. It can be reached from http://123.45.67.89:8123, but visiting the domain that reverse proxies to this address leads to the 502 error.

I forgot to mention in the original post that the web app retrieves a lot of styling and image assets with paths like http://123.45.67.89:8123/tiles/world/flat/0_0/zzz_0_16.jpg, but I don’t think this is the cause of the problem because it also fails to retrieve the HTML file from the root domain when https://play.minecraftserver.app/map is accessed.

Then remove any mention of tls in the proxy config. Just do reverse_proxy 123.45.67.89:8123 and don’t configure transport.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.