Tiny per-app reverse proxy that asks a separate smallwebwaf whether to pass each request #165

Open
opened 2026-10-31 18:23:56 +01:00 by clawbot · 7 comments
Collaborator

sneak, 2026-10-31, verbatim:

i have an idea for smallwebwaf that solves two problems. i propose we create a very tiny reverse proxy that is run per-app. it's the very smallest part of smallwebwaf. on each inbound request it fires the request to a configured waf endpoint, so smallwebwaf gets the request just as it does now. smallwebwaf responds to the reverse proxy however, instead of the client. it responds with headers that tell it whether to proxy it or not, and with what code, and if it's a no-proxy, the smallwebwaf response body is used as the client's response body.

this way, the process/container doing the decision logic for the app can be separate from the actual reverse proxy that is in the data path for the client. also, the reverse proxy can fail open (or configurably closed) in the event smallwebwaf (or whatever decider backend it is using) is unavailable or laggy - it can have a strict timeout so if the decider (which talks to external services, etc) lags or takes too long or gets overloaded or is down, the website it's fronting still works normally. this allows for smallwebwaf updates without site downtime. it also allows us to run a single decider for many sites, which is a huge benefit.

Reading, to be confirmed with sneak before any design is written up

  • A very small reverse proxy runs in front of each app. It is the only part in the visitor's data path.
  • For every inbound request it sends the request to a configured endpoint (smallwebwaf), which sees the request as it does today.
  • smallwebwaf answers the proxy, not the visitor: headers say whether to pass the request to the app and with what status code. When the answer is not to pass it, smallwebwaf's response body becomes the visitor's response body.
  • The proxy gives smallwebwaf a strict time limit. When smallwebwaf is down, slow or overloaded, the proxy fails open (passes the request to the app), or fails closed when configured so.
  • Gains: smallwebwaf can be updated or restarted without taking the site down, and one smallwebwaf can decide for many sites.

Definition of done

  • sneak has confirmed the reading above and answered the design questions the manager raises, each as its own issue assigned to him, linked here.
  • The proxy and smallwebwaf's answer to it are built as he confirmed, on next, with tests for: pass; block with smallwebwaf's status and body; smallwebwaf down; smallwebwaf too slow; fail open; fail closed.
  • Stopping or restarting smallwebwaf while the proxy runs leaves the site answering normally (fail open).
  • One smallwebwaf serves two sites through two proxies in a test.
  • README and SPEC.md describe the arrangement.
  • make check green on next.

Context for the manager

Design first: read the code, post the plain-language reading and the open questions the design turns on, file each question as its own issue assigned to sneak, then stop until he answers (memory design-needs-discussion-first, spec-first-means-stop). Simplest thing that works, fewest options.

Prior art, for reference only: Caddy forward_auth and Traefik ForwardAuth ask a separate service about every request (a 2xx answer passes it; any other answer goes to the visitor as is); Envoy's ext_authz adds a timeout and a fail-open switch.

Model: opus-5-5

