# Kvitt (SPEC §10.5.2, D-120, D-126, D-130). NPM, Proxy Host , Advanced tab, Custom Nginx Configuration. # App gate (SPEC §8.5, D-126): outside the home LAN only a request carrying the key built into the app passes; # everything else gets the connection closed (444: nothing sent, nothing to fingerprint). Put the key from # android/local.properties (kvitt.appKey) in place of KVITT_APP_KEY. To rotate, add a second if-line with the new key. # The LAN check reads $realip_remote_addr, the address that really connected: NPM replaces $remote_addr with the # request's own X-Real-IP header for private peers, so $remote_addr could be forged. Let's Encrypt's renewal passes. set $kvitt_gate 0; if ($http_x_kvitt_app = "KVITT_APP_KEY") { set $kvitt_gate 1; } if ($realip_remote_addr ~ "^192\.168\.1\.") { set $kvitt_gate 1; } if ($uri ~ "^/\.well-known/acme-challenge/") { set $kvitt_gate 1; } # Web client (SPEC §15, D-130): its files under /app/, the invite accept it logs in with, and any request carrying a # bearer token (the server checks the token; unknown tokens are rate-limited per IP), except /health, /setup/, /i/ # and /apk/, which stay LAN-or-key only. if ($uri ~ "^/app(/|$)") { set $kvitt_gate 1; } if ($uri = "/invites/accept") { set $kvitt_gate 1; } set $kvitt_bearer 0; if ($http_authorization ~ "^Bearer ") { set $kvitt_bearer 1; } if ($uri ~ "^/(health|setup/|i/|apk/)") { set $kvitt_bearer 0; } if ($kvitt_bearer = 1) { set $kvitt_gate 1; } if ($kvitt_gate = 0) { return 444; } # Server body: 1 MiB, PUT /me/picture 5 MiB (SPEC §8.6); the server answers anything larger with its own 413. client_max_body_size 6m; # No long-poll in the API (SPEC §14.3); 60 s between two reads from the server. proxy_read_timeout 60s; # Response headers (D-117). They live at server level, so no location below may use add_header (nginx would then # drop all of these in it); NPM's HSTS line (SSL tab) sits at the same level. The server's own copy is hidden, so # each header appears once (the landing page sends Referrer-Policy itself). proxy_hide_header Content-Security-Policy; proxy_hide_header X-Content-Type-Options; proxy_hide_header X-Frame-Options; proxy_hide_header Referrer-Policy; # The web client under /app/ (D-132) needs its own scripts, styles, images and API calls: one variable, still one # server-level add_header. set $kvitt_csp "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"; if ($uri ~ "^/app(/|$)") { set $kvitt_csp "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"; } add_header Content-Security-Policy $kvitt_csp always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy no-referrer always; # Replaces NPM's own "location /" (NPM leaves it out when this field has one), whose add_header X-Served-By would # drop the headers above. $forward_scheme, $server and $port are NPM's Details tab values. location / { proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_pass $forward_scheme://$server:$port$request_uri; } # Invite links /i/?from=: neither the code nor the inviter's name may reach a log (SPEC §8.7). NPM's # access log format writes the whole $request_uri, query string included, so this path is not logged at all, and # its error log (which would carry the request line on a 502) goes nowhere. location ^~ /i/ { access_log off; error_log /dev/null crit; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_pass $forward_scheme://$server:$port$request_uri; }