From aaba14731c2d10a84475a25d3f7cac9bce800b0f Mon Sep 17 00:00:00 2001 From: Daniel Maksymilian Syrnicki Date: Wed, 26 Aug 2026 13:21:19 +0200 Subject: [PATCH] fix(poll): resolve the client address via nginx real_ip, never from X-Forwarded-For A client-supplied X-Forwarded-For could mint a fresh device hash per request. nginx now trusts only the edge hop (192.168.178.164) when unwinding the chain and passes the result as X-Real-IP; the service uses nothing else. setup-poll.sh --reset wipes the vote table. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG --- ops/nginx/furtka.org.conf | 8 +++++++- ops/poll/pollsvc.py | 12 +++++++----- ops/poll/setup-poll.sh | 9 ++++++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/ops/nginx/furtka.org.conf b/ops/nginx/furtka.org.conf index 05fe37a..5071e7a 100644 --- a/ops/nginx/furtka.org.conf +++ b/ops/nginx/furtka.org.conf @@ -31,9 +31,15 @@ server { # Poll counter (ops/poll/pollsvc.py) — cookie-free, rate-limited. location /api/poll/ { + # Public traffic arrives from the edge proxy via 192.168.178.164. + # Only that hop is trusted when unwinding X-Forwarded-For, so a + # client-supplied X-Forwarded-For cannot forge a fresh "device". + set_real_ip_from 192.168.178.164; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + limit_req zone=poll burst=10 nodelay; proxy_pass http://127.0.0.1:8090/; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header User-Agent $http_user_agent; proxy_read_timeout 5s; diff --git a/ops/poll/pollsvc.py b/ops/poll/pollsvc.py index 5db53dd..b3f3147 100644 --- a/ops/poll/pollsvc.py +++ b/ops/poll/pollsvc.py @@ -7,8 +7,9 @@ beyond the standard library. GET / -> {"poll": "logo", "counts": {"m": 3, "r": 5}, "total": 8} POST / -> body {"choice": "m"}; answers like GET plus "yours" -A device is identified by a salted SHA-256 of client IP + User-Agent, so -nobody needs a cookie and the raw address is never stored. Voting again +A device is identified by a salted SHA-256 of client IP (as resolved by +nginx from the trusted edge proxy) + User-Agent, so nobody needs a cookie +and the raw address is never stored. Voting again from the same device changes the vote instead of adding one. """ @@ -85,9 +86,10 @@ class Handler(BaseHTTPRequestHandler): log.debug(fmt, *args) def _client_ip(self) -> str: - xff = self.headers.get("X-Forwarded-For") - if xff: - return xff.split(",")[0].strip() + # nginx resolves the trusted proxy chain (real_ip module, see + # ops/nginx/furtka.org.conf) and hands us the result as X-Real-IP. + # We never parse X-Forwarded-For here: its leading entries are + # whatever the client chose to send. return self.headers.get("X-Real-IP") or self.client_address[0] def _device(self) -> str: diff --git a/ops/poll/setup-poll.sh b/ops/poll/setup-poll.sh index f1ecd14..a7ccd85 100755 --- a/ops/poll/setup-poll.sh +++ b/ops/poll/setup-poll.sh @@ -2,10 +2,17 @@ # Install / update the poll counter on forge-runner-01. Idempotent. # # Usage (on the VM, with sudo): -# sudo ops/poll/setup-poll.sh +# sudo ops/poll/setup-poll.sh # install / update +# sudo ops/poll/setup-poll.sh --reset # ... and wipe all votes set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" +if [ "${1:-}" = "--reset" ]; then + systemctl stop furtka-poll 2>/dev/null || true + rm -f /var/lib/furtka-poll/votes.db + echo "votes wiped" +fi + install -d -m 0755 /opt/furtka-poll install -m 0644 "$HERE/pollsvc.py" /opt/furtka-poll/pollsvc.py install -m 0644 "$HERE/furtka-poll.service" /etc/systemd/system/furtka-poll.service