Reverse-Proxy chaining: VPS -> Local server -> Service. Trusted Proxies?

1. Output of caddy version:

root@birb:/srv# caddy version
v2.6.2 h1:wKoFIxpmOJLGl3QXoo6PNbYvGW4xLEgo32GPBEjWL8o=

root@FriendlyWrt:~# caddy version
v2.6.2 h1:wKoFIxpmOJLGl3QXoo6PNbYvGW4xLEgo32GPBEjWL8o=

2. How I run Caddy:

VPS: SystemD
OpenWrt: /etc/rc.local + caddy reload/stop/start/...

a. System environment:

VPS:

  • Debian 11
    OpenWrt:
# cat /etc/openwrt_release
DISTRIB_ID='OpenWrt'
DISTRIB_RELEASE='22.03.2'
DISTRIB_REVISION='r19803-9a599fee93'
DISTRIB_TARGET='rockchip/armv8'
DISTRIB_ARCH='aarch64_generic'
DISTRIB_DESCRIPTION='OpenWrt 22.03.2 r19803-9a599fee93'
DISTRIB_TAINTS='busybox'

b. Command:

Not applicable here... handled by init systems.

c. Service/unit/compose file:

VPS: Default systemd unit
OpenWrt:

# /etc/rc.local:
/usr/bin/caddy start --resume --config /srv/Caddyfile

d. My complete Caddy config:

Ok, this is where it becomes a little messy. :slight_smile:

First, my VPSā€™ config:

birb.it {
        root * /srv/birb.it
        file_server
}
# ... cutting out other unrelated domains ...
#*.birb.it {
#  # Home server link
#  reverse_proxy * 192.168.222.11:80 {
#    transport http {
#      # Add to nanopi: trusted_proxies 123.123.123.123
#      # maybe? tls_trusted_ca_certs <pem_files...>
#      compression on
#    }
#  }
#}

And my OpenWrt one:

# Globals
{
        log {
                level info
                output file /var/log/caddy.log {
                        roll_size 10mb
                        roll_keep 10
                        roll_keep_for 720h
                }
        }
        trusted_proxies 192.168.222.0/24 # <- Error, invalid!
}

(php) {
        php_fastcgi unix//var/run/php8-fpm.sock {
                capture_stderr true
        }
}

:80 {
        # Set this path to your site's directory.
        root * /srv/default

        # Enable the static file server.
        file_server

        # Another common task is to set up a reverse proxy:
        # reverse_proxy localhost:8080

        # Or serve a PHP site through php-fpm:
        # php_fastcgi localhost:9000
}

# Basic ping-pong
echo.birb.it:80 {
        respond "hello"
}

# LuCi
router.birb.it:80 {
        reverse_proxy * localhost:8080
}

# Home Assistant
hass.birb.it:80 {
        reverse_proxy * localhost:8123
}

# TVHeadend
tvh.birb.it:80 {
        reverse_proxy * localhost:9981 {
                header_up X-Real-IP {remote_host}
                header_up X-Forwarded-For {remote_host}
        }
}

# Jellyfin
video.birb.it:80 {
        reverse_proxy * localhost:8096
}

# Navidrome
audio.birb.it:80 {
        reverse_proxy * localhost:4533
}

# Photoprism...?
# photo.birb.it:80 { reverse_proxy * localhost:x }

