Phase 0: repo skeleton, image, compose stacks, doctor, migration runner, frontend shell
New repo for the Packet Station rebuild (Python 3.12, FastAPI, PostgreSQL 16, React, VisualKit, MFilesKit, Docker Compose on docker-1). Nothing is migrated from the M: install. - CLAUDE.md with the safety rules, README, CHANGELOG, docs (BRIEF, PLAN, SECURITY rewritten for the server with the requirement ID table, DEPLOY, stubs for FEED, POLICY, SCHEMA, GO-LIVE, PAPER-FALLBACK). - packetstation CLI: app, worker (heartbeat stub), migrate, doctor, version, healthcheck. Settings from the environment with secret files. Stack guard (test never points at VISUAL production). Security headers and CSP. Trusted proxy resolved by name. JSON logs with redaction. - Migration runner and 0001_base: settings, append-only audit_events with hash chain, ps_notify, grants for ps_app. - Multi-stage Dockerfile (ODBC Driver 18, PESCO libraries from the Gitea index only), compose base, proxy overlay for npm_default, CI overlay, postgres initdb roles, env examples, NPM proxy host notes, Gitea Actions. - Frontend: Vite, React, TypeScript, the PESCO dark theme ported with self-hosted fonts, placeholder sign-in and home. - Verified on docker-1: image builds, CI stack comes up healthy, doctor database checks pass, 77 server tests pass against PostgreSQL. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# Nginx Proxy Manager proxy hosts
|
||||
|
||||
NPM runs on docker-1 as container `npm_app_1` on network `npm_default` (compose project in
|
||||
`/home/spencerm/npm`). It terminates HTTPS for both Packet Station stacks. The stacks publish
|
||||
no port. Each app joins `npm_default` through `deploy/compose.proxy.yaml` with its own alias,
|
||||
and NPM forwards to that alias.
|
||||
|
||||
Make one proxy host per stack in the NPM web UI.
|
||||
|
||||
| | Production | Test |
|
||||
|---|---|---|
|
||||
| Domain name | `packetstation.pescoinc.biz` | `packetstation-test.pescoinc.biz` |
|
||||
| Forward hostname | `packetstation-app` | `packetstation-test-app` |
|
||||
| `APP_ALIAS` in the stack env file | `packetstation-app` | `packetstation-test-app` |
|
||||
|
||||
## Details tab
|
||||
|
||||
- Scheme: `http`
|
||||
- Forward hostname / IP: the alias from the table above. Never an IP address: container
|
||||
addresses change when a stack is recreated.
|
||||
- Forward port: `8000`
|
||||
- Cache assets: off. The app sets its own cache headers.
|
||||
- Block common exploits: on. Confirm it does not reject uploads when the test proxy host is made
|
||||
(plan open question 16).
|
||||
- Websockets support: off. Live updates use server-sent events, not websockets.
|
||||
- Access list: none. Sign-in is done by the app.
|
||||
|
||||
## SSL tab
|
||||
|
||||
- SSL certificate: the PESCO certificate for the name.
|
||||
- Force SSL: on.
|
||||
- HTTP/2 support: on.
|
||||
- HSTS enabled: off. The app sends `Strict-Transport-Security` itself, along with CSP and the
|
||||
other security headers. Two HSTS headers with different values would conflict.
|
||||
- HSTS subdomains: off.
|
||||
|
||||
## Advanced tab
|
||||
|
||||
Paste this block as is for both hosts:
|
||||
|
||||
```nginx
|
||||
client_max_body_size 30m;
|
||||
|
||||
location /api/events {
|
||||
proxy_pass $forward_scheme://$server:$port;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
```
|
||||
|
||||
Why each part is there:
|
||||
|
||||
- `client_max_body_size 30m`: drawings, MTRs and reports are uploaded through the app. NPM's
|
||||
default is 1 MB at the server level. The app enforces its own, smaller per-type limits.
|
||||
- `location /api/events`: the server-sent events stream. Each open screen holds one request
|
||||
open for up to an hour. Buffering must be off or events arrive late in batches. The app also
|
||||
sends `X-Accel-Buffering: no` on this response.
|
||||
- `proxy_http_version 1.1` with an empty `Connection` header keeps the upstream connection open
|
||||
for the stream.
|
||||
- The three `proxy_set_header` lines repeat what NPM's own `location /` adds. A custom location
|
||||
does not inherit NPM's proxy include, so without them the app would see NPM's address as the
|
||||
client and the wrong scheme.
|
||||
|
||||
### Why `$server` and not the alias by name
|
||||
|
||||
NPM writes `set $forward_scheme http; set $server "<forward hostname>"; set $port 8000;` into
|
||||
each proxy host's server block, and its own `location /` uses `proxy_pass` with those variables.
|
||||
When `proxy_pass` contains a variable, nginx resolves the name at request time through the
|
||||
`resolver` NPM configures (Docker's DNS at 127.0.0.11). A literal
|
||||
`proxy_pass http://packetstation-app:8000;` is resolved once when nginx loads its config. That
|
||||
breaks in two ways: nginx refuses to load the config if the stack is down at that moment, which
|
||||
takes every other proxy host down with it, and after the stack is recreated the app gets a new
|
||||
address while nginx keeps sending to the old one. Using `$forward_scheme://$server:$port` keeps
|
||||
the events location on the same name and the same live resolution as the rest of the host.
|
||||
|
||||
## Trust of X-Forwarded-*
|
||||
|
||||
The app trusts `X-Forwarded-For` and `X-Forwarded-Proto` only from `npm_app_1`'s own address
|
||||
(`TRUSTED_PROXY_HOST=npm_app_1`), resolved at start and re-resolved every 60 seconds so an NPM
|
||||
restart is followed. It does not trust the whole `npm_default` subnet (172.22.0.0/16): other
|
||||
stacks share that network, and the per-address sign-in rate limit depends on the real client
|
||||
address. A request from any other address has its forwarded headers ignored. uvicorn runs
|
||||
without `--proxy-headers`; the app does this check itself.
|
||||
|
||||
## Check after saving
|
||||
|
||||
From a PC on the LAN:
|
||||
|
||||
1. `https://<name>/` loads the sign-in page with a valid certificate.
|
||||
2. The response headers show one `Strict-Transport-Security` header, from the app.
|
||||
3. Open a packet screen and leave it for five minutes. A change made in another browser shows up
|
||||
without a reload, so the events stream is not being buffered or cut.
|
||||
4. Upload a 20 MB PDF on the test stack. It is accepted (Block common exploits did not reject it).
|
||||
Reference in New Issue
Block a user