- Delete `.claude/rules/livewire.md` (Livewire is vestigial – only starter-kit auth screens remain) - Add `.claude/rules/frontend.md` documenting Vue 3 SPA stack, router, composables, search-URL mirroring, and Leaflet integration - Add `docs/ops/nginx-log-masking.md` runbook for redacting lat/lng from Nginx access/error logs and PHP-FPM on IONOS VPS - Update `CLAUDE.md` and `architecture.md` to reflect Vue SPA as primary frontend, REST API as backend contract, and Livewire's reduced role - Note stale service names in legacy domain docs (`scoring.md`, `prediction.md`, `notifications.md`) now refactored into `Services/Forecasting/`, `Services/StationSearch/`, and `PlanFeatures`
7.3 KiB
Runbook — Mask lat/lng coordinates in server logs (IONOS VPS)
Goal: stop precise search coordinates (lat/lng) from being written to server
logs, while keeping GET-based shareable search URLs and full debugging value (IP,
status, path, fuel_type all stay readable).
Applies to: the production Nginx + PHP-FPM box on IONOS. None of this lives in the
app repo — it is server config you apply over SSH. The app already stores only a
~1km-rounded location bucket + a SHA-256 IP hash in the searches table; this runbook
covers the raw coordinates that transit the server in the URL.
Scope — what this does and does NOT cover
| Sink | Covered here? |
|---|---|
| Nginx access log | ✅ Step 1 (request line + referer) |
| Nginx error log | ✅ Step 2 (scrub, since log_format can't touch it) |
| PHP-FPM access log | ✅ Step 3 (check / disable) |
| Laravel app logs | ✅ Nothing to do — stock config logs no URL, no error tracker installed |
| IONOS edge (CDN / WAF / managed LB) | ❌ Cannot be redacted from your side — see "Honest limit" |
If you ever add Sentry/Flare/Bugsnag later, they capture the full request URL by default — you'd need a
before_sendscrubber there too.
Step 1 — Mask the Nginx access log
1a. Create /etc/nginx/conf.d/fuel-alert-log-masking.conf
# Redact lat/lng from the access log — request line AND referer.
# Every other field and query param is left intact.
# --- request line (e.g. GET /api/stations?lat=..&lng=.. HTTP/1.1) ---
map $request $fa_req_1 {
default $request;
"~^(?<rqa>.*[?&])lat=[^& ]*(?<rqb>.*)$" "${rqa}lat=***${rqb}";
}
map $fa_req_1 $fa_request_masked {
default $fa_req_1;
"~^(?<rqc>.*[?&])lng=[^& ]*(?<rqd>.*)$" "${rqc}lng=***${rqd}";
}
# --- referer header (e.g. https://fuel-alert.co.uk/?lat=..&lng=..) ---
map $http_referer $fa_ref_1 {
default $http_referer;
"~^(?<rfa>.*[?&])lat=[^&]*(?<rfb>.*)$" "${rfa}lat=***${rfb}";
}
map $fa_ref_1 $fa_referer_masked {
default $fa_ref_1;
"~^(?<rfc>.*[?&])lng=[^&]*(?<rfd>.*)$" "${rfc}lng=***${rfd}";
}
# A copy of the standard "combined" format, but using the masked values.
log_format fuelalert_masked
'$remote_addr - $remote_user [$time_local] '
'"$fa_request_masked" $status $body_bytes_sent '
'"$fa_referer_masked" "$http_user_agent"';
The map blocks must sit in the http {} context. Files in /etc/nginx/conf.d/ are
included there by default — if your setup doesn't include conf.d/*.conf, paste the
block inside http {} in /etc/nginx/nginx.conf instead.
1b. Point the site at the masked format
Find your site's server block and its current access_log line:
sudo nginx -T | grep -nE "server_name|access_log|root" | grep -i fuel
ls /etc/nginx/sites-enabled/ # the site file is usually here
Inside that server { … } block, set:
access_log /var/log/nginx/fuel-alert.access.log fuelalert_masked;
(Keep the path the same as whatever it currently is; only the trailing format name
fuelalert_masked is the change.)
Step 2 — Scrub the Nginx error log
Error-log lines are generated internally by Nginx and do not pass through
log_format, so they can't be masked at write time. They include request: and
referrer: fields that carry the coordinates. Two parts:
2a. Reduce how often request lines are written
In the same server { … } block (or globally), lower the level so routine
warn/error/info entries that embed the request line are dropped:
error_log /var/log/nginx/fuel-alert.error.log crit;
2b. Scrub whatever still gets written
Create /usr/local/bin/scrub-fuel-logs.sh:
#!/bin/sh
# Redact lat/lng values in the Nginx error log. GNU sed (Linux). Verified portable expr.
sed -E -i 's/([?&])(lat|lng)=[^ &"]*/\1\2=***/g' /var/log/nginx/fuel-alert.error.log
sudo chmod +x /usr/local/bin/scrub-fuel-logs.sh
Run it every minute via a systemd timer.
/etc/systemd/system/scrub-fuel-logs.service:
[Unit]
Description=Scrub lat/lng from Nginx error log
[Service]
Type=oneshot
ExecStart=/usr/local/bin/scrub-fuel-logs.sh
/etc/systemd/system/scrub-fuel-logs.timer:
[Unit]
Description=Run lat/lng scrub every minute
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
AccuracySec=10s
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now scrub-fuel-logs.timer
Trade-off: a coordinate from an errored request can sit on disk for up to ~60s before the scrub runs. Error entries are low-volume, so the surface is small but not zero. For a zero-window setup (nothing un-redacted ever hits disk) you'd route
error_logto syslog and scrub at the syslog layer, or tail a RAM-backed raw file with Vector and write only the redacted copy — more setup; ask if you want it.
Step 3 — Check PHP-FPM
grep -nE '^\s*(access\.log|access\.format)' /etc/php/*/fpm/pool.d/*.conf
- No output / commented out → FPM isn't logging requests. Nothing to do.
access.logis enabled → either comment it out, or remove the%r,%q,%Qtokens fromaccess.format, thensudo systemctl reload php*-fpm.
(FPM slowlog and error log record the PHP script/backtrace, not the query string — no action needed there.)
Step 4 — Apply, test, verify
# 1. Validate config BEFORE reloading — catches typos safely.
sudo nginx -t
# If this FAILS, do NOT reload. Fix the error first.
# 2. Reload Nginx (zero downtime).
sudo systemctl reload nginx
# 3. Trigger a real search in the app, then inspect the freshest lines:
sudo tail -n 5 /var/log/nginx/fuel-alert.access.log
sudo tail -n 5 /var/log/nginx/fuel-alert.error.log
# Expect: lat=***&lng=*** — IP, status, path, fuel_type all still present.
Honest limit — IONOS edge
Masking only reaches logs you control (your Nginx, FPM, app). It cannot touch anything IONOS runs in front of the box:
- Plain IONOS VPS / Cloud Server (root server): there is no HTTP-layer edge — your Nginx is the first HTTP hop, and IONOS network logging is L3/L4 (IPs/ports/bytes, no URLs). In this case this runbook covers everything that exists. ✅
- Managed hosting / Deploy Now / CDN / managed WAF or LB: those terminate HTTPS and log full URLs with coordinates, on their retention, and you cannot redact them. Levers: ask IONOS what they log + for how long, or stop putting coords in the URL.
The only way coords never reach any HTTP log (yours or theirs) is to keep them out
of the URL. To preserve shareable links without coords in the URL, use opaque share
tokens: store the search server-side, share /s/<token>, resolve token → coords on
the server. Coords then appear in zero logs and live in one DB row you control
(set its precision + expiry). This is an app change, tracked separately if wanted.
Rollback
- In the site
server {}block, restore the originalaccess_log(drop thefuelalert_maskedname) anderror_loglines. sudo rm /etc/nginx/conf.d/fuel-alert-log-masking.confsudo nginx -t && sudo systemctl reload nginxsudo systemctl disable --now scrub-fuel-logs.timer(optional).