mirror of
https://github.com/MeshCore-Beacon/beacon-docs.git
synced 2026-10-23 03:01:34 +00:00
* docs: move design and API contract into docs/ * docs: add releases and versioning page * docs: add configuration reference for server and web * docs: add reverse proxy page * docs: add upgrading page * docs: add operations page and move backup and profiling docs * docs: add getting packets in and security pages * docs: add troubleshooting page * docs(api): fold admin, activity, route and summary notes into the contract * docs(deploy): add type 2 split deployment, finish the beacon rename, ignore secrets * docs: rewrite README as the front door and share the contributor workflow * docs: remove em dashes from prose and config comments * docs: apply review fixes to type 2, upgrading, troubleshooting and first-run notes * docs: fix upgrade order, restore steps, type 2 setup and contract details from review * docs: keep the gitignore to deployment secrets and runtime state
118 lines
4.6 KiB
Markdown
118 lines
4.6 KiB
Markdown
# Reverse proxy
|
|
|
|
However you front Beacon, the proxy has the same three jobs:
|
|
|
|
- Send `/api/*` and `/ws` to beacon-server (port `8080`) and everything else to beacon-web. `/ws`
|
|
is a WebSocket, so the proxy must pass the upgrade through and keep idle sockets open for more
|
|
than 90 seconds (the browser pings every 30 seconds; the server drops a socket after 90 seconds
|
|
of silence).
|
|
- Overwrite `X-Real-IP` with the connecting client's address and pass the original `Host`
|
|
header. beacon-server ignores `X-Forwarded-For` and `True-Client-IP` entirely.
|
|
- Be listed in beacon-server's `server.trusted_proxies` (CIDR: `/32` for one IPv4 host, `/128`
|
|
for IPv6). Only those peers may set `X-Real-IP`. Get this wrong and every visitor shares the
|
|
proxy's rate limit and WebSocket connection cap.
|
|
|
|
`VITE_API_BASE` and `VITE_WS_URL` then point at the public paths, for example
|
|
`https://beacon.example.com/api/v1` and `wss://beacon.example.com/ws`.
|
|
|
|
## Caddy
|
|
|
|
Caddy is the default in both Docker deployments:
|
|
[`Caddyfile.proxy`](../docker-deployment-type1/data/Caddy/CaddyFile/Caddyfile.proxy). The
|
|
relevant part:
|
|
|
|
```caddy
|
|
handle /api/* {
|
|
reverse_proxy app:8080 {
|
|
header_up X-Real-IP {remote_host}
|
|
}
|
|
}
|
|
handle /ws {
|
|
reverse_proxy app:8080 {
|
|
header_up X-Real-IP {remote_host}
|
|
}
|
|
}
|
|
handle {
|
|
reverse_proxy web:80
|
|
}
|
|
```
|
|
|
|
Caddy handles the WebSocket upgrade and keeps the `Host` header by itself. It reaches the app
|
|
over the compose network, so `data/app/config.yaml` trusts that subnet:
|
|
|
|
```yaml
|
|
server:
|
|
trusted_proxies: [172.30.0.0/24]
|
|
```
|
|
|
|
## Behind a CDN
|
|
|
|
Behind Cloudflare or another CDN, `{remote_host}` is the CDN, not the visitor. The comment at
|
|
the top of `Caddyfile.proxy` shows how to trust the CDN's ranges and forward `{client_ip}`
|
|
instead. The same idea applies to nginx and Apache: trust the CDN at the edge and pass its
|
|
validated client address, never a header the client could have set.
|
|
|
|
## nginx
|
|
|
|
[`app_config/nginx/beacon.conf`](../app_config/nginx/beacon.conf) is a complete server block for
|
|
nginx on the host in front of the containers. The essentials:
|
|
|
|
```nginx
|
|
location /api/ {
|
|
proxy_pass http://127.0.0.1:8080;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
}
|
|
location = /ws {
|
|
proxy_pass http://127.0.0.1:8080;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade; # map is in the example file
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_read_timeout 120s;
|
|
}
|
|
location / {
|
|
proxy_pass http://127.0.0.1:8081; # beacon-web published on loopback
|
|
}
|
|
```
|
|
|
|
nginx sends the upstream address as `Host` by default, which fails the WebSocket origin check,
|
|
so keep `proxy_set_header Host $host`. When nginx reaches the containers through published
|
|
ports, the app sees the Docker network's gateway as the peer, so trust the compose subnet
|
|
(`[172.30.0.0/24]`). For a beacon-server running directly on the host, use
|
|
`["127.0.0.1/32", "::1/128"]`.
|
|
|
|
## Apache
|
|
|
|
Apache works the same way. [`app_config/apache/analyzer-vhost.snippet.conf`](../app_config/apache/analyzer-vhost.snippet.conf)
|
|
is a working virtual host snippet, including the `RequestHeader set X-Real-IP` line that
|
|
replaces whatever the client sent.
|
|
|
|
## Rate limits and connection caps
|
|
|
|
REST requests under `/api/v1` are rate limited per client IP, 300 a minute by default. IPv6
|
|
clients share one budget per /64. A client over the limit gets `429` with `rate_limited` in the
|
|
body and a `Retry-After` header.
|
|
|
|
WebSocket has two separate limits. At most 5 open connections per IP: a connection over the cap
|
|
is accepted and then closed with code `1013` (try again later) before `hello`. At most 10
|
|
upgrade attempts per IP per minute: beyond that the handshake gets `429` with `Retry-After: 60`.
|
|
Both are set under `websocket:` in `config.yaml`.
|
|
|
|
Behind a reverse proxy all of these only work with `server.trusted_proxies` set, or every
|
|
visitor counts as the proxy's IP. Without it every visitor shares the proxy's budget and the
|
|
site gets 429s under normal load. Beacon logs a warning at startup when limits are on and no
|
|
proxy is trusted, and once at runtime when a request carries forwarding headers it is ignoring.
|
|
|
|
## Split origins
|
|
|
|
When the web app and the API are on different hosts, browsers may only open `/ws` from the
|
|
page's own host unless the web origin is listed in `websocket.allowed_origins`
|
|
(`https://*.example.com` covers every subdomain). REST CORS allows any origin by default.
|
|
|
|
## Abuse controls
|
|
|
|
Access logs, a manual block list and fail2ban jails for both Caddy and Apache are in
|
|
[`app_config/fail2ban/`](../app_config/fail2ban/README.md).
|