morphit/ops/ansible/roles/bunkerweb/templates/bunkerweb.env.j2
2026-08-10 19:33:10 -07:00

137 lines
7 KiB
Django/Jinja

# Managed by Ansible — BunkerWeb configuration.
# Reference: https://docs.bunkerweb.io/latest/settings/
# ─── Server identity ────────────────────────────────────────────
SERVER_NAME={{ morphit_domain }}
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/{{ morphit_domain }}/fullchain.pem
CUSTOM_SSL_KEY=/etc/letsencrypt/live/{{ morphit_domain }}/privkey.pem
# Redirect plain HTTP to HTTPS.
AUTO_REDIRECT_HTTP_TO_HTTPS=yes
# ─── Scheduler <-> instance API ─────────────────────────────────
# The scheduler runs in a SEPARATE container and must be told the
# instance by name; without BUNKERWEB_INSTANCES it falls back to
# Docker-socket autoconf. The instance's API defaults to trusting
# only 127.0.0.0/8, but the scheduler reaches it from another
# container on this network's bridge (172.20.0.0/16 per
# docker-compose.yml), so that CIDR MUST be in API_WHITELIST_IP or the
# instance resets every config push ("Sending nginx configs failed").
# Both were missing on the first real install (cp663 #9/#11).
BUNKERWEB_INSTANCES=bunkerweb
API_WHITELIST_IP=127.0.0.0/8 172.20.0.0/16
# ─── Upstream — everything goes to the morphit frontend container ─
# NOTE: validate this reverse-proxy wiring on a real host (it
# can't run in the project's CI sandbox) — see OPERATIONS.md §32.
# BunkerWeb proxies EVERY path to the `frontend` nginx container,
# which serves the SvelteKit static build AND reverse-proxies the
# API paths (/v1/, /relay/, /rss/, SSE) to the relay + indexer on the
# host. See ops/bunkerweb/frontend/nginx.conf (mirrors web.conf);
# the relay/indexer ports live there, 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
MODSECURITY_CRS_PARANOIA={{ bunkerweb_crs_paranoia }}
# ─── Rate limiting at the WAF layer — COARSE ceiling only; the
# indexer/relay enforce finer per-endpoint per-IP limits ───────
# Must sit ABOVE the indexer's own limits (120 r/m list / 600 r/m
# single-record) so the app's limiter binds first. The old 60 r/m
# (1 r/s) was tighter than a normal SvelteKit page-load burst of
# /v1/* calls, producing 429s that bad-behavior counted into IP bans
# (cp231). BAD_BEHAVIOR_STATUS_CODES below excludes 429 so a 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) ───────
# Count only genuinely-anomalous codes. The default set bans real
# users because it counts 429 (rate-limit burst), 403 (which is also
# the ban's OWN response → self-perpetuating ban), and 404 (normal
# PWA/SPA asset/manifest/icon probing). See OPERATIONS.md §38.6.
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
# (§38.6 item d) ────────────────────────────────────────────
USE_BLOCK_REFERRER_NONE=yes
BLOCK_REFERRER_NONE_URL=/relay/v1/account/invite
# ─── Country block (§38.6 item b) ───────────────────────────────
{% if bunkerweb_block_countries | length > 0 %}
BLACKLIST_COUNTRY={{ bunkerweb_block_countries | join(' ') }}
{% else %}
# No country blocks configured. Add ISO-3166-1 alpha-2 codes to
# bunkerweb_block_countries in group_vars/all.yml to enable.
{% endif %}
# ─── ASN block (§38.6 item c) ───────────────────────────────────
{% if bunkerweb_block_asns | length > 0 %}
BLACKLIST_ASN={{ bunkerweb_block_asns | join(' ') }}
{% else %}
# No ASN blocks configured.
{% endif %}
# ─── Real-IP forwarding ─────────────────────────────────────────
# BunkerWeb sets the real client as the leftmost X-Forwarded-For
# entry; requests pass BunkerWeb -> frontend -> relay and the
# frontend appends to the chain, so the relay reads the client from
# XFF[0]. The relay sees the FRONTEND container as its immediate
# peer, so its trusted-proxy IPs setting must include this network's
# CIDR (172.20.0.0/16 per docker-compose.yml) — one CIDR covers both
# 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
# NO global antibot (cp663 #12). BunkerWeb's USE_ANTIBOT is SITE-WIDE:
# it challenges EVERY visitor to the whole marketplace, and ANTIBOT_URI
# only sets where the challenge renders — pointing it at a live API path
# (/relay/v1/account/invite) both gated the entire site behind a captcha
# AND shadowed that real relay endpoint. The invite endpoint is already
# protected, correctly scoped, by USE_BLOCK_REFERRER_NONE above plus the
# relay's own ALTCHA challenge. If a deliberate site-wide gate is ever
# wanted, set ANTIBOT_URI to a DEDICATED path (e.g. /__antibot), never a
# live endpoint.
USE_ANTIBOT=no
{% if morphit_instance_tor_address | default('') | length > 0 %}
# ─── Onion-Location (cp700) ─────────────────────────────────────
# Advertise this instance's Tor onion to Tor-capable browsers via the
# Onion-Location HTTP HEADER. Brave reads ONLY the header — it ignores the
# <meta http-equiv="onion-location"> tag the frontend also emits — so without
# this the ".onion available" pill never appears in Brave. (Tor Browser honours
# the meta tag, so it already shows there; this adds Brave + anything else that
# is header-only.) Non-Tor browsers (Chrome/Firefox/Safari/Opera) have no Tor
# and cannot reach a .onion, so they simply ignore it — the footer pill / link
# is their discovery path.
#
# Clearnet-only BY CONSTRUCTION: the onion bypasses BunkerWeb entirely
# (Tor → frontend:8090, cp695), so BunkerWeb only ever serves clearnet — the
# spec's "must not be emitted on the onion itself / must be HTTPS clearnet"
# rules are satisfied without any host gating. $request_uri keeps the visitor
# on the same page once redirected onto the onion.
CUSTOM_HEADER=Onion-Location: http://{{ morphit_instance_tor_address }}$request_uri
{% endif %}