Deploy model: apps build FROM the smallwebwaf image, settings prefixed SWWAF_ #32

Merged
clawbot merged 1 commits from issue-12-deploy-model into next 2026-09-29 01:48:48 +02:00
2 changed files with 826 additions and 669 deletions
+83 -63
View File
@@ -1,12 +1,14 @@
# smallwebwaf
`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
reverse proxy (traefik) and one application: traefik points at `smallwebwaf`,
and `smallwebwaf` points at the app. It needs one setting, the address of the
app, and protects the app from the first 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.
host their own services. It runs inside the container of the one application it
protects, between your reverse proxy (traefik) and the app: the app's Dockerfile
builds `FROM` the `smallwebwaf` image, traefik sends the app's requests to
`smallwebwaf` on port 8080, and `smallwebwaf` passes them on to the app on
`127.0.0.1:8081`. It needs no setting, and protects the app from the first
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
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
days;
- log everything in a form that is easy to search and ship elsewhere;
- stay small enough to understand: one binary, one container, environment
variables, no database, one required setting.
- stay small enough to understand: one binary in the app's own container,
environment variables, no database, no required setting.
## Proposed features
- Reverse proxy for one upstream application, streaming in both directions, with
WebSocket support. One `smallwebwaf` per app.
- Internet-ready out of the box: only `UPSTREAM_URL` must be set, and every
other setting has a default chosen for a service facing the internet in 2026.
- Reverse proxy for one application, streaming in both directions, with
WebSocket support. One `smallwebwaf` per app, inside the app's own container.
- Internet-ready out of the box: no setting is required, and every setting has a
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
networks you list, by default the private address ranges. IPv6 clients are
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
Lite database file, which you download and mount, can be used instead, or
lookups switched off (see "Country and AS number lookup" below).
- Country lists: `DENIED_COUNTRIES` refuses every request from the countries
listed, `EXCLUSIVELY_ALLOWED_COUNTRIES` every request from anywhere else. Such
a request gets the answer a banned client gets as soon as the client's address
has been looked up, before its body is read and without the rule files or the
Core Rule Set looking at it, and no ban is made.
- Country lists: `SWWAF_DENIED_COUNTRIES` refuses every request from the
countries listed, `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` every request from
anywhere else. Such a request gets the answer a banned client gets as soon as
the client's address has been looked up, before its body is read and without
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
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
@@ -142,37 +145,53 @@ For each request `smallwebwaf`:
the client if it broke a limit, updates its history, sends any alerts that are
due, and writes the log line.
A minimal deployment beside an app in docker-compose. `UPSTREAM_URL` is the only
setting:
A minimal deployment is the app's own Dockerfile, built on the `smallwebwaf`
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
services:
app:
image: example/app
networks: [internal]
```dockerfile
# The smallwebwaf image, pinned by digest.
FROM <registry>/smallwebwaf:<pinned digest>
waf:
image: <registry>/smallwebwaf:<pinned digest>
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"
# Packages the app needs, if any.
RUN apk add --no-cache tzdata
volumes:
waf-state:
# The app's binary, and a user of its own to run it.
COPY app /usr/local/bin/app
RUN adduser -D -H -s /sbin/nologin app
networks:
internal:
traefik:
external: true
# The app's runit service.
COPY --chmod=755 app.run /etc/service/app/run
```
The volume keeps bans and client history in a place you choose; without it
`smallwebwaf` still starts, on a volume docker creates for it.
with `app.run` beside the Dockerfile, where `--listen` and `--trusted-proxies`
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 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
```
[`SPEC.md`](SPEC.md) has the full design: every environment variable, the ban
rules, the rule file format, the state files, the log fields, the metrics,
failure behaviour and the build order.
[`SPEC.md`](SPEC.md) has the full design: the deployment, every environment
variable, the ban rules, the rule file format, the state files, the log fields,
the metrics, failure behaviour and the build order.
## 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
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
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
use the database file instead of GeoJS: `LOOKUP_SOURCE=file` reads the free
IPinfo Lite database (`ipinfo_lite.mmdb`). You download it with your own IPinfo
account, mount the directory that holds it into the container, point
`LOOKUP_DB_PATH` at the file and refresh it when you choose; `smallwebwaf` never
downloads it itself, and reads it again when you replace it. It has to be the
directory rather than the file itself: docker does not show a single mounted
file being replaced, so a refresh would go unseen. IPinfo releases it under the
Creative Commons Attribution-ShareAlike 4.0 International License and asks for
attribution, in its own words on https://ipinfo.io/lite: "The attribution
requirements can be met by giving our service credit as your data source. Simply
place a link to IPinfo on the website, application, or social media account that
uses our data." Its example of such a credit is a link mentioning "IP address
data is powered by IPinfo". A service that uses the database through
`smallwebwaf` should carry that link.
To keep your visitors' addresses on your own host, set
`SWWAF_LOOKUP_SOURCE=off`, or use the database file instead of GeoJS:
`SWWAF_LOOKUP_SOURCE=file` reads the free IPinfo Lite database
(`ipinfo_lite.mmdb`). You download it with your own IPinfo account, mount the
directory that holds it into the container, point `SWWAF_LOOKUP_DB_PATH` at the
file and refresh it when you choose; `smallwebwaf` never downloads it itself,
and reads it again when you replace it. It has to be the directory rather than
the file itself: docker does not show a single mounted file being replaced, so a
refresh would go unseen. IPinfo releases it under the Creative Commons
Attribution-ShareAlike 4.0 International License and asks for attribution, in
its own words on https://ipinfo.io/lite: "The attribution requirements can be
met by giving our service credit as your data source. Simply place a link to
IPinfo on the website, application, or social media account that uses our data."
Its example of such a credit is a link mentioning "IP address data is powered by
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
visitor on your local network, another container or your monitoring, has no
country: `EXCLUSIVELY_ALLOWED_COUNTRIES` refuses it unless you list it in
`ALLOW_NETS`. Such addresses are never sent to GeoJS.
country: `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` refuses it unless you list it in
`SWWAF_ALLOW_NETS`. Such addresses are never sent to GeoJS.
## Documents
+743 -606
View File
File diff suppressed because it is too large Load Diff