From 7521c0188b7e5a4ecf3af80699099c6e2dbe2529 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Wed, 21 Oct 2026 19:12:40 +0200 Subject: [PATCH] infra/npm/kvitt.conf: the NPM block as a file to paste (D-126) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B --- infra/README.md | 3 +++ infra/npm/kvitt.conf | 47 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 infra/npm/kvitt.conf diff --git a/infra/README.md b/infra/README.md index 72bb043..5d76f13 100644 --- a/infra/README.md +++ b/infra/README.md @@ -21,6 +21,7 @@ from the checklist in "Pi: what the owner does". | `dev/secrets/` | Git-ignored: only `fcm-service-account.json`, copied there by `dev/up.sh` and mounted read-only at `/secrets` | | `backup/Dockerfile`, `backup/backup.sh`, `backup/healthcheck.sh` | Backup image and job of SPEC §10.9 (shared with the Pi stack); Drive upload and D-002 deletion | | `pi/` | **The Pi project** (copied to `~/kvitt` on the Kvitt Pi): `compose.yaml`, `.env.example`; git-ignored `pi/.env`, `pi/secrets/`, `pi/out/` | +| `npm/kvitt.conf` | The complete NPM Custom Nginx Configuration block of step 4, as a file to open and paste (placeholder `KVITT_APP_KEY`) | | `build-images.sh` | Mac fallback for the image build: linux/arm64, push to the Gitea registry | | `publish-apk.sh` | Mac: release APK, mapping and `latest.json` into `pi/out/apk/` for the owner to copy to the Pi | | `RESTORE.md` | Restore from a Drive backup, step by step (SPEC §10.9) | @@ -427,6 +428,8 @@ Advanced tab, Custom Nginx Configuration: paste this block exactly (all of it, c place of `KVITT_APP_KEY`. Before the app gate first goes in, do step 4a items 1 to 3 (both phones on an APK that carries the key); until then paste the block without the five lines from `set $kvitt_gate 0;` to `return 444;`. +The same block is the file `infra/npm/kvitt.conf`: open it, replace `KVITT_APP_KEY` with the key from `android/local.properties`, and paste the whole content. The copy here and the copy in SPEC §10.5.2 stay the reference and must match the file. + ```nginx # Kvitt (SPEC §10.5.2, D-120, D-126). 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; diff --git a/infra/npm/kvitt.conf b/infra/npm/kvitt.conf new file mode 100644 index 0000000..5a136a6 --- /dev/null +++ b/infra/npm/kvitt.conf @@ -0,0 +1,47 @@ +# Kvitt (SPEC §10.5.2, D-120, D-126). 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; } +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; +add_header Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" 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; +}