From 5488de3383d33d8a037587dd9112d36ea035c465 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Fri, 22 May 2026 13:32:01 +0200 Subject: [PATCH] fix(nginx): honour outer X-Forwarded-Proto when behind a reverse proxy (#547) When PicPeak runs behind NPM / Traefik / Caddy, the inner nginx receives plain HTTP from the outer proxy. The previous `X-Forwarded-Proto $scheme` therefore always forwarded "http" to the backend, even when the public URL was HTTPS. Express has `trust proxy` enabled for loopback/linklocal, so req.secure became false, the Secure cookie flag wasn't set, and generated URLs (cookies, tokens) used http://. Add a top-of-file `map` block that picks the incoming X-Forwarded-Proto when present and falls back to `$scheme` for direct access. Applied to both nginx.conf (bundled production image) and nginx.dev.conf. Validated with `nginx -t` against nginx:1.28-alpine (the same image used by Dockerfile.prod / Dockerfile). --- frontend/nginx.conf | 27 +++++++++++++++++++-------- frontend/nginx.dev.conf | 11 +++++++++-- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 6b7765e9..9a16f008 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,3 +1,14 @@ +# Honour the outer reverse proxy's X-Forwarded-Proto when present (e.g. NPM, +# Traefik, Caddy in front of PicPeak). Falls back to nginx's own $scheme when +# the header is absent (direct access / no outer proxy). Without this the +# inner nginx was always forwarding "http" to the backend because the outer +# proxy → inner nginx hop is plain HTTP, breaking Secure cookies and HTTPS +# URL generation in the backend. See issue #547. +map $http_x_forwarded_proto $real_proto { + default $http_x_forwarded_proto; + "" $scheme; +} + server { listen 80; server_name localhost; @@ -80,7 +91,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; proxy_cache_bypass $http_upgrade; proxy_read_timeout 86400; @@ -97,7 +108,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; # Cache photos proxy_cache_valid 200 302 1d; @@ -112,7 +123,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; # Cache thumbnails proxy_cache_valid 200 302 7d; @@ -128,7 +139,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; # Cache uploads proxy_cache_valid 200 302 7d; @@ -146,7 +157,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; # Fonts rarely change; cache aggressively (matches backend Cache-Control). proxy_cache_valid 200 302 7d; @@ -161,7 +172,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; } # Delegate root requests to backend for public landing page handling @@ -175,7 +186,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; proxy_read_timeout 60s; } @@ -203,7 +214,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; } # SPA fallback diff --git a/frontend/nginx.dev.conf b/frontend/nginx.dev.conf index 07692ea1..7506f943 100644 --- a/frontend/nginx.dev.conf +++ b/frontend/nginx.dev.conf @@ -1,3 +1,10 @@ +# Honour outer reverse-proxy's X-Forwarded-Proto when present (see #547 / +# frontend/nginx.conf for full rationale). +map $http_x_forwarded_proto $real_proto { + default $http_x_forwarded_proto; + "" $scheme; +} + server { listen 80; server_name localhost; @@ -10,7 +17,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; } # Photos proxy to backend @@ -41,7 +48,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $real_proto; } # SPA fallback