Caddy reverse proxy sends upstream hostname instead of original Host header #451

Closed
opened 2026-06-17 04:39:50 +00:00 by ldraney · 0 comments
Owner

Type

Bug

Lineage

Discovered during custom domain validation (wave 2/3 of godaddy-tofu DNS deployment). No parent plan.

Repo

ldraney/pal-e-platform

What Broke

landscaping-assistant.app redirects to https://landscaping-assistant.tail5b443a.ts.net/login instead of https://landscaping-assistant.app/login. The Rails app generates redirect URLs using the Host header it receives, but Caddy is sending the upstream Tailscale hostname instead of the original client hostname.

Confirmed by testing from the edge proxy:

  • curl -H 'Host: landscaping-assistant.app' https://landscaping-assistant.tail5b443a.ts.net/location: https://landscaping-assistant.app/login (correct)
  • curl -H 'Host: landscaping-assistant.tail5b443a.ts.net' https://landscaping-assistant.tail5b443a.ts.net/location: https://landscaping-assistant.tail5b443a.ts.net/login (wrong)
  • curl https://landscaping-assistant.app/ through Caddy → location: https://landscaping-assistant.tail5b443a.ts.net/login (wrong — proves Caddy sends upstream host)

Repro Steps

  1. Visit https://landscaping-assistant.app in a browser (unauthenticated)
  2. Observe redirect to https://landscaping-assistant.tail5b443a.ts.net/login instead of https://landscaping-assistant.app/login

Expected Behavior

Caddy should pass the original client Host header (landscaping-assistant.app) to the backend, so the Rails app generates redirect URLs using the custom domain.

Environment

  • Edge proxy: 178.156.129.142 (Hetzner VPS, Caddy + Tailscale)
  • Caddy config: Salt-managed via salt/states/caddy/Caddyfile.j2
  • Affects all sites using HTTPS Tailscale backends (palinks.app works only because it has no auth redirects — the Host header issue exists there too)

Fix

One-line change in salt/states/caddy/Caddyfile.j2 line 18:

{{ site.domain }} {
	reverse_proxy {{ site.proxy_target }}:443 {
		header_up Host {http.request.host}
		transport http {
			tls_server_name {{ site.proxy_target }}
		}
	}
}

The header_up Host {http.request.host} directive tells Caddy to preserve the original client Host header when proxying to the backend. tls_server_name still handles the TLS SNI correctly.

After merging, run salt state.apply caddy on the edge proxy to deploy.

Acceptance Criteria

  • curl -sI https://landscaping-assistant.app/ returns location: https://landscaping-assistant.app/login (not Tailscale URL)
  • curl -sI https://palinks.app/ still returns 200 OK
  • Salt highstate applies cleanly on edge proxy
  • Both domains fully functional end-to-end in browser
  • PR #439 (palinks Caddy config), PR #442 (landscaping Caddy config) — created the template
  • PR #445 — fixed Tailscale hostname in pillar
  • Keycloak redirect URIs already include landscaping-assistant.app (verified via admin API) — not the cause
### Type Bug ### Lineage Discovered during custom domain validation (wave 2/3 of godaddy-tofu DNS deployment). No parent plan. ### Repo ldraney/pal-e-platform ### What Broke `landscaping-assistant.app` redirects to `https://landscaping-assistant.tail5b443a.ts.net/login` instead of `https://landscaping-assistant.app/login`. The Rails app generates redirect URLs using the `Host` header it receives, but Caddy is sending the upstream Tailscale hostname instead of the original client hostname. Confirmed by testing from the edge proxy: - `curl -H 'Host: landscaping-assistant.app' https://landscaping-assistant.tail5b443a.ts.net/` → `location: https://landscaping-assistant.app/login` (correct) - `curl -H 'Host: landscaping-assistant.tail5b443a.ts.net' https://landscaping-assistant.tail5b443a.ts.net/` → `location: https://landscaping-assistant.tail5b443a.ts.net/login` (wrong) - `curl https://landscaping-assistant.app/` through Caddy → `location: https://landscaping-assistant.tail5b443a.ts.net/login` (wrong — proves Caddy sends upstream host) ### Repro Steps 1. Visit `https://landscaping-assistant.app` in a browser (unauthenticated) 2. Observe redirect to `https://landscaping-assistant.tail5b443a.ts.net/login` instead of `https://landscaping-assistant.app/login` ### Expected Behavior Caddy should pass the original client `Host` header (`landscaping-assistant.app`) to the backend, so the Rails app generates redirect URLs using the custom domain. ### Environment - Edge proxy: 178.156.129.142 (Hetzner VPS, Caddy + Tailscale) - Caddy config: Salt-managed via `salt/states/caddy/Caddyfile.j2` - Affects all sites using HTTPS Tailscale backends (palinks.app works only because it has no auth redirects — the Host header issue exists there too) ### Fix One-line change in `salt/states/caddy/Caddyfile.j2` line 18: ```jinja {{ site.domain }} { reverse_proxy {{ site.proxy_target }}:443 { header_up Host {http.request.host} transport http { tls_server_name {{ site.proxy_target }} } } } ``` The `header_up Host {http.request.host}` directive tells Caddy to preserve the original client Host header when proxying to the backend. `tls_server_name` still handles the TLS SNI correctly. After merging, run `salt state.apply caddy` on the edge proxy to deploy. ### Acceptance Criteria - [ ] `curl -sI https://landscaping-assistant.app/` returns `location: https://landscaping-assistant.app/login` (not Tailscale URL) - [ ] `curl -sI https://palinks.app/` still returns 200 OK - [ ] Salt highstate applies cleanly on edge proxy - [ ] Both domains fully functional end-to-end in browser ### Related - PR #439 (palinks Caddy config), PR #442 (landscaping Caddy config) — created the template - PR #445 — fixed Tailscale hostname in pillar - Keycloak redirect URIs already include `landscaping-assistant.app` (verified via admin API) — not the cause
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/pal-e-platform#451
No description provided.