Deploy model: apps build FROM the smallwebwaf image, settings prefixed SWWAF_ #32
@@ -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