fix(poll): resolve the client address via nginx real_ip, never from X-Forwarded-For
All checks were successful
CI / lint (push) Successful in 33s
CI / test (push) Successful in 2m7s
CI / validate-json (push) Successful in 25s
CI / markdown-links (push) Successful in 16s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
This commit is contained in:
Daniel Maksymilian Syrnicki 2026-08-26 13:21:19 +02:00
parent 9fd9d1b7d1
commit aaba14731c
3 changed files with 22 additions and 7 deletions

View file

@ -31,9 +31,15 @@ server {
# Poll counter (ops/poll/pollsvc.py) — cookie-free, rate-limited. # Poll counter (ops/poll/pollsvc.py) — cookie-free, rate-limited.
location /api/poll/ { 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; limit_req zone=poll burst=10 nodelay;
proxy_pass http://127.0.0.1:8090/; 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 X-Real-IP $remote_addr;
proxy_set_header User-Agent $http_user_agent; proxy_set_header User-Agent $http_user_agent;
proxy_read_timeout 5s; proxy_read_timeout 5s;

View file

@ -7,8 +7,9 @@ beyond the standard library.
GET /<poll> -> {"poll": "logo", "counts": {"m": 3, "r": 5}, "total": 8} GET /<poll> -> {"poll": "logo", "counts": {"m": 3, "r": 5}, "total": 8}
POST /<poll> -> body {"choice": "m"}; answers like GET plus "yours" POST /<poll> -> body {"choice": "m"}; answers like GET plus "yours"
A device is identified by a salted SHA-256 of client IP + User-Agent, so A device is identified by a salted SHA-256 of client IP (as resolved by
nobody needs a cookie and the raw address is never stored. Voting again 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. from the same device changes the vote instead of adding one.
""" """
@ -85,9 +86,10 @@ class Handler(BaseHTTPRequestHandler):
log.debug(fmt, *args) log.debug(fmt, *args)
def _client_ip(self) -> str: def _client_ip(self) -> str:
xff = self.headers.get("X-Forwarded-For") # nginx resolves the trusted proxy chain (real_ip module, see
if xff: # ops/nginx/furtka.org.conf) and hands us the result as X-Real-IP.
return xff.split(",")[0].strip() # 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] return self.headers.get("X-Real-IP") or self.client_address[0]
def _device(self) -> str: def _device(self) -> str:

View file

@ -2,10 +2,17 @@
# Install / update the poll counter on forge-runner-01. Idempotent. # Install / update the poll counter on forge-runner-01. Idempotent.
# #
# Usage (on the VM, with sudo): # 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 set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)" 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 -d -m 0755 /opt/furtka-poll
install -m 0644 "$HERE/pollsvc.py" /opt/furtka-poll/pollsvc.py install -m 0644 "$HERE/pollsvc.py" /opt/furtka-poll/pollsvc.py
install -m 0644 "$HERE/furtka-poll.service" /etc/systemd/system/furtka-poll.service install -m 0644 "$HERE/furtka-poll.service" /etc/systemd/system/furtka-poll.service