# 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; "~^(?.*[?&])lat=[^& ]*(?.*)$" "${rqa}lat=***${rqb}"; } map $fa_req_1 $fa_request_masked { default $fa_req_1; "~^(?.*[?&])lng=[^& ]*(?.*)$" "${rqc}lng=***${rqd}"; } # --- referer header (e.g. https://fuel-alert.co.uk/?lat=..&lng=..) --- map $http_referer $fa_ref_1 { default $http_referer; "~^(?.*[?&])lat=[^&]*(?.*)$" "${rfa}lat=***${rfb}"; } map $fa_ref_1 $fa_referer_masked { default $fa_ref_1; "~^(?.*[?&])lng=[^&]*(?.*)$" "${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/`, 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).