Deploy model: apps build FROM the smallwebwaf image, settings prefixed SWWAF_ (closes #12)

SPEC.md and README.md now describe sneak's recommended deploy: an app's Dockerfile builds FROM the smallwebwaf image, and runit, started by runsvinit, runs smallwebwaf on :8080 in front of the app on 127.0.0.1:8081, so no setting is required. The spec covers what the app's Dockerfile adds, the users each process runs as, restarts, the health check, ports, the state directory and its volume, the app trusting loopback for forwarded headers, and upaas needing no change. An example app Dockerfile replaces the docker-compose examples. Every setting carries the SWWAF_ prefix, the file form too, and "sidecar" no longer names the deploy shape.

Model: opus-5-5
This commit was merged in pull request #32.
This commit is contained in:
2026-09-29 01:48:47 +02:00
parent 789895782e
commit ba54ecb009
2 changed files with 826 additions and 669 deletions
+83 -63
View File
@@ -1,12 +1,14 @@
# smallwebwaf # smallwebwaf
`smallwebwaf` is a simple, fast, logging web application firewall for people who `smallwebwaf` is a simple, fast, logging web application firewall for people who
host their own services. It is one small container that sits between your host their own services. It runs inside the container of the one application it
reverse proxy (traefik) and one application: traefik points at `smallwebwaf`, protects, between your reverse proxy (traefik) and the app: the app's Dockerfile
and `smallwebwaf` points at the app. It needs one setting, the address of the builds `FROM` the `smallwebwaf` image, traefik sends the app's requests to
app, and protects the app from the first request with defaults chosen for a `smallwebwaf` on port 8080, and `smallwebwaf` passes them on to the app on
service on the open internet. It keeps its state in memory and in JSON files you `127.0.0.1:8081`. It needs no setting, and protects the app from the first
can read and edit, and writes a detailed JSON log line for every request. request with defaults chosen for a service on the open internet. It keeps its
state in memory and in JSON files you can read and edit, and writes a detailed
JSON log line for every request.
Status: design stage. This repository currently holds the documents only; no Status: design stage. This repository currently holds the documents only; no
code has been written. The design is in [`SPEC.md`](SPEC.md), and the survey of code has been written. The design is in [`SPEC.md`](SPEC.md), and the survey of
@@ -41,15 +43,16 @@ goes through the candidates one by one.
permanently when they keep at it; a scanner's first probe bans it for seven permanently when they keep at it; a scanner's first probe bans it for seven
days; days;
- log everything in a form that is easy to search and ship elsewhere; - log everything in a form that is easy to search and ship elsewhere;
- stay small enough to understand: one binary, one container, environment - stay small enough to understand: one binary in the app's own container,
variables, no database, one required setting. environment variables, no database, no required setting.
## Proposed features ## Proposed features
- Reverse proxy for one upstream application, streaming in both directions, with - Reverse proxy for one application, streaming in both directions, with
WebSocket support. One `smallwebwaf` per app. WebSocket support. One `smallwebwaf` per app, inside the app's own container.
- Internet-ready out of the box: only `UPSTREAM_URL` must be set, and every - Internet-ready out of the box: no setting is required, and every setting has a
other setting has a default chosen for a service facing the internet in 2026. default chosen for a service facing the internet in 2026. Every setting's name
starts with `SWWAF_`, so that it cannot clash with the app's own.
- Real client address worked out from `X-Forwarded-For`, trusting only the proxy - Real client address worked out from `X-Forwarded-For`, trusting only the proxy
networks you list, by default the private address ranges. IPv6 clients are networks you list, by default the private address ranges. IPv6 clients are
counted by /64 by default. counted by /64 by default.
@@ -65,11 +68,11 @@ goes through the candidates one by one.
GeoJS web service, which is sent the address of every new visitor. The IPinfo GeoJS web service, which is sent the address of every new visitor. The IPinfo
Lite database file, which you download and mount, can be used instead, or Lite database file, which you download and mount, can be used instead, or
lookups switched off (see "Country and AS number lookup" below). lookups switched off (see "Country and AS number lookup" below).
- Country lists: `DENIED_COUNTRIES` refuses every request from the countries - Country lists: `SWWAF_DENIED_COUNTRIES` refuses every request from the
listed, `EXCLUSIVELY_ALLOWED_COUNTRIES` every request from anywhere else. Such countries listed, `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` every request from
a request gets the answer a banned client gets as soon as the client's address anywhere else. Such a request gets the answer a banned client gets as soon as
has been looked up, before its body is read and without the rule files or the the client's address has been looked up, before its body is read and without
Core Rule Set looking at it, and no ban is made. the rule files or the Core Rule Set looking at it, and no ban is made.
- Biased limits: listed AS numbers and countries get a percentage of every - Biased limits: listed AS numbers and countries get a percentage of every
limit, for example 50 percent for common abuse-source networks, so their limit, for example 50 percent for common abuse-source networks, so their
clients are banned after fewer requests. Zero percent is a zero allowance: the clients are banned after fewer requests. Zero percent is a zero allowance: the
@@ -142,37 +145,53 @@ For each request `smallwebwaf`:
the client if it broke a limit, updates its history, sends any alerts that are the client if it broke a limit, updates its history, sends any alerts that are
due, and writes the log line. due, and writes the log line.
A minimal deployment beside an app in docker-compose. `UPSTREAM_URL` is the only A minimal deployment is the app's own Dockerfile, built on the `smallwebwaf`
setting: image, with no setting. Beyond its `FROM` line it adds the app's binary, any
packages it needs, and the app's runit service, which starts the app as a user
of its own, listening on `127.0.0.1:8081`:
```yaml ```dockerfile
services: # The smallwebwaf image, pinned by digest.
app: FROM <registry>/smallwebwaf:<pinned digest>
image: example/app
networks: [internal]
waf: # Packages the app needs, if any.
image: <registry>/smallwebwaf:<pinned digest> RUN apk add --no-cache tzdata
environment:
UPSTREAM_URL: http://app:3000
volumes: [waf-state:/data]
networks: [internal, traefik]
labels:
traefik.enable: "true"
traefik.http.routers.app.rule: Host(`app.example.invalid`)
traefik.http.services.app.loadbalancer.server.port: "8080"
volumes: # The app's binary, and a user of its own to run it.
waf-state: COPY app /usr/local/bin/app
RUN adduser -D -H -s /sbin/nologin app
networks: # The app's runit service.
internal: COPY --chmod=755 app.run /etc/service/app/run
traefik:
external: true
``` ```
The volume keeps bans and client history in a place you choose; without it with `app.run` beside the Dockerfile, where `--listen` and `--trusted-proxies`
`smallwebwaf` still starts, on a volume docker creates for it. stand for the app's own options:
```sh
#!/bin/sh
sleep 1
exec chpst -u app:app /usr/local/bin/app \
--listen 127.0.0.1:8081 \
--trusted-proxies 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/32,::1/128
```
- The image's entrypoint, `runsvinit`, has runit start `smallwebwaf` and the app
side by side, each as its own user, and start either again a second after it
exits. Leave out `ENTRYPOINT` and `USER` from the app's Dockerfile.
- Deploy it as you deploy any app, with traefik's labels on this one container
pointing at port 8080. upaas needs no change for this.
- The app has to trust `127.0.0.1` and `::1` for forwarded headers, besides the
private address ranges, since the requests it gets now come from `smallwebwaf`
on loopback. An app left at the usual default, the private ranges alone, sees
every visitor as `127.0.0.1`.
- Port 8080 is the only one the app must leave free: the health check, the
metrics and ban management are all on it, under `/_smallwebwaf/`. The image's
health check passes while `smallwebwaf` answers and the app accepts
connections.
- `smallwebwaf` keeps its state files in `/var/lib/smallwebwaf`. Mount a volume
there to keep bans and client history when a deploy replaces the container;
without one, it still starts.
A rule file is one rule per line: a name, what to match against, what to do, and A rule file is one rule per line: a name, what to match against, what to do, and
a regex. a regex.
@@ -182,9 +201,9 @@ env-file path ban (?i)^/\.env(\.[a-z]+)?$
scanner-agent user_agent ban (?i)\b(sqlmap|nikto|nuclei|wpscan)\b scanner-agent user_agent ban (?i)\b(sqlmap|nikto|nuclei|wpscan)\b
``` ```
[`SPEC.md`](SPEC.md) has the full design: every environment variable, the ban [`SPEC.md`](SPEC.md) has the full design: the deployment, every environment
rules, the rule file format, the state files, the log fields, the metrics, variable, the ban rules, the rule file format, the state files, the log fields,
failure behaviour and the build order. the metrics, failure behaviour and the build order.
## Country and AS number lookup ## Country and AS number lookup
@@ -196,28 +215,29 @@ default, the address of every new visitor is sent to GeoJS. Each answer is kept
for seven days, across restarts, and many addresses are asked about in one for seven days, across restarts, and many addresses are asked about in one
request. GeoJS publishes no rate limit but may block a caller it thinks asks too request. GeoJS publishes no rate limit but may block a caller it thinks asks too
much; while it is not answering, new visitors count as coming from an unknown much; while it is not answering, new visitors count as coming from an unknown
country, which `EXCLUSIVELY_ALLOWED_COUNTRIES` refuses. country, which `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` refuses.
To keep your visitors' addresses on your own host, set `LOOKUP_SOURCE=off`, or To keep your visitors' addresses on your own host, set
use the database file instead of GeoJS: `LOOKUP_SOURCE=file` reads the free `SWWAF_LOOKUP_SOURCE=off`, or use the database file instead of GeoJS:
IPinfo Lite database (`ipinfo_lite.mmdb`). You download it with your own IPinfo `SWWAF_LOOKUP_SOURCE=file` reads the free IPinfo Lite database
account, mount the directory that holds it into the container, point (`ipinfo_lite.mmdb`). You download it with your own IPinfo account, mount the
`LOOKUP_DB_PATH` at the file and refresh it when you choose; `smallwebwaf` never directory that holds it into the container, point `SWWAF_LOOKUP_DB_PATH` at the
downloads it itself, and reads it again when you replace it. It has to be the file and refresh it when you choose; `smallwebwaf` never downloads it itself,
directory rather than the file itself: docker does not show a single mounted and reads it again when you replace it. It has to be the directory rather than
file being replaced, so a refresh would go unseen. IPinfo releases it under the the file itself: docker does not show a single mounted file being replaced, so a
Creative Commons Attribution-ShareAlike 4.0 International License and asks for refresh would go unseen. IPinfo releases it under the Creative Commons
attribution, in its own words on https://ipinfo.io/lite: "The attribution Attribution-ShareAlike 4.0 International License and asks for attribution, in
requirements can be met by giving our service credit as your data source. Simply its own words on https://ipinfo.io/lite: "The attribution requirements can be
place a link to IPinfo on the website, application, or social media account that met by giving our service credit as your data source. Simply place a link to
uses our data." Its example of such a credit is a link mentioning "IP address IPinfo on the website, application, or social media account that uses our data."
data is powered by IPinfo". A service that uses the database through Its example of such a credit is a link mentioning "IP address data is powered by
`smallwebwaf` should carry that link. IPinfo". A service that uses the database through `smallwebwaf` should carry
that link.
Neither source can place a private address, so a client on one, such as a Neither source can place a private address, so a client on one, such as a
visitor on your local network, another container or your monitoring, has no visitor on your local network, another container or your monitoring, has no
country: `EXCLUSIVELY_ALLOWED_COUNTRIES` refuses it unless you list it in country: `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` refuses it unless you list it in
`ALLOW_NETS`. Such addresses are never sent to GeoJS. `SWWAF_ALLOW_NETS`. Such addresses are never sent to GeoJS.
## Documents ## Documents
+743 -606
View File
File diff suppressed because it is too large Load Diff