sneak, 2026-10-31, verbatim: > i have an idea for smallwebwaf that solves two problems. i propose we create a very tiny reverse proxy that is run per-app. it's the very smallest part of smallwebwaf. on each inbound request it fires the request to a configured waf endpoint, so smallwebwaf gets the request just as it does now. smallwebwaf responds to the reverse proxy however, instead of the client. it responds with headers that tell it whether to proxy it or not, and with what code, and if it's a no-proxy, the smallwebwaf response body is used as the client's response body. > > this way, the process/container doing the decision logic for the app can be separate from the actual reverse proxy that is in the data path for the client. also, the reverse proxy can fail open (or configurably closed) in the event smallwebwaf (or whatever decider backend it is using) is unavailable or laggy - it can have a strict timeout so if the decider (which talks to external services, etc) lags or takes too long or gets overloaded or is down, the website it's fronting still works normally. this allows for smallwebwaf updates without site downtime. it also allows us to run a single decider for many sites, which is a huge benefit. ## Reading, to be confirmed with sneak before any design is written up - A very small reverse proxy runs in front of each app. It is the only part in the visitor's data path. - For every inbound request it sends the request to a configured endpoint (smallwebwaf), which sees the request as it does today. - smallwebwaf answers the proxy, not the visitor: headers say whether to pass the request to the app and with what status code. When the answer is not to pass it, smallwebwaf's response body becomes the visitor's response body. - The proxy gives smallwebwaf a strict time limit. When smallwebwaf is down, slow or overloaded, the proxy fails open (passes the request to the app), or fails closed when configured so. - Gains: smallwebwaf can be updated or restarted without taking the site down, and one smallwebwaf can decide for many sites. ## Definition of done - sneak has confirmed the reading above and answered the design questions the manager raises, each as its own issue assigned to him, linked here. - The proxy and smallwebwaf's answer to it are built as he confirmed, on `next`, with tests for: pass; block with smallwebwaf's status and body; smallwebwaf down; smallwebwaf too slow; fail open; fail closed. - Stopping or restarting smallwebwaf while the proxy runs leaves the site answering normally (fail open). - One smallwebwaf serves two sites through two proxies in a test. - README and `SPEC.md` describe the arrangement. - `make check` green on `next`. ## Context for the manager Design first: read the code, post the plain-language reading and the open questions the design turns on, file each question as its own issue assigned to sneak, then stop until he answers (memory design-needs-discussion-first, spec-first-means-stop). Simplest thing that works, fewest options. Prior art, for reference only: Caddy forward_auth and Traefik ForwardAuth ask a separate service about every request (a 2xx answer passes it; any other answer goes to the visitor as is); Envoy's ext_authz adds a timeout and a fail-open switch. Model: opus-5-5
Owner

This would be part of smallwebwaf, same binary. The proxy would be something like smallwebwaf rproxy that reads a few variables from the environment - SWRP_TIMEOUT_MS SWRP_WAF_BACKEND SWRP_DEFAULT_DISPOSITION (defaults to "allow", optionally can be set to "deny") - SWRP_MAX_REQUEST_SIZE SWRP_MAX_REQUEST_TIME_MS SWRP_MAX_RESPONSE_SIZE SWRP_MAX_RESPONSE_TIME - anything else?

SWRP_WAF_BACKEND would be pointed at an instance of smallwebwaf servewaf which runs the decision engine and keeps state. the SWRP (smallweb reverse proxy) would be entirely and completely and totally stateless and never touch disk for any reason.

request logging could be moved to the SWRP, but perhaps SWRP should do minimal logging (request size, response size, request time, response time, status code, WAF disposition) and the WAF decision process can do more detailed logging. unfortunately neither process can do both, because only the RP has access to the backend response size and latency/timing/response code.

This would be part of smallwebwaf, same binary. The proxy would be something like `smallwebwaf rproxy` that reads a few variables from the environment - SWRP_TIMEOUT_MS SWRP_WAF_BACKEND SWRP_DEFAULT_DISPOSITION (defaults to "allow", optionally can be set to "deny") - SWRP_MAX_REQUEST_SIZE SWRP_MAX_REQUEST_TIME_MS SWRP_MAX_RESPONSE_SIZE SWRP_MAX_RESPONSE_TIME - anything else? SWRP_WAF_BACKEND would be pointed at an instance of `smallwebwaf servewaf` which runs the decision engine and keeps state. the SWRP (smallweb reverse proxy) would be entirely and completely and totally stateless and *never* touch disk for any reason. request logging could be moved to the SWRP, but perhaps SWRP should do minimal logging (request size, response size, request time, response time, status code, WAF disposition) and the WAF decision process can do more detailed logging. unfortunately neither process can do both, because only the RP has access to the backend response size and latency/timing/response code.
Author
Collaborator

On "anything else?":

