Compare commits

..

2 Commits

Author SHA1 Message Date
fad417697a Merge branch 'fix/proxy-forwarded-ips' into dev: trust X-Forwarded-* from off-host Caddy 2026-04-22 13:08:01 -05:00
5aad7fd48f fix: trust X-Forwarded-* when Caddy is on another host
Site was loading under https but Starlette built logo/static URLs as
http, tripping CSP `img-src 'self'`. Root cause: Dockerfile hardcoded
`--forwarded-allow-ips 127.0.0.1`, but with Caddy on a separate host
(10.10.99.10) reverse-proxying to this VM, the container sees the
request's source IP as the Docker bridge gateway (~172.17.0.1), not
Caddy's real IP — uvicorn discarded X-Forwarded-Proto and fell back
to scheme=http.

Fixes both now and later:

- Dockerfile CMD switched to `sh -c exec uvicorn ... --forwarded-allow-ips
  "${FORWARDED_ALLOW_IPS:-127.0.0.1}"`. PID-1 / signal behaviour preserved
  via exec. Default stays 127.0.0.1 so host-uvicorn dev is unchanged;
  prod sets FORWARDED_ALLOW_IPS=* via .env or compose `command:`.
- docker-compose.prod.yml gets an explicit `command:` override so the
  currently-deployed image (with the hardcoded 127.0.0.1 flag) can be
  fixed without waiting for a rebuild. Also corrected the port binding
  comment + default: Caddy is off-host, so the port must be reachable
  over the LAN — restrict with host firewall / OPNsense instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:07:54 -05:00
2 changed files with 35 additions and 11 deletions

View File

@@ -98,10 +98,14 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
# Run Uvicorn directly. --proxy-headers + --forwarded-allow-ips make # Run Uvicorn directly. --proxy-headers + --forwarded-allow-ips make
# Starlette's ProxyHeadersMiddleware trust X-Forwarded-* only from the # Starlette's ProxyHeadersMiddleware trust X-Forwarded-* only from the
# listed peer IPs (Caddy on the host). No --reload: this is a prod-shape # listed peer IPs. The trusted-IP value is env-driven so the image
# image; local hot-reload is a dev concern and runs outside Docker. # can be reused across topologies:
CMD ["uvicorn", "app.main:app", \ # - local: defaults to 127.0.0.1 (when running uvicorn on the host)
"--host", "0.0.0.0", \ # - docker/compose behind Caddy: set FORWARDED_ALLOW_IPS="*" in .env
"--port", "8080", \ # because the container's source IP is the bridge gateway, not
"--proxy-headers", \ # 127.0.0.1. Safe because the host only binds 127.0.0.1:8080 so
"--forwarded-allow-ips", "127.0.0.1"] # nothing off-host can reach uvicorn directly.
# `sh -c exec` keeps uvicorn as PID 1 so SIGTERM still triggers a
# graceful shutdown (exec form was fine before, but we need shell
# expansion for ${FORWARDED_ALLOW_IPS}).
CMD ["sh", "-c", "exec uvicorn app.main:app --host 0.0.0.0 --port 8080 --proxy-headers --forwarded-allow-ips \"${FORWARDED_ALLOW_IPS:-127.0.0.1}\""]

View File

@@ -27,11 +27,31 @@ services:
pull_policy: always pull_policy: always
env_file: env_file:
- .env - .env
# Override the Dockerfile CMD so uvicorn trusts X-Forwarded-* headers.
# Caddy lives on another server (10.10.99.10) and speaks HTTP to this
# VM on port 8080. Even so, the source IP *inside* the container is
# the Docker bridge gateway (typically 172.17.0.1), NOT 10.10.99.10,
# because Docker NATs the inbound connection. That means allowlisting
# 10.10.99.10 would never match — uvicorn would still drop X-Forwarded-*
# and Starlette would build http:// URLs under an https:// page,
# tripping `img-src 'self'` CSP on the logo, fonts, etc.
#
# "*" is acceptable here because access to port 8080 is controlled
# at the network layer (host firewall / VLAN) — only the Caddy box
# can reach it. If you later move Caddy onto this same host, change
# this back to a specific gateway IP.
command: >
uvicorn app.main:app
--host 0.0.0.0
--port 8080
--proxy-headers
--forwarded-allow-ips *
ports: ports:
# Bind to loopback only. Caddy (on the host) proxies inbound 443 here. # Caddy on 10.10.99.10 reverse-proxies to <this-vm>:8080. Binding
# Exposing on 0.0.0.0 would let anything on the LAN hit uvicorn # on all interfaces keeps the compose portable; lock down access
# directly and bypass TLS termination + rate limits. # with the VM's host firewall (nftables / ufw) or upstream
- "127.0.0.1:8080:8080" # (OPNsense) to only permit 10.10.99.10 → :8080.
- "8080:8080"
volumes: volumes:
# SQLite DB + media uploads live on the host so container rebuilds / # SQLite DB + media uploads live on the host so container rebuilds /
# image rolls don't wipe content. The container runs as uid 10001; # image rolls don't wipe content. The container runs as uid 10001;