From f111e5063b36c03eba73e8fd5420978b1ef2e971 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 25 Apr 2026 00:32:33 +0200 Subject: [PATCH] ops: add nginx site config to repo as source-of-truth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live host nginx config (/etc/nginx/sites-enabled/overlord) was not tracked in git, leading to drift. This commit checks in a source-of-truth copy under nginx/overlord.conf with a deploy procedure documented at the top of the file. Includes the proxy_read_timeout/proxy_send_timeout 1d settings for both WebSocket location blocks (/websocket/ and /). Without these, nginx's default 60s timeout drops idle plugin connections in a reconnect loop — the symptom users saw was "WebSocket error … State: Aborted" every ~60s on idle characters. Co-Authored-By: Claude Opus 4.6 (1M context) --- nginx/overlord.conf | 97 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 nginx/overlord.conf diff --git a/nginx/overlord.conf b/nginx/overlord.conf new file mode 100644 index 00000000..1aaaab92 --- /dev/null +++ b/nginx/overlord.conf @@ -0,0 +1,97 @@ +# Nginx site config for overlord.snakedesert.se +# +# Lives on the host (not in the Docker stack) at: +# /etc/nginx/sites-enabled/overlord +# +# This file is the source-of-truth copy committed to git. To deploy a change: +# 1. Edit this file in the repo +# 2. SSH to the host +# 3. sudo cp /home/erik/MosswartOverlord/nginx/overlord.conf /etc/nginx/sites-enabled/overlord +# 4. sudo nginx -t && sudo nginx -s reload +# +# Critical settings: +# - proxy_read_timeout / proxy_send_timeout 1d on /websocket/ and / +# WebSockets are long-lived; nginx's default 60s timeout drops idle clients. +# Removing these timeouts caused all plugin connections to drop every +# ~60s when no data flowed from backend to client (April 2026 incident). +# - Bearer token in /grafana/ proxy_set_header is a Grafana service account +# token used for anonymous panel embeds. Rotate when credentials leak. + +server { + listen 443 ssl; + server_name overlord.snakedesert.se; + + # Security hardening + server_tokens off; + add_header X-Frame-Options SAMEORIGIN always; + add_header X-Content-Type-Options nosniff always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; + + # SSL certificates + ssl_certificate /etc/letsencrypt/live/overlord.snakedesert.se/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/overlord.snakedesert.se/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Plugin WebSocket ingest — `/ws/position` upstream + location /websocket/ { + proxy_pass http://tracker/ws/position; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Plugin-Secret $http_x_plugin_secret; + proxy_cache_bypass $http_upgrade; + # Long-lived WebSocket: don't time out the proxy + proxy_read_timeout 1d; + proxy_send_timeout 1d; + } + + # API endpoints (live, trails, history, stats) — short-lived HTTP + location /api/ { + proxy_pass http://tracker/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_cache_bypass $http_upgrade; + } + + # Frontend UI and browser WebSocket (`/ws/live` upstream) + location / { + proxy_pass http://tracker/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_cache_bypass $http_upgrade; + # Long-lived browser WebSocket (/ws/live): don't time out + proxy_read_timeout 1d; + proxy_send_timeout 1d; + } + + # Grafana Dashboard UI (served under /grafana) + location /grafana/ { + proxy_pass http://grafana; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Authorization "Bearer glsa_AcDTcN5CUX9h5Bi2ipmVAs6g1FRTSIWk_8b81cf99"; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_cache_bypass $http_upgrade; + } +}