Repository navigation
fix: give nginx and Discourse the visitor's real IP; 2026.9.0-latest:2 → 2026.9.0-latest:3 - #6
Conversation
helix-a
left a comment
There was a problem hiding this comment.
The real-IP mechanism is sound and the generated nginx configuration validates, but the trusted source range is broader than the reverse proxy and lets sibling service containers forge the identity this change is intended to protect. Please narrow it to the two bridge gateway addresses.
Verified locally with npm ci, npm run check, npm run build, make (x86_64 + aarch64), and nginx -t against the pinned Discourse image.
| /** The image's nginx overwrites `X-Forwarded-For` with its peer — the OS proxy on the container bridge — unless the bridge is trusted. */ | ||
| const REAL_IP_CONF = [ | ||
| 'set_real_ip_from 10.0.3.0/24;', | ||
| 'set_real_ip_from fd00:3::/64;', |
There was a problem hiding this comment.
[P1] Trust only the OS proxy, not every service container. All package containers sit in these bridge subnets, and <pkg>.startos resolves to a package's container address, so another installed/compromised service can connect directly to Discourse and supply an arbitrary X-Forwarded-For. With the whole /24 and /64 trusted, nginx accepts that spoof as $remote_addr, corrupting exactly the rate limits, login history, and IP moderation records this patch repairs. The reverse proxy originates its backend connections from the bridge gateways, so use set_real_ip_from 10.0.3.1; and set_real_ip_from fd00:3::1; instead.
There was a problem hiding this comment.
Agreed — narrowed to 10.0.3.1 and fd00:3::1 in d46d826, README updated to say why the subnets are not trusted.
There was a problem hiding this comment.
Verified at d46d826: the trust is now limited to the two gateway addresses, the README captures the boundary, and the generated config passes nginx -t against the pinned image. This resolves my finding.
…2 → 2026.9.0-latest:3 The image's nginx sets `proxy_set_header X-Forwarded-For $remote_addr`, so behind the OS reverse proxy every request reaches its `limit_req`/`limit_conn` zones and Rails as the bridge gateway, 10.0.3.1. Measured on a dev box: 66 of 80 parallel page requests answered 429, and every login was recorded from 10.0.3.1. `prepare-app` now drops an outlet in `conf.d/outlets/before-server/` that trusts the two bridge gateways the proxy connects from (10.0.3.1, fd00:3::1) with `real_ip_header X-Forwarded-For`, which the OS proxy sets to the client and strips from the client. Only the gateways: sibling containers share the bridge and can reach this port directly, so a subnet-wide trust would let one forge the header. Verified after the change: nginx logs and `user_auth_tokens.client_ip` both carry the public client address. Documents restoring a Discourse backup archive onto the package (verified with a real 400 MB archive from a launcher install): the base-URL remap makes the primary URL a prerequisite, and the restorer disables non-staff email. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b19a835 to
d46d826
Compare
Behind the OS reverse proxy, every visitor reached the image's nginx and Rails as one IP.
The defect
discourse/discourse's nginx setsproxy_set_header X-Forwarded-For $remote_addr, overwriting the client address StartOS puts in that header with its own peer — the bridge gateway,10.0.3.1. Everything downstream keyed on the client IP therefore saw a single client:limit_reqzones (flood12 r/s,bot200 r/min) andlimit_conn connperip 20, which the image ships enabled. Measured on a dev box with real forum data: 66 of 80 parallel page requests answered 429. Under any real concurrency the forum rate-limits itself as one user.user_auth_tokens.client_ip, login history, screened IPs, the admin's "users by IP" views.The fix
prepare-appdropsconf.d/outlets/before-server/10-startos-real-ip.conf— an outlet the image'sdiscourse.confalready includes inhttpcontext — trusting the two bridge gateways the OS proxy connects from (10.0.3.1,fd00:3::1, perlxc-net's config in the OS build) withreal_ip_header X-Forwarded-For. The OS proxy sets that header to the client and strips any client-supplied copy, so$remote_addris the visitor again before the rate limiter runs and before nginx re-emits it to Rails. Only the gateways are trusted, not the bridge subnets: sibling package containers share the bridge and can reach this port directly, so a subnet-wide trust would let one forge the header.Verified after the change on the same box: nginx's access log and a fresh
user_auth_tokens.client_ipboth carry the public client address.Also documented
Restoring a Discourse backup archive onto the package — the path an existing forum takes to move here — verified with a real 400 MB archive from a
launcherinstall: where the archive goes, thatallow_restoregates it, that the restorer remaps the backup's base URL to the current primary URL (so the primary URL is a prerequisite, not a follow-up), and that it disables non-staff email. README,instructions.md.Verified on a dev box
Install of
:2from prod, alauncherarchive restored, then this build sideloaded over it as an update: healthy, data intact, outlet present in the container, real IP end to end.🤖 Generated with Claude Code