Missing from the list:

  • The address and port smallwebwaf rproxy listens on, e.g. SWRP_LISTEN.
  • The app's own address, where allowed requests go, e.g. SWRP_UPSTREAM. The list has the WAF backend but not the app.
  • The visitor's real address. The proxy must send servewaf the visitor's address, not its own. When the proxy itself sits behind another proxy, it believes X-Forwarded-For only from trusted proxies: by our standing rule all private (RFC 1918) addresses by default, replaced by an explicit list, e.g. SWRP_TRUSTED_PROXIES.

Naming: SWRP_MAX_RESPONSE_TIME lacks the _MS its siblings carry, and SWRP_WAF_TIMEOUT_MS would say which wait SWRP_TIMEOUT_MS limits.

Two points the manager will check against the code before the design is written, and put to you one issue each if they need you:

  • Whether servewaf sees the request body. Either way the proxy holds a body up to SWRP_MAX_REQUEST_SIZE in memory, never on disk.
  • Whether smallwebwaf's decisions today use the app's responses, such as counting one visitor's 404s. If they do, the proxy has to tell servewaf each response's status, size and time after sending it to the visitor, without making the visitor wait. That same report would let servewaf write the full log line, so the proxy could log nothing.

Model: opus-5-5

On "anything else?": Missing from the list: - The address and port `smallwebwaf rproxy` listens on, e.g. `SWRP_LISTEN`. - The app's own address, where allowed requests go, e.g. `SWRP_UPSTREAM`. The list has the WAF backend but not the app. - The visitor's real address. The proxy must send `servewaf` the visitor's address, not its own. When the proxy itself sits behind another proxy, it believes `X-Forwarded-For` only from trusted proxies: by our standing rule all private (RFC 1918) addresses by default, replaced by an explicit list, e.g. `SWRP_TRUSTED_PROXIES`. Naming: `SWRP_MAX_RESPONSE_TIME` lacks the `_MS` its siblings carry, and `SWRP_WAF_TIMEOUT_MS` would say which wait `SWRP_TIMEOUT_MS` limits. Two points the manager will check against the code before the design is written, and put to you one issue each if they need you: - Whether `servewaf` sees the request body. Either way the proxy holds a body up to `SWRP_MAX_REQUEST_SIZE` in memory, never on disk. - Whether smallwebwaf's decisions today use the app's responses, such as counting one visitor's 404s. If they do, the proxy has to tell `servewaf` each response's status, size and time after sending it to the visitor, without making the visitor wait. That same report would let `servewaf` write the full log line, so the proxy could log nothing. Model: opus-5-5
Author
Collaborator

The design as it stands, for you to discuss. Nothing is built until you have answered the three questions at the end.

Two commands, one binary

  • smallwebwaf rproxy runs in each app's container, in front of the app. It is the only part between the visitor and the app. Its settings start with SWRP_. It keeps nothing and never touches disk.
  • smallwebwaf servewaf decides, and keeps everything that lasts: bans, counters, history, lookups, rule files, alerts and metrics, with today's SWWAF_ settings. It runs from the same image in a container of its own, with the state volume. One serves many sites.

One request

  1. rproxy works out the visitor's address, as smallwebwaf does today, and asks servewaf before reading the body. It sends the method, path, query and headers, with the visitor's address and the request id in headers of its own.
  2. servewaf runs today's checks, from the static lists to the Core Rule Set, and answers with a header saying allow or deny. On deny, its status and body go to the visitor unchanged: SWWAF_BAN_RESPONSE, or 403 for a block rule or the Core Rule Set.
  3. On allow, rproxy passes the request to the app, streaming, and the app's answer back, within its size and time limits.
  4. Once the answer has ended, rproxy tells servewaf its status, the sizes of the request and response bodies, and the times. The visitor does not wait for this. A report that fails is dropped.
  5. When servewaf does not answer within SWRP_TIMEOUT_MS, or answers without that header, SWRP_DEFAULT_DISPOSITION decides: allow passes the request to the app, deny answers 503. Such a request is neither checked nor counted.

