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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
This commit is contained in:
parent
9fd9d1b7d1
commit
aaba14731c
3 changed files with 22 additions and 7 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue