Caddy v2 - redirect non-SSL HTTP traffic to HTTPS on same (non-standard) port

1. Caddy version (caddy version):

2.0.0

2. How I run Caddy:

a. System environment:

Ubuntu 20.04 LTS

b. Command:

systemctl start caddy.service
# or
systemctl reload caddy.service

c. Service/unit/compose file:

Standard file installed by OS package manager. No changes.

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

[Service]
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

d. My complete Caddyfile or JSON config:

(redirect) {
        @http {
                protocol http
        }
        redir @http https://{hostport}{uri} 301
}

(geoserver_reverse_proxy) {
	# Reverse proxy to Geoserver running on different host on non-HTTPS port
    reverse_proxy 192.168.100.100:8080 {
        header_up X-Forwarded-Host {hostport}
    }
}

mydomain.com:8080 {
    import redirect
    import geoserver_reverse_proxy
}

mydomain.com {
	# Serve other content on port 80 / 443
}

3. The problem Iā€™m having:

  • I am trying to use Caddy as a reverse proxy for Geoserver.

  • Geoserver is running on host with IP 192.168.100.100:8080 (not HTTPS).

  • The Caddy host will be running another app on the default HTTP(S) ports 80/443.

  • My problem/requirement is that HTTP requests on mydomain.com:8080 should be re-directed to HTTPS on the same port.

  • With Nginx, the relevant working configuration is shown below. Any HTTP request to mydomain.com:8080 gets redirected to HTTPS.

# Nginx config
server {
   listen 8080 ssl;

   server_name mydomain.com;

   ssl_certificate host.crt;
   ssl_certificate_key  host.key;
   ssl_protocols TLSv1.2 TLSv1.3;
   ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;

   error_page 497 =301 https://$host:$server_port$request_uri;

   location / {
			proxy_pass http://192.168.100.100:8080/geoserver/;
			proxy_pass_header Set-Cookie;
			proxy_set_header Host $host:$server_port;
			proxy_set_header X-Forwarded-Proto $scheme;
			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	}


	location /geoserver/ {
			 proxy_pass http://192.168.100.100:8080/geoserver/;
			 proxy_pass_header Set-Cookie;
			 proxy_set_header Host $host:$server_port;
			 proxy_set_header X-Forwarded-Proto $scheme;
			 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	}
}
  • You may notice I have not set up the routes in my Caddyfile the way it is in Nginx, but that can come later. Re-direction from HTTP to HTTPS is more crucial for me.

  • So my question would be - is redirecting HTTP to HTTPS on the same port possible? If so, what changes should I make to the Caddyfile?

4. Error messages and/or full log output:

I get a HTTP 400 Bad Request when visiting http://mydomain.com:8080. There is also no automatic redirection to https.

If using https instead of http, reverse proxying works fine.

5. What I already tried:

I tried using the redir directive with matcher for http protocol as seen above to no avail. If I use scheme instead of protocol as shown in Caddyfile Concepts ā€” Caddy Documentation , I get an error when validating the Caddyfile

validate: adapting config using caddyfile: getting matcher module 'scheme': module not registered: http.matchers.scheme

Maybe I am not using it in the right place???

I also tried using the Nginx adapter to try and convert the working configuration, but the result doesnā€™t give me much confidence. Here is the output if anyoneā€™s interested.

