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