Docs: rproxy runs behind any TLS-terminating reverse proxy (closes #388)
check / check (push) Successful in 4m13s

README, SPEC and docs no longer present traefik as the required front
proxy. The README gains "The reverse proxy in front", listing what that
proxy must do: forward plain HTTP to port 8080, keep Host, set
X-Forwarded-For and X-Forwarded-Proto, and connect from an address in
SWW_RPROXY_TRUSTED_PROXIES. traefik, Caddy and nginx appear as examples,
with the nginx headers spelled out since it sets none by default.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-14 16:43:06 +00:00
1 parent 53d9bf6af6
commit cddcf3d065
4 files changed
+95 -57

No files matched your search

+37 -29
View File
@@ -3,7 +3,8 @@
`smallwebwaf` is a web application firewall for self-hosted apps. It is one
binary, `sww`, and two of its commands do the work:
- `sww rproxy` is a reverse proxy between traefik and one app.
- `sww rproxy` is a reverse proxy between one app and the reverse proxy that
terminates TLS.
- `sww waf` decides what happens to each request, for many sites.
This document says what it must do and why, each requirement once. Everything in
@@ -45,7 +46,8 @@ never as a time.
## Non-goals
- TLS, certificates and routing by host name. Those are traefik's job.
- TLS, certificates and routing by host name. Those are the job of the reverse
proxy in front of `rproxy`, such as traefik, nginx or Caddy.
- More than one app behind one `rproxy`. Each app has its own.
- Browser challenges such as captchas or proof of work. If wanted, chain Anubis
between `rproxy` and the app.
@@ -1943,10 +1945,10 @@ leave out a cookie only when its name matches exactly, case included.
### Ban lengths, response and scope
`SWW_WAF_BAN_RESPONSE` is `403` by default, which makes a ban easy to recognise
when debugging; `429` and `close` are the alternatives. Behind traefik, `close`
does not leave the client unanswered: traefik answers `502`, as it does whenever
its backend drops a connection. `SWW_WAF_BAN_SCOPE_V4_PREFIX` widens an IPv4 ban
to the surrounding netblock. The four ban lengths are under "Bans".
when debugging; `429` and `close` are the alternatives. `close` does not leave
the client unanswered: the reverse proxy in front answers `502`, as it does
whenever its backend drops a connection. `SWW_WAF_BAN_SCOPE_V4_PREFIX` widens an
IPv4 ban to the surrounding netblock. The four ban lengths are under "Bans".
### Reputation
@@ -2771,9 +2773,10 @@ its whole body. The visitor gets that answer at once, and the connection closes
after it.
The largest request line and headers is 32 KiB. A kept-open connection is closed
after 120 seconds idle. Neither has a setting. traefik, by default, closes an
unused connection after 90 seconds. So traefik closes first, and never sends a
request on a connection `rproxy` is closing.
after 120 seconds idle. Neither has a setting. The reverse proxy in front should
close an unused connection sooner, so that it never sends a request on a
connection `rproxy` is closing. traefik closes one after 90 seconds by default.
nginx, by default, opens a new connection for each request.
What the visitor gets when a limit is passed:
@@ -2824,14 +2827,16 @@ chooses: `waf` runs `waf`, and with it unset the image runs `rproxy`.
The usual setup is three containers on one private network:
- the app;
- `rproxy`, which traefik routes to, with `SWW_RPROXY_UPSTREAM_URL` naming the
app and `SWW_RPROXY_WAF_BACKEND` naming `waf`;
- `rproxy`, which the reverse proxy that terminates TLS forwards to, with
`SWW_RPROXY_UPSTREAM_URL` naming the app and `SWW_RPROXY_WAF_BACKEND` naming
`waf`;
- `waf`, with `SWW_COMMAND=waf` and a volume for its state files.
Each further app adds its own container and its own `rproxy`, whose
`SWW_RPROXY_WAF_BACKEND` names the same `waf` (see "Sites"). Anything on that
network can reach an app around `rproxy`, and can ask `waf` questions or send it
reports. So the network holds only what you trust.
reports. So the network holds only what you trust. "The reverse proxy in front"
in `README.md` lists what that proxy must do.
### `rproxy` in the app's container
@@ -2903,10 +2908,10 @@ process that stops at start is started again every second, not in a tight loop.
container's output, which `docker logs` shows.
- When a service exits, runit runs its `run` script again, and the container
does not stop. Beside an app, the other process keeps running.
- While `rproxy` is down, traefik answers `502`. While the app is down, `rproxy`
answers `502`, which is not a client offence. While `waf` is down,
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` decides (see "Without an answer
from `waf`").
- While `rproxy` is down, the reverse proxy in front answers `502`. While the
app is down, `rproxy` answers `502`, which is not a client offence. While
`waf` is down, `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` decides (see
"Without an answer from `waf`").
- Only faults found at start stop `waf`:
- invalid configuration;
- a lookup database that is missing or unreadable;
@@ -2939,23 +2944,26 @@ checks:
- With `SWW_COMMAND` unset, empty or `rproxy`, it passes while `rproxy` answers
that request on `127.0.0.1`, at the port in `SWW_RPROXY_LISTEN_ADDR`, and the
app accepts connections at `SWW_RPROXY_UPSTREAM_URL`. It does not ask `waf`,
so that the site stays in traefik's routes while `waf` is down.
so that the site stays in the routes of the reverse proxy in front while `waf`
is down.
- With any other value, it fails with a message naming `SWW_COMMAND` and its
value.
traefik sends a container no requests until it shows as healthy. So the check
runs every second from the container's start until it first passes, for up to a
minute, and every 30 seconds after that. An app with a health check of its own
can replace the image's `HEALTHCHECK` with one that checks both.
A reverse proxy that reads Docker's health status, as traefik does, sends a
container no requests until it shows as healthy. So the check runs every second
from the container's start until it first passes, for up to a minute, and every
30 seconds after that. An app with a health check of its own can replace the
image's `HEALTHCHECK` with one that checks both.
### Ports
`rproxy` listens on no port but the one in `SWW_RPROXY_LISTEN_ADDR`, 8080 by
default, which traefik routes to. It passes requests to the app at
`SWW_RPROXY_UPSTREAM_URL`, `http://127.0.0.1:8081` by default. Another port may
be set: the health check follows the setting, and traefik's port label
(`traefik.http.services.<name>.loadbalancer.server.port`) must name it. An app
beside `rproxy` must leave that port free.
default, which the reverse proxy in front forwards to. It passes requests to the
app at `SWW_RPROXY_UPSTREAM_URL`, `http://127.0.0.1:8081` by default. Another
port may be set: the health check follows the setting, and the reverse proxy in
front must name it, as traefik's port label
(`traefik.http.services.<name>.loadbalancer.server.port`) does. An app beside
`rproxy` must leave that port free.
`waf` listens on no port but the one in `SWW_WAF_LISTEN_ADDR`, 8080 by default.
The questions, the reports and its own endpoints all come there.
@@ -2996,9 +3004,9 @@ in `docs/settings.md` shows the commands.
### Forwarded headers
The app's TCP peer is `rproxy`, and the `X-Forwarded-For` the app receives ends
with traefik's address, which `rproxy` adds after the visitor's. So the app must
trust `rproxy`'s address for forwarded headers, besides the private address
ranges traefik is on.
with the address of the reverse proxy in front, which `rproxy` adds after the
visitor's. So the app must trust `rproxy`'s address for forwarded headers,
besides the private address ranges that proxy is on.
- In a container of its own, `rproxy`'s address is on the private network. The
usual default for a trusted-proxy setting, the private ranges, covers it.