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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user