# NextCloud
cloud.birb.it:80 {
        root * /sdcard/srv/nextcloud/
        import php
        file_server
        redir /.well-known/carddav /remote.php/dav 301
        redir /.well-known/caldav /remote.php/dav 301
        redir /.well-known/webfinger /index.php/.well-known/webfinger 301
        redir /.well-known/nodeinfo /index.php/.well-known/nodeinfo 301
        @forbidden {
                path /.htaccess /data/* /config/* /db_structure /.xml /README /3rdparty/* /lib/* /templates/* /occ /console.php
        }
        respond @forbidden 404
}

# Monica
monica.birb.it:80 {
        root * /sdcard/srv/monica/public
        import php
        file_server
}

# Grocy
grocy.birb.it:80 {
        root * /sdcard/srv/grocy/public
        import php
        file_server
}

# Paperless / Paperspace
# paper.birb.it:80 { reverse_proxy * localhost:x }

tubesync.birb.it:80 {
        reverse_proxy * localhost:4848
}

neko.birb.it:80 {
        reverse_proxy * localhost:8989
}

rclone.birb.it:80 {
        root * /www/rclone-webui-react/
        file_server
}

3. The problem Iā€™m having:

From my local server itself, OpenWrt, it looks fine (i abused my Grocy host for this test ^^')

root@FriendlyWrt:/sdcard/srv/grocy/public# curl grocy.birb.it/hdrs.php
Array
(
    [TEMP] => /tmp
    [TMPDIR] => /tmp
    [TMP] => /tmp
    [PATH] => /usr/local/bin:/usr/bin:/bin
    [HOSTNAME] =>
    [USER] => www
    [HOME] => /home/www
    [REQUEST_URI] => /hdrs.php
    [SERVER_PROTOCOL] => HTTP/1.1
    [GATEWAY_INTERFACE] => CGI/1.1
    [HTTP_X_FORWARDED_HOST] => grocy.birb.it
    [SERVER_PORT] => 80
    [REQUEST_METHOD] => GET
    [REMOTE_USER] =>
    [HTTP_ACCEPT] => */*
    [REMOTE_PORT] => 54042
    [REMOTE_ADDR] => 192.168.2.1
    [QUERY_STRING] =>
    [CONTENT_LENGTH] => 0
    [HTTP_X_FORWARDED_PROTO] => http
    [HTTP_HOST] => grocy.birb.it
    [REQUEST_SCHEME] => http
    [REMOTE_IDENT] =>
    [HTTP_USER_AGENT] => curl/7.86.0
    [PATH_INFO] =>
    [SCRIPT_FILENAME] => /sdcard/srv/grocy/public/hdrs.php
    [DOCUMENT_ROOT] => /sdcard/srv/grocy/public
    [HTTP_X_FORWARDED_FOR] => 192.168.2.1
    [DOCUMENT_URI] => /hdrs.php
    [SERVER_SOFTWARE] => Caddy/v2.6.2
    [SERVER_NAME] => grocy.birb.it
    [REMOTE_HOST] => 192.168.2.1
    [SCRIPT_NAME] => /hdrs.php
    [CONTENT_TYPE] =>
    [AUTH_TYPE] =>
    [FCGI_ROLE] => RESPONDER
    [PHP_SELF] => /hdrs.php
    [REQUEST_TIME_FLOAT] => 1670944509.83
    [REQUEST_TIME] => 1670944509
    [argv] => Array
        (
        )

    [argc] => 0
)

Now, from my VPS via itā€™s VPN link (192.168.222.0/24):

root@birb:/srv# curl -H "Host: grocy.birb.it" 192.168.222.11/hdrs.php
Array
(
    [TEMP] => /tmp
    [TMPDIR] => /tmp
    [TMP] => /tmp
    [PATH] => /usr/local/bin:/usr/bin:/bin
    [HOSTNAME] =>
    [USER] => www
    [HOME] => /home/www
    [HTTP_X_FORWARDED_FOR] => 192.168.222.10
    [SCRIPT_FILENAME] => /sdcard/srv/grocy/public/hdrs.php
    [HTTP_HOST] => grocy.birb.it
    [REQUEST_URI] => /hdrs.php
    [DOCUMENT_URI] => /hdrs.php
    [SERVER_SOFTWARE] => Caddy/v2.6.2
    [QUERY_STRING] =>
    [CONTENT_TYPE] =>
    [HTTP_X_FORWARDED_PROTO] => http
    [SERVER_PORT] => 80
    [REMOTE_USER] =>
    [HTTP_ACCEPT] => */*
    [REQUEST_SCHEME] => http
    [SCRIPT_NAME] => /hdrs.php
    [SERVER_PROTOCOL] => HTTP/1.1
    [REMOTE_PORT] => 42306
    [PATH_INFO] =>
    [CONTENT_LENGTH] => 0
    [HTTP_USER_AGENT] => curl/7.74.0
    [REQUEST_METHOD] => GET
    [GATEWAY_INTERFACE] => CGI/1.1
    [SERVER_NAME] => grocy.birb.it
    [REMOTE_HOST] => 192.168.222.10
    [REMOTE_ADDR] => 192.168.222.10
    [REMOTE_IDENT] =>
    [AUTH_TYPE] =>
    [HTTP_X_FORWARDED_HOST] => grocy.birb.it
    [DOCUMENT_ROOT] => /sdcard/srv/grocy/public
    [FCGI_ROLE] => RESPONDER
    [PHP_SELF] => /hdrs.php
    [REQUEST_TIME_FLOAT] => 1670945586.72
    [REQUEST_TIME] => 1670945586
    [argv] => Array
        (
        )

    [argc] => 0
)

And finally, I added a tiny reverse-proxy block for grocy.birb.it. Accessing this from a totally different server reveals:

root@drachennetz:~# curl -Lv grocy.birb.it/hdrs.php
*   Trying 185.185.127.216:80...
* TCP_NODELAY set
* Connected to grocy.birb.it (185.185.127.216) port 80 (#0)
> GET /hdrs.php HTTP/1.1
> Host: grocy.birb.it
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://grocy.birb.it/hdrs.php
< Server: Caddy
< Date: Tue, 13 Dec 2022 15:37:23 GMT
< Content-Length: 0
<
* Closing connection 0
* Clear auth, redirects to port from 80 to 443Issue another request to this URL: 'https://grocy.birb.it/hdrs.php'
*   Trying 185.185.127.216:443...
* TCP_NODELAY set
* Connected to grocy.birb.it (185.185.127.216) port 443 (#1)
* 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=grocy.birb.it
*  start date: Dec 13 14:34:31 2022 GMT
*  expire date: Mar 13 14:34:30 2023 GMT
*  subjectAltName: host "grocy.birb.it" matched cert's "grocy.birb.it"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  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 0x55db005c8210)
> GET /hdrs.php HTTP/2
> Host: grocy.birb.it
> user-agent: curl/7.68.0
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< alt-svc: h3=":443"; ma=2592000
< content-type: text/html; charset=UTF-8
< date: Tue, 13 Dec 2022 15:37:23 GMT
< server: Caddy
< server: Caddy
< x-powered-by: PHP/8.1.13
< content-length: 1299
<
Array
(
    [TEMP] => /tmp
    [TMPDIR] => /tmp
    [TMP] => /tmp
    [PATH] => /usr/local/bin:/usr/bin:/bin
    [HOSTNAME] =>
    [USER] => www
    [HOME] => /home/www
    [HTTP_X_FORWARDED_FOR] => 192.168.222.10
    [SERVER_PROTOCOL] => HTTP/1.1
    [REQUEST_SCHEME] => http
    [REMOTE_IDENT] =>
    [GATEWAY_INTERFACE] => CGI/1.1
    [REQUEST_URI] => /hdrs.php
    [DOCUMENT_ROOT] => /sdcard/srv/grocy/public
    [HTTP_USER_AGENT] => curl/7.68.0
    [REMOTE_ADDR] => 192.168.222.10
    [CONTENT_TYPE] =>
    [HTTP_ACCEPT] => */*
    [SCRIPT_NAME] => /hdrs.php
    [HTTP_X_FORWARDED_HOST] => grocy.birb.it
    [QUERY_STRING] =>
    [CONTENT_LENGTH] => 0
    [REMOTE_PORT] => 49910
    [PATH_INFO] =>
    [AUTH_TYPE] =>
    [HTTP_ACCEPT_ENCODING] => gzip
    [HTTP_HOST] => grocy.birb.it
    [DOCUMENT_URI] => /hdrs.php
    [REMOTE_USER] =>
    [SERVER_PORT] => 80
    [SCRIPT_FILENAME] => /sdcard/srv/grocy/public/hdrs.php
    [SERVER_SOFTWARE] => Caddy/v2.6.2
    [HTTP_X_FORWARDED_PROTO] => http
    [SERVER_NAME] => grocy.birb.it
    [REQUEST_METHOD] => GET
    [REMOTE_HOST] => 192.168.222.10
    [FCGI_ROLE] => RESPONDER
    [PHP_SELF] => /hdrs.php
    [REQUEST_TIME_FLOAT] => 1670945843.46
    [REQUEST_TIME] => 1670945843
    [argv] => Array
        (
        )

    [argc] => 0
)

As you can see, the X-Forwarded-For header is not set properly - its set to my VPSā€™, but if you ping drachennetz.com, you can tell that this isnā€™t exactly correctā€¦

This is the snippet I inserted for the last test:

grocy.birb.it {
  reverse_proxy * 192.168.222.11:80
}

4. Error messages and/or full log output:

No error messages, only misaligned headers.

5. What I already tried:

I tried to add trusted_proxies 192.168.222.10 to my root config, but that was not correct, caddy validate rejected that. Then I looked at the directive itself in the docs,m and there it is only mentioned as part of the reverse_proxy block. Soā€¦ Do I have to add that to /every/ single reverse proxy setup?..

Thank you!

PS.: My next step is to make VPS and OpenWRT share the same config directory with the latter not auto-renewing and leaving that to the VPS and simply pulling the key bundles from there to allow for HTTPS on the local network with the correct SNI. The router is configured to statically assign *.birb.it to itself. The idea: Route to the router when at home, and go through a reverse proxy/VPN chain when away and only expose select services (Grocy would be one). Once that is working, TOTP and such to harden those exposed services properly. But, for now, I need to get the basics working - and having the correct IP in requests is kinda importantā€¦ ^^ā€™

Thank you and kind regards,
Ingwie!

6. Links to relevant resources:

None

For now, yes.

There is a PR in the works to make it configurable via global options, but itā€™s not merged yet. I havenā€™t had the time to finish it up yet, but I will soon.

2 Likes

Oh boy. Welp, let the copy-pasta begin! xD

Just to clarify:

# On VPS:
domain {
  reverse_proxy * router_via_vpn
}

# On router:
domain {
  trusted_proxies 192.168.222.0/24
  ... other flags ...
}

Yes?

No, trusted_proxies is an option that goes inside of reverse_proxy and/or php_fastcgi.

1 Like

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