Attach HTTPS deliberately, with narrow limits and minimal logs.
This runbook turns the public edge into a controlled handoff: the Rust app remains private, TLS terminates at Caddy or nginx, request bodies stay tiny, detailed diagnostics remain local, and abuse controls are described without pretending body caps are a WAF.
Files in this slice
| File | Purpose | Rule |
|---|---|---|
deploy/reverse-proxy/Caddyfile.example | Caddy HTTPS edge template with body caps, no persistent access log, one private upstream, and an explicit stock-Caddy rate-limit boundary. | Copy and replace/export placeholders on the VPS; do not commit live config. Supply a separately validated trusted-edge throttle before widening. |
deploy/reverse-proxy/nginx.nocturne.conf.example | nginx http-context template with TLS placeholders, body caps, media buffering stance, no access log, and memory-only sensitive-POST request buckets. | Include from nginx http context after replacing placeholders; validate and tune the baseline from private-test evidence. |
make reverse-proxy-template-check | Dependency-free structural check that the copy-safe templates still carry the intended privacy and limit markers. | Run locally before editing or shipping template changes. |
Edge behavior to preserve
- Only the HTTPS proxy owns public ports 80/443. The app, PostgreSQL, Redis, backups, shell, and private media root stay unreachable from the public internet.
- Run
production-preflightcleanly before connecting the proxy and again after proxy attachment. - Keep default request bodies at or below
16 KiB; keep public/reportat or below8 KiB. - Forward only routing context needed by the app:
Host,X-Forwarded-Proto=https, andX-Forwarded-Host. The app does not need raw client IP or user-agent identity for the current MVP. - Keep public
/healthzshallow. Use localdoctorandproduction-preflightfor database, migration, backup, checkout, and media diagnostics. - Disable persistent access logs by default. If an incident requires logs, define fields, retention, access controls, and deletion date before enabling them.
- Before widening private testing, throttle anonymous sensitive POST routes at the trusted edge. Keep counters bounded and ephemeral, do not forward source identifiers to the app, and assess shared-NAT false positives.
Caddy deployment sketch
sudo cp deploy/reverse-proxy/Caddyfile.example /etc/caddy/Caddyfile
sudoedit /etc/caddy/Caddyfile
# If keeping the env-placeholder style:
export NOCTURNE_PUBLIC_DOMAIN=members.final-domain.example
export NOCTURNE_UPSTREAM=127.0.0.1:3000
caddy fmt --diff /etc/caddy/Caddyfile
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl reload caddy
Caddy is the lower-ops option when automatic certificate management fits the VPS. The template uses request_body caps, discards access logs by default, and proxies every public route to the private app upstream.
request_body directive, do not remove body limits silently. Upgrade Caddy, use nginx, or enforce equal/stricter body limits at another trusted edge layer.nginx deployment sketch
sudo cp deploy/reverse-proxy/nginx.nocturne.conf.example \
/etc/nginx/conf.d/nocturne.conf
sudoedit /etc/nginx/conf.d/nocturne.conf
sudo nginx -t
sudo systemctl reload nginx
nginx is the explicit-control option. The example uses client_max_body_size 16k globally, 8k for /report, no persistent access log, and proxy_buffering off for /media/. It also applies bounded shared-memory source buckets only to POST /report, POST /join, POST /login, and POST /owner/login, returns 429 when exceeded, forwards no client-IP header, and loses counters on restart.
After proxy attachment
Prefer the scripted harness once the final origin resolves externally:
scripts/external-edge-smoke.sh https://members.final-domain.example
The manual commands below mirror the same intent when debugging a failed smoke step. The full public-edge sequence lives in External edge smoke; run smoke-member-provision --recovery-key-file ..., then External member smoke, then smoke-member-cleanup --confirm-smoke-member-cleanup to prove authenticated media and checkout paths without leaving active smoke access behind.
# Shallow public health only.
curl -fsS https://members.final-domain.example/healthz
# Confirm docs are reachable but deep diagnostics are not public routes.
curl -fsSI https://members.final-domain.example/docs/
curl -fsSI https://members.final-domain.example/doctor || true
# Oversized public report body should be rejected by the edge or app.
python3 - <<'PY' >/tmp/nocturne-report-oversize.txt
print('a' * 9000)
PY
curl -sS -o /tmp/nocturne-report-response.txt \
-w '%{http_code}\n' \
-X POST --data-binary @/tmp/nocturne-report-oversize.txt \
https://members.final-domain.example/report
# Then run locally on the VPS with production env loaded.
set -a; . ./.env.production; set +a
nocturne-platform production-preflight
- Expected
/healthz: plainok. - Expected missing diagnostic route:
404, not database or filesystem detail. - Expected oversized report:
413from proxy or app. - Expected preflight: zero failures before any broader invite/private test.
Do not continue external testing if any are true
- The app, Postgres, Redis, backups, or private media root are reachable directly from the internet.
- Proxy template placeholders remain in the live config.
NOCTURNE_PUBLIC_ORIGINdoes not exactly match the final HTTPS origin.- Body limits are missing, larger than the app limits, or untested.
- The selected edge has no validated sensitive-POST throttle: nginx limits were removed, or stock Caddy has no separately validated trusted-edge equivalent.
- Rate-limit testing causes unacceptable shared-NAT false positives, exposes source identifiers in evidence, or has not confirmed
429behavior. - Access logs are enabled without a documented incident purpose, retention window, and deletion date.
- Browser login, member library, authenticated media byte ranges, public report intake, owner gate posture, disposable smoke-member cleanup, and
production-preflighthave not been smoke-tested after proxy attachment.