morphit/ops/bunkerweb/bunkerweb.env.example

177 lines
9.4 KiB
Text

# Morphit — canonical BunkerWeb environment file.
#
# Copy to /etc/bunkerweb/bunkerweb.env and edit the DUMMY-VALUE
# entries before bringing the compose up.
#
# Reference: https://docs.bunkerweb.io/latest/settings/
# ─── Server identity ────────────────────────────────────────────
# DUMMY-VALUE: your instance's public domain (the one with the
# Let's Encrypt cert).
SERVER_NAME=morphit.example.com
SERVER_TYPE=http
MULTISITE=no
# ─── TLS (host-mounted Let's Encrypt cert) ──────────────────────
USE_LETS_ENCRYPT=no
USE_CUSTOM_SSL=yes
CUSTOM_SSL_CERT=/etc/letsencrypt/live/${SERVER_NAME}/fullchain.pem
CUSTOM_SSL_KEY=/etc/letsencrypt/live/${SERVER_NAME}/privkey.pem
# Redirect plain HTTP to HTTPS.
AUTO_REDIRECT_HTTP_TO_HTTPS=yes
# ─── Upstream — everything goes to the morphit frontend container ─
# NOTE: BunkerWeb can't be exercised in the project's CI sandbox,
# so validate this reverse-proxy wiring on a real host (hit the
# site root, then /relay/v1/health, /v1/instance,
# /rss/orderbook.xml) before relying on it. See OPERATIONS.md §32.
#
# BunkerWeb terminates TLS + runs the WAF, then proxies EVERY path
# to the `frontend` nginx container (docker-compose.yml). That
# container serves the SvelteKit static build for page routes AND
# reverse-proxies the API paths (/v1/, /relay/, /rss/, and the
# SSE .../stream paths) to the relay + indexer on the host — see
# ops/bunkerweb/frontend/nginx.conf, which mirrors ops/nginx/web.conf.
# Routing static-serving + SPA fallback + per-path proxy + SSE in one
# nginx is far easier to get right (and matches the bare-metal
# topology) than expressing all of it in BunkerWeb env vars, so the
# only reverse-proxy setting BunkerWeb needs is the single upstream.
# The relay/indexer ports live in frontend/nginx.conf (relay 8080,
# indexer 8081 — the morphit defaults), not here.
USE_REVERSE_PROXY=yes
REVERSE_PROXY_HOST=http://frontend:80
# ─── WAF: OWASP CRS ─────────────────────────────────────────────
USE_MODSECURITY=yes
USE_MODSECURITY_CRS=yes
MODSECURITY_CRS_VERSION=4
# Paranoia 3 is the sweet spot for a public API. Drop to 2 if
# you see real-user false positives in rejection logs. Raise
# to 4 only if you verify it doesn't break legitimate traffic.
MODSECURITY_CRS_PARANOIA=3
# ─── Rate limiting at the WAF layer ─────────────────────────────
# COARSE edge ceiling ONLY — the indexer and relay enforce their own
# finer per-endpoint, per-IP limits (the indexer documents 120 r/m for
# list endpoints and 600 r/m for single-record lookups; both operator-
# tunable). The WAF limit must sit ABOVE those so the app's limiter is
# the binding one and the WAF only catches egregious abuse.
#
# WHY THIS IS GENEROUS (cp231): a single SvelteKit page load fires many
# /v1/* calls near-simultaneously (/v1/instance, /v1/orderbook,
# /v1/orderbook/featured, /v1/listing-fee, /v1/chain-fee, /v1/instances,
# /v1/release, plus the SSE stream). A tight limit here (the old 60 r/m
# = 1 r/s) made that normal burst overflow into 429s, which BunkerWeb's
# bad-behavior counted toward an IP ban — so ordinary browsing banned
# real users. Keep /v1/ well above the indexer's own ceiling, and note
# the BAD_BEHAVIOR_STATUS_CODES below deliberately exclude 429 so a
# transient burst can never escalate to a ban.
USE_LIMIT_REQ=yes
LIMIT_REQ_URL_1=/v1/
LIMIT_REQ_RATE_1=1800r/m
LIMIT_REQ_URL_2=/relay/
LIMIT_REQ_RATE_2=120r/m
# ─── Bad-behavior bans: SPA/PWA + public-API safe (cp231) ───────
# BunkerWeb's bad-behavior counts "bad" response codes per IP and bans
# the IP when the count crosses the threshold inside the window. Its
# DEFAULT counted set (400 401 403 404 405 429 444) is wrong for a site
# that is a SPA + PWA + public read API + RSS + SSE:
# • 429 — the rate limiter's own response; counting it turns a normal
# asset/API burst into a ban (the cp231 incident).
# • 403 — also the response BunkerWeb returns to an already-banned IP,
# so counting it makes a ban SELF-PERPETUATE: every blocked request
# re-arms the counter and the IP never recovers until it goes fully
# silent for the whole window.
# • 404 — a PWA/SPA legitimately probes for assets, the manifest,
# icons, service-worker scopes, etc.; transient 404s are normal.
# So we count only the genuinely-anomalous codes and leave a generous
# threshold. This keeps bad-behavior useful against real abuse without
# banning ordinary visitors or trapping them in a self-feeding ban.
USE_BAD_BEHAVIOR=yes
BAD_BEHAVIOR_STATUS_CODES=400 401 405 444
BAD_BEHAVIOR_THRESHOLD=50
BAD_BEHAVIOR_COUNT_TIME=60
BAD_BEHAVIOR_BAN_TIME=3600
# ─── Anti-bot: referer-none rule on the invite endpoint ─────────
# Per OPERATIONS.md §38.6 item d. Browsers send a Referer
# header; bot scripts often don't. This filters the lazy
# curl-based squatter tier (real attackers bypass with `-H
# 'Referer: https://x.example/'`, but lazy ones don't).
USE_BLOCK_REFERRER_NONE=yes
BLOCK_REFERRER_NONE_URL=/relay/v1/account/invite
# ─── Country block (§38.6 item b) ───────────────────────────────
# Empty by default. Populate ONLY under active attack — Morphit
# serves global users and country-blocking is ethically fraught.
# ISO-3166-1 alpha-2 codes, space-separated.
# BLACKLIST_COUNTRY=
# ─── ASN block (§38.6 item c) ───────────────────────────────────
# Empty by default. Uncomment + populate based on YOUR rejection
# logs (§38.2). Cheap-VPS provider ASNs are over-represented in
# squatter traffic because they're cheap and don't scrutinize
# signups. Real users almost never connect from these ASNs.
# Verify against your own logs before enabling — false positives
# DO exist (rare, but they do).
# BLACKLIST_ASN=AS14061 AS24940 AS16276
# AS14061 — DigitalOcean
# AS24940 — Hetzner
# AS16276 — OVH
# ─── Real-IP forwarding ─────────────────────────────────────────
# BunkerWeb terminates TLS and sets the real client as the leftmost
# X-Forwarded-For entry. Requests then pass BunkerWeb -> frontend
# -> relay, and the frontend APPENDS to X-Forwarded-For (keeping the
# client leftmost), so the relay reads the client from XFF[0]. The
# relay sees the FRONTEND container as its immediate peer, so the
# relay's MORPHIT_RELAY_TRUSTED_PROXY_IPS must include this Docker
# network's CIDR (172.20.0.0/16 per docker-compose.yml) — that one
# CIDR covers BOTH the BunkerWeb and the frontend containers.
USE_REAL_IP=yes
REAL_IP_FROM=0.0.0.0/0
REAL_IP_HEADER=X-Forwarded-For
# ─── Generic hardening ──────────────────────────────────────────
DISABLE_DEFAULT_SERVER=yes
SERVER_TOKENS=off
USE_GZIP=yes
USE_BROTLI=yes
# Active-bot challenge on the invite endpoint. CAPTCHA blocks
# headless bots without WebGL/canvas; real users see it once
# per session.
USE_ANTIBOT=captcha
ANTIBOT_URI=/relay/v1/account/invite
# ─── Security headers ───────────────────────────────────────────
# In this topology BunkerWeb OWNS the response security headers (the
# frontend nginx.conf sets none). These mirror ops/nginx/web.conf so
# both deploy paths deliver the identical policy — keep them in sync.
#
# CONTENT_SECURITY_POLICY is the one you MUST set: BunkerWeb's default
# is `default-src 'self'`, which blocks the in-browser WASM crypto
# (argon2 KDF / signing) and the inline bootstrap — the site loads but
# login / keystore-unlock silently breaks. See docs/OPERATIONS.md §15
# for the directive-by-directive rationale. connect-src lists the six
# default Blurt RPC nodes (apps/web/src/lib/net/config.ts); edit to
# match if you run a different RPC set. NO price API is listed — the
# browser never calls one (price is server-side), so adding it would
# only leak visitor IPs.
CONTENT_SECURITY_POLICY=default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' https://rpc.drakernoise.com https://blurtrpc.dagobert.uk https://rpc.blurt.blog https://rpc.beblurt.com https://rpc.blurt.one https://blurt-rpc.saboin.com; media-src 'none'; object-src 'none'; child-src 'none'; frame-src 'none'; worker-src 'self' blob:; manifest-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'self'
# no-referrer matches Morphit's privacy posture; BunkerWeb's default
# (strict-origin-when-cross-origin) leaks the origin cross-site.
REFERRER_POLICY=no-referrer
# DENY mirrors web.conf. The CSP's frame-ancestors 'none' enforces the
# same thing; this is the legacy belt-and-suspenders header.
X_FRAME_OPTIONS=DENY
# Microphone/geolocation/FLoC off; camera=(self) so the same-origin
# QR-login scanner (getUserMedia) keeps working — camera=() would
# break it. Delivered as a real header (the app.html <meta> form,
# now removed, was ignored by browsers).
PERMISSIONS_POLICY=camera=(self), microphone=(), geolocation=(), interest-cohort=()