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