Files
buildfor_life_repair/docs/caddy-reverse-proxy.md
T
grabowski dbc140c1f9
Deploy to LXC / deploy (push) Successful in 18s
Fix CSRF 403: override Origin header in Caddy proxy snippet
SvelteKit checks the browser's Origin header, not just Host or
X-Forwarded-Proto. Rewrite Origin to https://collection.newedge.house
so CSRF passes on all non-public routes (NetBird, Yggdrasil, Tor).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:18:56 +07:00

5.7 KiB

Caddy Reverse Proxy Setup

The LXC runs a local Caddy that accepts traffic from all networks and proxies to the SvelteKit app. TLS termination for the public domain happens on a separate upstream Caddy — this instance only handles plain HTTP.

Architecture

                                        ┌─────────────────────────────────┐
Internet ──► Upstream Caddy (TLS) ──────┤                                 │
             collection.newedge.house   │                                 │
                                        │   LXC: Caddy (:80) ──► :3000   │
NetBird  ──► nb-ip:80 ──────────────────┤         (SvelteKit app)        │
                                        │                                 │
Yggdrasil──► [200:...]:80 ──────────────┤                                 │
                                        │                                 │
Tor      ──► .onion ──► tor ──► :8880 ──┤                                 │
                                        └─────────────────────────────────┘

All routes inject Host: collection.newedge.house so SvelteKit CSRF passes.

1. Install Caddy and Tor

# Caddy
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install caddy

# Tor
apt install tor

2. Configure Tor hidden service

Edit /etc/tor/torrc:

HiddenServiceDir /var/lib/tor/bflr/
HiddenServicePort 80 127.0.0.1:8880
systemctl restart tor
cat /var/lib/tor/bflr/hostname
# → your .onion address

3. Get your interface addresses

# Yggdrasil IPv6
yggdrasilctl getSelf
# → 200:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx

# NetBird IP
ip addr show wt0
# → 100.x.x.x (or whatever your NetBird interface is called)

# LAN IP
ip addr show eth0
# → 192.168.x.x

4. Caddyfile

Edit /etc/caddy/Caddyfile:

# ─── Shared snippets ────────────────────────────────────────────────

(proxy) {
	reverse_proxy 127.0.0.1:3000 {
		header_up Host collection.newedge.house
		header_up Origin https://collection.newedge.house
		header_up X-Real-IP {remote_host}
		header_up X-Forwarded-For {remote_host}
		header_up X-Forwarded-Proto https
	}
}

(common) {
	encode gzip
	request_body {
		max_size 50MB
	}
}

# ─── Public traffic (HTTP from upstream Caddy, TLS already terminated) ─

:80 {
	import common
	import proxy
}

# ─── Tor hidden service (Tor connects to localhost:8880) ─────────────

:8880 {
	import common
	import proxy
	bind 127.0.0.1
}

# ─── Yggdrasil (bind to Yggdrasil IPv6 only) ────────────────────────
# Replace with your address from: yggdrasilctl getSelf

http://[200:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:80 {
	import common
	import proxy
}

# ─── NetBird (bind to NetBird interface IP only) ─────────────────────
# Replace with your address from: ip addr show wt0

http://100.x.x.x:80 {
	import common
	import proxy
}

Replace:

  • 200:xxxx:... with your Yggdrasil address
  • 100.x.x.x with your NetBird IP

Note: The :80 block catches all traffic including from the upstream Caddy. The Yggdrasil and NetBird blocks are explicit bindings so Caddy listens on those interfaces. If they conflict with :80, remove the :80 block and add explicit LAN binding instead:

# If you need separate LAN binding instead of catch-all :80
http://192.168.x.x:80 {
	import common
	import proxy
}

5. Upstream Caddy (the TLS termination server)

On your upstream Caddy that handles public TLS, add:

collection.newedge.house {
	reverse_proxy LXC_IP:80 {
		header_up X-Forwarded-Proto https
	}
}

Replace LXC_IP with the LXC's LAN or Tailscale/NetBird IP as seen from the upstream server.

6. App config

The app only listens on localhost:

# /home/bflr/buildfor_life_repair/.env
HOST=127.0.0.1
PORT=3000
ORIGIN=https://collection.newedge.house
BASE_URL=https://collection.newedge.house

systemd service:

Environment=HOST=127.0.0.1
Environment=PORT=3000

7. Start everything

systemctl daemon-reload
systemctl restart bflr
systemctl enable --now caddy
systemctl restart tor

8. Verify each path

# Via upstream (public domain)
curl -s -o /dev/null -w "%{http_code}" https://collection.newedge.house/login

# Direct LAN
curl -s -o /dev/null -w "%{http_code}" http://LXC_LAN_IP/login

# NetBird
curl -s -o /dev/null -w "%{http_code}" http://100.x.x.x/login

# Yggdrasil (from a peer)
curl -s -o /dev/null -w "%{http_code}" http://[200:xxxx:...]/login

# Tor (from Tor Browser)
# http://your-onion-address.onion/login

# Local test
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/login

Troubleshooting

# Caddy
systemctl status caddy
journalctl -u caddy -f
caddy validate --config /etc/caddy/Caddyfile

# Tor
systemctl status tor
cat /var/lib/tor/bflr/hostname

# Yggdrasil
yggdrasilctl getSelf
yggdrasilctl getPeers

# NetBird
netbird status

# App
systemctl status bflr
journalctl -u bflr -f

# Test CSRF manually
curl -H "Host: collection.newedge.house" http://127.0.0.1:3000/login