{"apps":{"http":{"servers":{"server_0":{"listen":[":8080"],"routes":[{"match":[{"host":["mydomain.com"]},{"path":["/*"]}],"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","headers":{"request":{"set":{"Host":["{http.reverse_proxy.upstream.host}"]}}},"upstreams":[{"dial":"tcp
/192.168.100.100:8080"}]}],"match":[{"path":["/*"]}]}]}]},{"match":[{"host":["mydomain.com"]},{"path":["/geoserver/*"]}],"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","headers":{"request":{"set":{"Host":["{http.reverse_proxy.upstream.host}"]}}},"upstreams":[{"dial":"tcp/192.168.100.100:8080"}]}],"match":[{"path":["/geoserver/*"]}]}]}]}]}}}}}

6. Links to relevant resources:

1 Like

Oops, thatā€™s a mistake! Thanks for pointing that one out. I think the matcher may have been called scheme at one point in the past, but itā€™s now protocol. Fixed! docs: Fix bad matcher in snippet example Ā· caddyserver/website@c7b4ed3 Ā· GitHub (will go out with the next docs update)

What exactly are you getting in that 400 response? Can you use curl -v to replicate the issue and show the full HTTP response? Iā€™m not convinced that the 400 status actually comes from Caddy, thatā€™s not something it typically does.

1 Like

Thank you for fixing the error in the documentation.

The output from curl -v is pasted below (note: domain name and IP addresses changed for privacy)

# curl -v http://mydomain.com:8080/

* Trying 1.2.3.4 ...
* TCP_NODELAY set
* Connected to mydomain.com (1.2.3.4) port 8080 (#0)
> GET / HTTP/1.1
> Host: mydomain.com:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 400 Bad Request
<
Client sent an HTTP request to an HTTPS server.
* Closing connection 0

Corresponding output via tcpdump on the server side (11.22.33.44 is the client IP; Caddy runs on 192.168.1.100)

root@192.168.1.100:~# tcpdump -vv -eni any 'tcp port 8080'

tcpdump: listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
09:14:01.529057  In 02:6a:4c:ae:51:88 ethertype IPv4 (0x0800), length 68: (tos 0x0, ttl 112, id 22393, offset 0, flags [none], proto TCP (6), length 52)
    11.22.33.44.35642 > 192.168.1.100.8080: Flags [S], cksum 0x75c4 (correct), seq 3236704269, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
09:14:01.529099 Out 02:d3:b4:11:4e:34 ethertype IPv4 (0x0800), length 68: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.100.8080 > 11.22.33.44.35642: Flags [S.], cksum 0x62bd (incorrect -> 0xd121), seq 2587554547, ack 3236704270, win 62727, options [mss 8961,nop,nop,sackOK,nop,wscale 7], length 0
09:14:01.541098  In 02:6a:4c:ae:51:88 ethertype IPv4 (0x0800), length 62: (tos 0x0, ttl 112, id 22394, offset 0, flags [none], proto TCP (6), length 40)
    11.22.33.44.35642 > 192.168.1.100.8080: Flags [.], cksum 0x2248 (correct), seq 1, ack 1, win 513, length 0
09:14:01.544039  In 02:6a:4c:ae:51:88 ethertype IPv4 (0x0800), length 149: (tos 0x0, ttl 112, id 22395, offset 0, flags [none], proto TCP (6), length 133)
    11.22.33.44.35642 > 192.168.1.100.8080: Flags [P.], cksum 0x3f3b (correct), seq 1:94, ack 1, win 513, length 93: HTTP, length: 93
        GET / HTTP/1.1
        Host: mydomain.com:8080
        User-Agent: curl/7.58.0
        Accept: */*

09:14:01.544075 Out 02:d3:b4:11:4e:34 ethertype IPv4 (0x0800), length 56: (tos 0x0, ttl 64, id 20274, offset 0, flags [DF], proto TCP (6), length 40)
    192.168.1.100.8080 > 11.22.33.44.35642: Flags [.], cksum 0x62b1 (incorrect -> 0x2202), seq 1, ack 94, win 490, length 0
09:14:01.544143 Out 02:d3:b4:11:4e:34 ethertype IPv4 (0x0800), length 132: (tos 0x0, ttl 64, id 20275, offset 0, flags [DF], proto TCP (6), length 116)
    192.168.1.100.8080 > 11.22.33.44.35642: Flags [P.], cksum 0x62fd (incorrect -> 0x67fb), seq 1:77, ack 94, win 490, length 76: HTTP, length: 76
        HTTP/1.0 400 Bad Request

        Client sent an HTTP request to an HTTPS server.
09:14:01.544165 Out 02:d3:b4:11:4e:34 ethertype IPv4 (0x0800), length 56: (tos 0x0, ttl 64, id 20276, offset 0, flags [DF], proto TCP (6), length 40)
    192.168.1.100.8080 > 11.22.33.44.35642: Flags [F.], cksum 0x62b1 (incorrect -> 0x21b5), seq 77, ack 94, win 490, length 0
09:14:01.555925  In 02:6a:4c:ae:51:88 ethertype IPv4 (0x0800), length 62: (tos 0x0, ttl 112, id 22396, offset 0, flags [none], proto TCP (6), length 40)
    11.22.33.44.35642 > 192.168.1.100.8080: Flags [.], cksum 0x219f (correct), seq 94, ack 78, win 512, length 0
09:14:01.556322  In 02:6a:4c:ae:51:88 ethertype IPv4 (0x0800), length 62: (tos 0x0, ttl 112, id 22397, offset 0, flags [none], proto TCP (6), length 40)
    11.22.33.44.35642 > 192.168.1.100.8080: Flags [F.], cksum 0x219e (correct), seq 94, ack 78, win 512, length 0
09:14:01.556339 Out 02:d3:b4:11:4e:34 ethertype IPv4 (0x0800), length 56: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40)
    192.168.1.100.8080 > 11.22.33.44.35642: Flags [.], cksum 0x21b4 (correct), seq 78, ack 95, win 490, length 0


Am also pasting the output from Curl when I use https instead (the 404 error here is fine because the root of the upstream server has no pages. Existing upstream routes resolve properly)

# curl -v https://mydomain.com:8080/

*   Trying 1.2.3.4...
* TCP_NODELAY set
* Connected to mydomain.com (1.2.3.4) port 8080 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS Unknown, Certificate Status (22):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS Unknown, Certificate Status (22):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS Unknown, Certificate Status (22):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS Unknown, Certificate Status (22):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS change cipher, Client hello (1):
* (304) (OUT), TLS Unknown, Certificate Status (22):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using unknown / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=mydomain.com
*  start date: Jul  1 10:11:06 2020 GMT
*  expire date: Sep 29 10:11:06 2020 GMT
*  subjectAltName: host "mydomain.com" matched cert's "mydomain.com"
*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* (304) (OUT), TLS Unknown, Unknown (23):
* (304) (OUT), TLS Unknown, Unknown (23):
* (304) (OUT), TLS Unknown, Unknown (23):
* Using Stream ID: 1 (easy handle 0x7fffc06c1580)
* (304) (OUT), TLS Unknown, Unknown (23):
> GET / HTTP/2
> Host: mydomain.com:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
* (304) (IN), TLS Unknown, Certificate Status (22):
* (304) (IN), TLS handshake, Newsession Ticket (4):
* (304) (IN), TLS Unknown, Unknown (23):
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
* (304) (OUT), TLS Unknown, Unknown (23):
* (304) (IN), TLS Unknown, Unknown (23):
* (304) (IN), TLS Unknown, Unknown (23):
* (304) (IN), TLS Unknown, Unknown (23):
< HTTP/2 404
< content-type: text/html;charset=utf-8
< server: Caddy
< server: Jetty(9.4.18.v20190429)
< content-length: 864
< date: Mon, 20 Jul 2020 09:30:00 GMT
<
* (304) (IN), TLS Unknown, Unknown (23):
<!DOCTYPE html>
<html lang="en">
<head>
<title>Error 404 - Not Found</title>
<meta charset="utf-8">
<style>body { font-family: sans-serif; } table, td { border: 1px solid #333; } td, th { padding: 5px; } thead, tfoot { background-color: #333; co
lor: #fff; } </style>
</head>
<body>
<h2>Error 404 - Not Found.</h2>
<p>No context on this server matched or handled this request.</p>
<p>Contexts known to this server are:</p>
<table class="contexts"><thead><tr><th>Context Path</th><th>Display Name</th><th>Status</th><th>LifeCycle</th></tr></thead><tbody>
<tr><td><a href="/geoserver/">/geoserver</a></td><td>GeoServer&nbsp;</td><td>Available</td><td>STARTED</td></tr>
</tbody></table><hr/>
<a href="http://eclipse.org/jetty"><img alt="icon" src="/favicon.ico"/></a>&nbsp;<a href="http://eclipse.org/jetty">Powered by Eclipse Jetty:// S
erver</a><hr/>
</body>
</html>
* Connection #0 to host mydomain.com left intact

TL;DR - is HTTP to HTTPS redirection on the same port (8080 in my case) possible in Caddy without encountering a HTTP 400 error? If so, how can this be configured in the Caddyfile?

Looks like the error comes from Golang net/http. Thereā€™s an issue with some relevant discussion here:

I donā€™t think Caddy currently supports having HTTP and HTTPS on the same port. @matt can probably give a better explanation.

Yeah, protocol multiplexing is tricky. Project Conncept does this though:

Otherwise, a socket typically answers with one expected protocol. Thatā€™s why HTTP is standardized to be on port 80 and TLS is standardized on port 443 (and SSH on 22, DNS on 53, etc) ā€“ by connecting to that port, you can know what protocol the other end will speak. Without that, it takes some gymnastics to accommodate each client properly. Project Conncept can do that, and itā€™s early access for sponsors right now until we reach the funding goal!

Why are you trying to serve HTTP and HTTPS on the same port?

@francislavoie and @matt - thanks for your replies. The golang Github issue thread was especially helpful in gaining an understanding. If the issue stems from lack of support at golangā€™s net/http level, then thereā€™s not much hope that we will see a fix anytime soon.

As for why Iā€™m serving HTTP and HTTPS on the same port, itā€™s to support legacy apps which default to the former. Our hands are also tied in that we are not free to serve HTTPS on a different port. While I enjoyed working with Caddy, I guess itā€™s back to Nginx for me which provides a way to handle such usecases.

Good luck with Project Conncept!

Well, itā€™s not so much about net/http as it is about how the Internet works (which is even harder to change, of course). You generally have to know which protocol to speak. The only ā€œprotocol negotiationā€ type thing I know of is in TLS: ALPN lets a server choose the next protocol, but of course it has to start with TLS, which is itself a protocol.

If you donā€™t know the protocol, you have to peek a few bytes and guess. Thatā€™s the best you can do. Conncept does this, youā€™re welcome to try it today if you want to become a sponsor!

Can you clarify, what does it redirect to? Is it just the same host and port but with the https:// scheme prefixed?

Yes, this is achieved via the error_page directive. I use it as shown below to respond with the 308 status code and redirect to the https:// version.

error_page 497 =308 https://$host:$server_port$request_uri;

If you are interested, the response in curl (without following redirects) is as shown below:

curl -v http://mydomain.com:8080
*   Trying 1.2.3.4:8080...
* Connected to mydomain.com (1.2.3.4) port 8080 (#0)
> GET / HTTP/1.1
> Host: mydomain.com:8080
> User-Agent: curl/7.71.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 308 Permanent Redirect
< Server: nginx/1.18.0 (Ubuntu)
< Date: Tue, 21 Jul 2020 16:48:14 GMT
< Content-Type: text/html
< Content-Length: 180
< Connection: close
< Location: https://mydomain.com:8080/
< 
<html>
<head><title>308 Permanent Redirect</title></head>
<body>
<center><h1>308 Permanent Redirect</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
* Closing connection 0

Oh weird, so Nginx uses a non-standard HTTP status 497 for that.

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

For anyone who finds this via searches, this can now be done in Caddy with the http_redirect listener wrapper.