400 Bad request when using reverse proxy to web server via reverse SSH tunnel

1. The problem I’m having:

What I am trying to do

I am trying to setup a communication channel from a browser to a web server on a GNU/Linux system, more specifically Home Assistant, which resides behind a firewall. I have setup a reverse SSH tunnel according to this:

I have also created my own Docker image, which includes the module dns.providers.gandi, and followed the instructions ’ Adding custom Caddy modules’ here:


See section ‘Dockerfile’ further down.

I suspect I am quite close to a solution and suspect that I might need to add some reverse_proxy directives in order for this to work.

Curl result

mikael@vps:~$ curl -vL https://orust-nedre-tratte.site.gridenforcer.se
*   Trying
* Connected to orust-nedre-tratte.site.gridenforcer.se ( port 443 (#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
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* 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=orust-nedre-tratte.site.gridenforcer.se
*  start date: Jul 22 00:00:00 2023 GMT
*  expire date: Oct 20 23:59:59 2023 GMT
*  subjectAltName: host "orust-nedre-tratte.site.gridenforcer.se" matched cert's "orust-nedre-tratte.site.gridenforcer.se"
*  issuer: C=AT; O=ZeroSSL; CN=ZeroSSL ECC Domain Secure Site CA
*  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
* Using Stream ID: 1 (easy handle 0x5592876a42c0)
> GET / HTTP/2
> Host: orust-nedre-tratte.site.gridenforcer.se
> user-agent: curl/7.74.0
> accept: */*
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 400 
< alt-svc: h3=":443"; ma=2592000
< content-type: text/plain; charset=utf-8
< date: Sun, 23 Jul 2023 15:03:34 GMT
< server: Caddy
< server: Python/3.11 aiohttp/3.8.4
< content-length: 16
* Connection #0  to host orust-nedre-tratte.site.gridenforcer.se left intact
400: Bad Request

What isn’t working?

In the Curl request one can see “400 Bad Request”.

2. Error messages and/or full log output:

Caddy debug log

When issuing curl -vL https://orust-nedre-tratte.site.gridenforcer.se, Caddy logs the following:

2023/07/23 15:34:14.850	DEBUG	events	event	{"name": "tls_get_certificate", "id": "c975edba-4c79-4067-a614-0efda974b5fa", "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":"orust-nedre-tratte.site.gridenforcer.se","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],"Conn":{}}}}
2023/07/23 15:34:14.850	DEBUG	tls.handshake	choosing certificate	{"identifier": "orust-nedre-tratte.site.gridenforcer.se", "num_choices": 1}
2023/07/23 15:34:14.851	DEBUG	tls.handshake	default certificate selection results	{"identifier": "orust-nedre-tratte.site.gridenforcer.se", "subjects": ["orust-nedre-tratte.site.gridenforcer.se"], "managed": true, "issuer_key": "acme.zerossl.com-v2-DV90", "hash": "29db6058e8ef19c3b1757c9fac871465e397d06beb3ec5f20e84e15bd1a3b3a5"}
2023/07/23 15:34:14.852	DEBUG	tls.handshake	matched certificate in cache	{"remote_ip": "", "remote_port": "45660", "subjects": ["orust-nedre-tratte.site.gridenforcer.se"], "managed": true, "expiration": "2023/10/21 00:00:00.000", "hash": "29db6058e8ef19c3b1757c9fac871465e397d06beb3ec5f20e84e15bd1a3b3a5"}
2023/07/23 15:34:14.923	DEBUG	http.handlers.reverse_proxy	selected upstream	{"dial": "localhost:8888", "total_upstreams": 1}
2023/07/23 15:34:14.950	DEBUG	http.handlers.reverse_proxy	upstream roundtrip	{"upstream": "localhost:8888", "duration": 0.026422414, "request": {"remote_ip": "", "remote_port": "45660", "client_ip": "", "proto": "HTTP/2.0", "method": "GET", "host": "orust-nedre-tratte.site.gridenforcer.se", "uri": "/", "headers": {"User-Agent": ["curl/7.81.0"], "Accept": ["*/*"], "X-Forwarded-For": [""], "X-Forwarded-Proto": ["https"], "X-Forwarded-Host": ["orust-nedre-tratte.site.gridenforcer.se"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "orust-nedre-tratte.site.gridenforcer.se"}}, "headers": {"Content-Type": ["text/plain; charset=utf-8"], "Content-Length": ["16"], "Date": ["Sun, 23 Jul 2023 15:34:14 GMT"], "Server": ["Python/3.11 aiohttp/3.8.4"]}, "status": 400}

3. Caddy version:

Running Caddy with Docker but not with Docker Compose. When running ‘caddy version’ in Docker container, I get:

v2.7.0-beta.2 h1:jaS1odoRuDR2W8igaKgVGvVjhTNt8xfoz3YPC4bcenA=

4. How I installed and ran Caddy:


FROM caddy:2.7-builder AS builder

RUN xcaddy build
–with GitHub - caddy-dns/gandi: Caddy module: dns.providers.gandi

FROM caddy:2.7

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Docker run

docker run --name caddy-gandi --net host -p -p 443:443 -p 443:443/udp -it -v $PWD/Caddyfile:/etc/caddy/Caddyfile -v /site:/srv -v caddy_data:/data -v caddy_config:/config 2.7-gandi:latest

a. System environment:

  • OS: Debian GNU/Linux 11
  • Docker server and client versions: 20.10.5+dfsg1
  • Systemd: 247 (247.3-7+deb11u2)

b. Command:

docker start -a caddy-gandi

Where caddy-gandi is the Docker container name which I created before. See section ‘Docker run’.

d. My complete Caddy config:

orust-nedre-tratte.site.gridenforcer.se {
        tls info@gridenforcer.se {
                dns gandi 'MY GANDI API KEY'
        reverse_proxy localhost:8888

5. Links to relevant resources:

I think you need to configure HomeAssistant to trust requests coming from Caddy. See HTTP - Home Assistant, the trusted_proxies config. Check your HomeAssistant logs, it should show which IP address it’s rejecting.

Many thanks for the reply and the tip. :slight_smile: Indeed, that seems to be the case. I found the following in the log:

Logger: homeassistant.components.http.forwarded
Source: components/http/forwarded.py:114
Integration: HTTP ([documentation](https://www.home-assistant.io/integrations/http), [issues](https://github.com/home-assistant/core/issues?q=is%3Aissue+is%3Aopen+label%3A%22integration%3A+http%22))
First occurred: July 23, 2023 at 10:42:22 (33 occurrences)
Last logged: 09:11:13

A request from a reverse proxy was received from ::1, but your HTTP integration is not set-up for reverse proxies

I will look into this and get back. :slight_smile:

I added the following in Home Assistant’s configuration:

  use_x_forwarded_for: true
    - ::1

And it just worked! :smiley:

I love Caddy and its community, many thanks! :smiley:

1 Like

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