Docs: rproxy runs behind any TLS-terminating reverse proxy (closes #388)
check / check (push) Failing after 2m26s

README, SPEC and docs no longer present traefik as the required front
proxy, and call it the TLS-terminating proxy. The README section of that
name lists what the proxy must do: forward plain HTTP to port 8080, keep
Host, set X-Forwarded-For and X-Forwarded-Proto, close an unused
connection in under 120 seconds, and connect from an address in
SWW_RPROXY_TRUSTED_PROXIES. traefik, Caddy and nginx appear as examples,
with the Caddy and nginx settings their defaults lack.

Model: opus-5-5
This commit was merged in pull request #391.
This commit is contained in:
clawbot committed 2026-11-14 18:41:10 +01:00
1 parent 53d9bf6af6
commit a05a82b1af
4 files changed
+100 -53

No files matched your search

+35 -29
View File
@@ -3,7 +3,7 @@
`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 a TLS-terminating proxy and one app.
- `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 +45,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
TLS-terminating proxy, 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 +1944,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 TLS-terminating proxy 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 +2772,9 @@ 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 TLS-terminating proxy 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.
What the visitor gets when a limit is passed:
@@ -2824,14 +2825,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 TLS-terminating proxy 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. `README.md` lists what the
TLS-terminating proxy must do.
### `rproxy` in the app's container
@@ -2903,10 +2906,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 TLS-terminating proxy 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 +2942,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 TLS-terminating proxy's routes 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 TLS-terminating 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 TLS-terminating proxy 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 TLS-terminating
proxy 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 +3002,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 TLS-terminating proxy'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 the TLS-terminating 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.