The two points checked against the code

  • Request bodies: at the defaults servewaf needs none. The Core Rule Set reads a body only while SWWAF_WAF_BODY_LIMIT is set, which it is not by default, and rule files never see one. Whether servewaf should read any is the second question below.
  • The app's answers: today's decisions use their size. The byte limits, on by default, count the body bytes of each request and response, and ban a client that passes one. The status goes only into the client's history, the ban notes, the log and the metrics, and the times only into the log and the metrics; no decision uses either. Hence the report in step 4: without it, the byte limits cannot work behind the proxy.

rproxy's settings: your list, plus three. The defaults are today's.

  • SWRP_WAF_BACKEND: servewaf's URL. Required, with no default.
  • SWRP_TIMEOUT_MS (2000): how long rproxy waits for servewaf's answer. It is above the one second servewaf may wait for GeoJS on a new visitor.
  • SWRP_DEFAULT_DISPOSITION: allow (the default) or deny.
  • SWRP_MAX_REQUEST_SIZE (100M) and SWRP_MAX_RESPONSE_SIZE (1G): the largest request and response bodies.
  • SWRP_MAX_REQUEST_TIME_MS (60000): how long the visitor may take to send its request. SWRP_MAX_RESPONSE_TIME_MS (1800000): how long the answer may take, from the end of the request to its last byte reaching the visitor. _MS is added to your SWRP_MAX_RESPONSE_TIME to match the others. These two take the place of four of today's five timeouts. The fifth, the idle time of a kept-open connection, and the header size limit stay at today's defaults, with no setting.
  • SWRP_LISTEN_ADDR (:8080), SWRP_UPSTREAM_URL (http://127.0.0.1:8081, the app) and SWRP_TRUSTED_PROXIES (the private address ranges), like their SWWAF_ namesakes.

Logging, as you wrote: rproxy writes one short line per request (request size, response size, request time, response time, status, servewaf's answer, and whether SWRP_DEFAULT_DISPOSITION gave it). servewaf writes one detailed line per decision. The request id joins the two. The report in step 4 would also let servewaf write one complete line, but only by holding each request open until its report comes.

Taken as obvious, unless you say otherwise:

  • servewaf's own endpoints (health, metrics, bans, clients) are at its own address, reached only from the private network. Nothing routes the internet to it. rproxy answers /_smallwebwaf/healthz itself, and servewaf answers 404 to any other /_smallwebwaf/ path on a site, so the app never sees that prefix.
  • No token between rproxy and servewaf. Anything on that private network can ask servewaf or send it reports, and so could get a visitor banned with false byte counts.
  • The app's container shows as healthy while rproxy and the app answer, whether or not servewaf does. servewaf's container has a health check of its own.
  • With SWWAF_ADD_LOOKUP_HEADERS, servewaf's allow carries X-Client-ASN and X-Client-Country, which rproxy adds to the request it passes to the app.

Questions, each its own issue, assigned to you

  1. #166: does today's one-process smallwebwaf stay beside the two? Recommended: no, they replace it.
  2. #167: does servewaf read request bodies? Recommended: no.
  3. #168: what do the sites behind one servewaf share? Recommended: one table of clients and bans for all of them.

Model: opus-5-5

The design as it stands, for you to discuss. Nothing is built until you have answered the three questions at the end. **Two commands, one binary** - `smallwebwaf rproxy` runs in each app's container, in front of the app. It is the only part between the visitor and the app. Its settings start with `SWRP_`. It keeps nothing and never touches disk. - `smallwebwaf servewaf` decides, and keeps everything that lasts: bans, counters, history, lookups, rule files, alerts and metrics, with today's `SWWAF_` settings. It runs from the same image in a container of its own, with the state volume. One serves many sites. **One request** 1. `rproxy` works out the visitor's address, as smallwebwaf does today, and asks `servewaf` before reading the body. It sends the method, path, query and headers, with the visitor's address and the request id in headers of its own. 2. `servewaf` runs today's checks, from the static lists to the Core Rule Set, and answers with a header saying `allow` or `deny`. On `deny`, its status and body go to the visitor unchanged: `SWWAF_BAN_RESPONSE`, or `403` for a block rule or the Core Rule Set. 3. On `allow`, `rproxy` passes the request to the app, streaming, and the app's answer back, within its size and time limits. 4. Once the answer has ended, `rproxy` tells `servewaf` its status, the sizes of the request and response bodies, and the times. The visitor does not wait for this. A report that fails is dropped. 5. When `servewaf` does not answer within `SWRP_TIMEOUT_MS`, or answers without that header, `SWRP_DEFAULT_DISPOSITION` decides: `allow` passes the request to the app, `deny` answers `503`. Such a request is neither checked nor counted. **The two points checked against the code** - Request bodies: at the defaults `servewaf` needs none. The Core Rule Set reads a body only while `SWWAF_WAF_BODY_LIMIT` is set, which it is not by default, and rule files never see one. Whether `servewaf` should read any is the second question below. - The app's answers: today's decisions use their size. The byte limits, on by default, count the body bytes of each request and response, and ban a client that passes one. The status goes only into the client's history, the ban notes, the log and the metrics, and the times only into the log and the metrics; no decision uses either. Hence the report in step 4: without it, the byte limits cannot work behind the proxy. **`rproxy`'s settings**: your list, plus three. The defaults are today's. - `SWRP_WAF_BACKEND`: `servewaf`'s URL. Required, with no default. - `SWRP_TIMEOUT_MS` (`2000`): how long `rproxy` waits for `servewaf`'s answer. It is above the one second `servewaf` may wait for GeoJS on a new visitor. - `SWRP_DEFAULT_DISPOSITION`: `allow` (the default) or `deny`. - `SWRP_MAX_REQUEST_SIZE` (`100M`) and `SWRP_MAX_RESPONSE_SIZE` (`1G`): the largest request and response bodies. - `SWRP_MAX_REQUEST_TIME_MS` (`60000`): how long the visitor may take to send its request. `SWRP_MAX_RESPONSE_TIME_MS` (`1800000`): how long the answer may take, from the end of the request to its last byte reaching the visitor. `_MS` is added to your `SWRP_MAX_RESPONSE_TIME` to match the others. These two take the place of four of today's five timeouts. The fifth, the idle time of a kept-open connection, and the header size limit stay at today's defaults, with no setting. - `SWRP_LISTEN_ADDR` (`:8080`), `SWRP_UPSTREAM_URL` (`http://127.0.0.1:8081`, the app) and `SWRP_TRUSTED_PROXIES` (the private address ranges), like their `SWWAF_` namesakes. **Logging**, as you wrote: `rproxy` writes one short line per request (request size, response size, request time, response time, status, `servewaf`'s answer, and whether `SWRP_DEFAULT_DISPOSITION` gave it). `servewaf` writes one detailed line per decision. The request id joins the two. The report in step 4 would also let `servewaf` write one complete line, but only by holding each request open until its report comes. **Taken as obvious**, unless you say otherwise: - `servewaf`'s own endpoints (health, metrics, bans, clients) are at its own address, reached only from the private network. Nothing routes the internet to it. `rproxy` answers `/_smallwebwaf/healthz` itself, and `servewaf` answers `404` to any other `/_smallwebwaf/` path on a site, so the app never sees that prefix. - No token between `rproxy` and `servewaf`. Anything on that private network can ask `servewaf` or send it reports, and so could get a visitor banned with false byte counts. - The app's container shows as healthy while `rproxy` and the app answer, whether or not `servewaf` does. `servewaf`'s container has a health check of its own. - With `SWWAF_ADD_LOOKUP_HEADERS`, `servewaf`'s `allow` carries `X-Client-ASN` and `X-Client-Country`, which `rproxy` adds to the request it passes to the app. **Questions, each its own issue, assigned to you** 1. https://git.eeqj.de/sneak/smallwebwaf/issues/166: does today's one-process smallwebwaf stay beside the two? Recommended: no, they replace it. 2. https://git.eeqj.de/sneak/smallwebwaf/issues/167: does `servewaf` read request bodies? Recommended: no. 3. https://git.eeqj.de/sneak/smallwebwaf/issues/168: what do the sites behind one `servewaf` share? Recommended: one table of clients and bans for all of them. Model: opus-5-5
Author
Collaborator

sneak, 4 November, in chat: yes, build it now. Its purpose, in his words: if the decider goes down, the reverse proxy continues reverse proxying and fails open. The design questions are settled (#166, #167, #168). Running it in front of Gitea, with defaults for API use, follows in #169.

Model: opus-5-5

sneak, 4 November, in chat: yes, build it now. Its purpose, in his words: if the decider goes down, the reverse proxy continues reverse proxying and fails open. The design questions are settled (https://git.eeqj.de/sneak/smallwebwaf/issues/166, https://git.eeqj.de/sneak/smallwebwaf/issues/167, https://git.eeqj.de/sneak/smallwebwaf/issues/168). Running it in front of Gitea, with defaults for API use, follows in https://git.eeqj.de/sneak/smallwebwaf/issues/169. Model: opus-5-5
Author
Collaborator

The design, with sneak's rulings of 4 November folded in. It is being built now.

Two commands of one binary, replacing today's one process (#166)

  • smallwebwaf rproxy stands in front of one app, the only part between the visitor and the app. Its settings start with SWRP_. It keeps nothing and never touches disk.
  • smallwebwaf servewaf decides, and keeps everything that lasts: bans, counters, history, lookups, rule files, alerts and metrics, with today's SWWAF_ settings and the state volume.
  • The usual setup is three containers: the app, rproxy and servewaf. Building the app's image FROM the smallwebwaf image, with rproxy beside the app as today, stays possible.

One request

  1. rproxy answers GET /_smallwebwaf/healthz itself. It passes on only the methods GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH and QUERY, and answers any other with 405 itself (sneak, #181 (comment)). For any other request it works out the visitor's address as smallwebwaf does today and reads the first 64 KiB of the body into memory (#167).
  2. It sends servewaf the request as it came, with that part of the body, adding the visitor's address and the request id.
  3. servewaf runs today's checks, from the static lists to the Core Rule Set, and answers allow or deny in a header. On deny, its status, headers and body go to the visitor: SWWAF_BAN_RESPONSE, 403 for a block rule or the Core Rule Set, 404 for a path under /_smallwebwaf/.
  4. On allow, rproxy passes the request to the app, the part it holds and then the rest streaming, and the answer back, within its size and time limits.
  5. Once the answer has ended, rproxy reports its status, bytes and times to servewaf, which counts the bytes toward the byte limits and adds the request to the client's history. The visitor does not wait for this; a failed report is dropped.
  6. Any answer servewaf gives other than allow, whether a refusal, an error or an answer without that header, goes to the visitor as servewaf gave it: its status, headers and body. Only when servewaf cannot be connected to, does not answer at all, or does not answer within SWRP_TIMEOUT_MS does SWRP_DEFAULT_DISPOSITION decide: allow (the default) passes the request to the app, deny answers 503 (sneak, #181 (comment)). So servewaf can be down, slow, overloaded or restarting while the site keeps answering. While it answers, it must answer well within SWRP_TIMEOUT_MS (sneak, #181 (comment); #182).

Many sites, one servewaf (#168): one table of clients, counters and bans, and one cache of lookups and reputation answers, for every site behind it. A client banned on one site is refused on all of them, and its requests on all of them count toward one set of limits. Log lines, alerts and ban notes name the site by its host.

Request bodies: servewaf gets the first 64 KiB of each. The Core Rule Set reads it only while SWWAF_WAF_BODY_LIMIT is set, off by default as today; rule files still see no body. At small-web scale, 64 KiB held per request in progress is no concern.

rproxy's settings, defaults as today's: SWRP_WAF_BACKEND (servewaf's URL, required), SWRP_TIMEOUT_MS (2000), SWRP_DEFAULT_DISPOSITION (allow), SWRP_MAX_REQUEST_SIZE (100M), SWRP_MAX_RESPONSE_SIZE (1G), SWRP_MAX_REQUEST_TIME_MS (60000), SWRP_MAX_RESPONSE_TIME_MS (1800000), SWRP_LISTEN_ADDR (:8080), SWRP_UPSTREAM_URL (http://127.0.0.1:8081), SWRP_TRUSTED_PROXIES (the private ranges). The SWWAF_ settings they replace go.

Logs: rproxy writes one short line per request (status, bytes, times, servewaf's answer, and whether the default gave it); servewaf one detailed line per decision. The request id joins them.

servewaf's own endpoints (health, metrics, bans, clients, reports) are at its own address on the private network; nothing routes the internet there. No token between rproxy and servewaf; the admin and metrics tokens stay.

Units, in order: servewaf #170, rproxy #171, servewaf answering within the time limit #182, the replacement #172 (closes this issue), the package rename #173. Held until after 1.0: the request viewer #174. After #169: tags #175.

Judgement call: the Core Rule Set does not read the 64 KiB by default, since in front of Gitea it already refuses everyday use (#169).

Model: opus-5-5

The design, with sneak's rulings of 4 November folded in. It is being built now. **Two commands of one binary, replacing today's one process** (https://git.eeqj.de/sneak/smallwebwaf/issues/166) - `smallwebwaf rproxy` stands in front of one app, the only part between the visitor and the app. Its settings start with `SWRP_`. It keeps nothing and never touches disk. - `smallwebwaf servewaf` decides, and keeps everything that lasts: bans, counters, history, lookups, rule files, alerts and metrics, with today's `SWWAF_` settings and the state volume. - The usual setup is three containers: the app, rproxy and servewaf. Building the app's image `FROM` the smallwebwaf image, with rproxy beside the app as today, stays possible. **One request** 1. rproxy answers `GET /_smallwebwaf/healthz` itself. It passes on only the methods `GET`, `HEAD`, `POST`, `OPTIONS`, `PUT`, `DELETE`, `PATCH` and `QUERY`, and answers any other with `405` itself (sneak, https://git.eeqj.de/sneak/smallwebwaf/pulls/181#issuecomment-168571). For any other request it works out the visitor's address as smallwebwaf does today and reads the first 64 KiB of the body into memory (https://git.eeqj.de/sneak/smallwebwaf/issues/167). 2. It sends servewaf the request as it came, with that part of the body, adding the visitor's address and the request id. 3. servewaf runs today's checks, from the static lists to the Core Rule Set, and answers `allow` or `deny` in a header. On `deny`, its status, headers and body go to the visitor: `SWWAF_BAN_RESPONSE`, `403` for a block rule or the Core Rule Set, `404` for a path under `/_smallwebwaf/`. 4. On `allow`, rproxy passes the request to the app, the part it holds and then the rest streaming, and the answer back, within its size and time limits. 5. Once the answer has ended, rproxy reports its status, bytes and times to servewaf, which counts the bytes toward the byte limits and adds the request to the client's history. The visitor does not wait for this; a failed report is dropped. 6. Any answer servewaf gives other than `allow`, whether a refusal, an error or an answer without that header, goes to the visitor as servewaf gave it: its status, headers and body. Only when servewaf cannot be connected to, does not answer at all, or does not answer within `SWRP_TIMEOUT_MS` does `SWRP_DEFAULT_DISPOSITION` decide: `allow` (the default) passes the request to the app, `deny` answers `503` (sneak, https://git.eeqj.de/sneak/smallwebwaf/pulls/181#issuecomment-168550). So servewaf can be down, slow, overloaded or restarting while the site keeps answering. While it answers, it must answer well within `SWRP_TIMEOUT_MS` (sneak, https://git.eeqj.de/sneak/smallwebwaf/pulls/181#issuecomment-168570; https://git.eeqj.de/sneak/smallwebwaf/issues/182). **Many sites, one servewaf** (https://git.eeqj.de/sneak/smallwebwaf/issues/168): one table of clients, counters and bans, and one cache of lookups and reputation answers, for every site behind it. A client banned on one site is refused on all of them, and its requests on all of them count toward one set of limits. Log lines, alerts and ban notes name the site by its host. **Request bodies**: servewaf gets the first 64 KiB of each. The Core Rule Set reads it only while `SWWAF_WAF_BODY_LIMIT` is set, off by default as today; rule files still see no body. At small-web scale, 64 KiB held per request in progress is no concern. **rproxy's settings**, defaults as today's: `SWRP_WAF_BACKEND` (servewaf's URL, required), `SWRP_TIMEOUT_MS` (`2000`), `SWRP_DEFAULT_DISPOSITION` (`allow`), `SWRP_MAX_REQUEST_SIZE` (`100M`), `SWRP_MAX_RESPONSE_SIZE` (`1G`), `SWRP_MAX_REQUEST_TIME_MS` (`60000`), `SWRP_MAX_RESPONSE_TIME_MS` (`1800000`), `SWRP_LISTEN_ADDR` (`:8080`), `SWRP_UPSTREAM_URL` (`http://127.0.0.1:8081`), `SWRP_TRUSTED_PROXIES` (the private ranges). The `SWWAF_` settings they replace go. **Logs**: rproxy writes one short line per request (status, bytes, times, servewaf's answer, and whether the default gave it); servewaf one detailed line per decision. The request id joins them. **servewaf's own endpoints** (health, metrics, bans, clients, reports) are at its own address on the private network; nothing routes the internet there. No token between rproxy and servewaf; the admin and metrics tokens stay. **Units**, in order: servewaf https://git.eeqj.de/sneak/smallwebwaf/issues/170, rproxy https://git.eeqj.de/sneak/smallwebwaf/issues/171, servewaf answering within the time limit https://git.eeqj.de/sneak/smallwebwaf/issues/182, the replacement https://git.eeqj.de/sneak/smallwebwaf/issues/172 (closes this issue), the package rename https://git.eeqj.de/sneak/smallwebwaf/issues/173. Held until after 1.0: the request viewer https://git.eeqj.de/sneak/smallwebwaf/issues/174. After https://git.eeqj.de/sneak/smallwebwaf/issues/169: tags https://git.eeqj.de/sneak/smallwebwaf/issues/175. Judgement call: the Core Rule Set does not read the 64 KiB by default, since in front of Gitea it already refuses everyday use (https://git.eeqj.de/sneak/smallwebwaf/issues/169). Model: opus-5-5
Owner

Once the answer has ended, rproxy reports its status, bytes and times to servewaf, which counts the bytes toward the byte limits and adds the request to the client's history. The visitor does not wait for this; a failed report is dropped.

servewaf should log a complete log message with all known data - servewaf's stuff as well as the rproxy stuff. it's ok if this is a duplicated log message, because the request ID (generated at request time by rproxy) will match and this one will be later/more complete.

> Once the answer has ended, rproxy reports its status, bytes and times to servewaf, which counts the bytes toward the byte limits and adds the request to the client's history. The visitor does not wait for this; a failed report is dropped. servewaf should log a complete log message with *all* known data - servewaf's stuff as well as the rproxy stuff. it's ok if this is a duplicated log message, because the request ID (generated at request time by rproxy) will match and this one will be later/more complete.
Owner

Logs: rproxy writes one short line per request (status, bytes, times, servewaf's answer, and whether the default gave it); servewaf one detailed line per decision. The request id joins them.

this remains. servewaf then logs a third line with all known information (its detailed line + rproxy stats)

> Logs: rproxy writes one short line per request (status, bytes, times, servewaf's answer, and whether the default gave it); servewaf one detailed line per decision. The request id joins them. this remains. servewaf then logs a third line with all known information (its detailed line + rproxy stats)
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/smallwebwaf#165