Remove Livewire docs, add Vue SPA frontend docs, document log-masking runbook
- 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`
This commit is contained in:
214
docs/ops/nginx-log-masking.md
Normal file
214
docs/ops/nginx-log-masking.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# 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_send` scrubber there too.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Mask the Nginx access log
|
||||
|
||||
### 1a. Create `/etc/nginx/conf.d/fuel-alert-log-masking.conf`
|
||||
|
||||
```nginx
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```nginx
|
||||
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:
|
||||
|
||||
```nginx
|
||||
error_log /var/log/nginx/fuel-alert.error.log crit;
|
||||
```
|
||||
|
||||
### 2b. Scrub whatever still gets written
|
||||
|
||||
Create `/usr/local/bin/scrub-fuel-logs.sh`:
|
||||
|
||||
```bash
|
||||
#!/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
|
||||
```
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```ini
|
||||
[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`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Run lat/lng scrub every minute
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=1min
|
||||
AccuracySec=10s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```
|
||||
|
||||
```bash
|
||||
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_log` to 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
|
||||
|
||||
```bash
|
||||
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.log` is enabled** → either comment it out, or remove the `%r`, `%q`, `%Q`
|
||||
tokens from `access.format`, then `sudo 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
1. In the site `server {}` block, restore the original `access_log` (drop the
|
||||
`fuelalert_masked` name) and `error_log` lines.
|
||||
2. `sudo rm /etc/nginx/conf.d/fuel-alert-log-masking.conf`
|
||||
3. `sudo nginx -t && sudo systemctl reload nginx`
|
||||
4. `sudo systemctl disable --now scrub-fuel-logs.timer` (optional).
|
||||
Reference in New Issue
Block a user