Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fef6b2f5ce |
@@ -38,13 +38,11 @@ It protects against:
|
|||||||
traversal. The OWASP Core Rule Set, a published set of rules that recognise
|
traversal. The OWASP Core Rule Set, a published set of rules that recognise
|
||||||
common web attacks, runs on every request through Coraza, a Go engine. A
|
common web attacks, runs on every request through Coraza, a Go engine. A
|
||||||
request the rules take for an attack is refused.
|
request the rules take for an attack is refused.
|
||||||
- Abuse from networks and countries you do not want. You can:
|
- Abuse from networks and countries you do not want. You can refuse whole
|
||||||
- refuse whole netblocks and countries;
|
netblocks and countries; give the clients of chosen networks and countries a
|
||||||
- give the clients of chosen networks and countries a fraction of every
|
fraction of every limit, so that they are banned sooner; refuse or limit
|
||||||
limit, so that they are banned sooner;
|
clients on a blocklist, a DNS blocklist, or with a high AbuseIPDB score; and
|
||||||
- refuse or limit clients on a blocklist, a DNS blocklist, or with a high
|
ban clients named by your own CrowdSec engine.
|
||||||
AbuseIPDB score;
|
|
||||||
- ban clients named by your own CrowdSec engine.
|
|
||||||
- Slow and oversized requests. Every request and every answer has a time limit
|
- Slow and oversized requests. Every request and every answer has a time limit
|
||||||
and a size limit.
|
and a size limit.
|
||||||
|
|
||||||
@@ -174,7 +172,8 @@ does.
|
|||||||
- To see a refusal without banning yourself, run `servewaf` with
|
- To see a refusal without banning yourself, run `servewaf` with
|
||||||
`SWWAF_MODE=observe` and ask for `/.env`. The request reaches the app.
|
`SWWAF_MODE=observe` and ask for `/.env`. The request reaches the app.
|
||||||
`servewaf`'s log lines have `"would_action":"banned"` and
|
`servewaf`'s log lines have `"would_action":"banned"` and
|
||||||
`"rule_ids":["env-file"]`. A ban you get in `enforce` mode is lifted as
|
`"rule_ids":["env-file"]`. In `enforce` mode the same request bans your
|
||||||
|
address, and your next request makes the ban permanent. Lift the ban as
|
||||||
"Clients and bans" below describes.
|
"Clients and bans" below describes.
|
||||||
- With `SWWAF_METRICS_TOKEN` set, this answers with the metrics, from a host on
|
- With `SWWAF_METRICS_TOKEN` set, this answers with the metrics, from a host on
|
||||||
`servewaf`'s network:
|
`servewaf`'s network:
|
||||||
@@ -339,9 +338,9 @@ full, and the limits that are fixed rather than settings.
|
|||||||
Each example is a complete `compose.yaml`. `smallwebwaf` is the tag
|
Each example is a complete `compose.yaml`. `smallwebwaf` is the tag
|
||||||
`make docker` gives the image; on another host, use the name you pushed it
|
`make docker` gives the image; on another host, use the name you pushed it
|
||||||
under, by digest. Docker Compose puts the services on a private network of its
|
under, by digest. Docker Compose puts the services on a private network of its
|
||||||
own, where each reaches the others by service name. Only "In front of gitea"
|
own, where each reaches the others by service name. The first two examples
|
||||||
uses traefik. The other three publish `rproxy`'s port on the host, for trying it
|
publish `rproxy`'s port on the host, for trying it out; the others leave that to
|
||||||
out.
|
traefik.
|
||||||
|
|
||||||
### Smallest setup
|
### Smallest setup
|
||||||
|
|
||||||
@@ -375,15 +374,10 @@ volumes:
|
|||||||
|
|
||||||
### Custom per-address limits
|
### Custom per-address limits
|
||||||
|
|
||||||
For a site with no API:
|
A site with no API: tighter limits for everyone, no limits for the office and
|
||||||
|
the automation host, no checks for monitoring, a refused netblock, a smaller
|
||||||
- tighter limits for everyone;
|
share of the limits for two hosting networks and one country, trap paths, and
|
||||||
- no limits for the office and the automation host;
|
`429` for a refused client.
|
||||||
- no checks for monitoring;
|
|
||||||
- a refused netblock;
|
|
||||||
- a smaller share of the limits for two hosting networks and one country;
|
|
||||||
- trap paths;
|
|
||||||
- `429` for a refused client.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -429,15 +423,10 @@ volumes:
|
|||||||
|
|
||||||
### In front of gitea
|
### In front of gitea
|
||||||
|
|
||||||
gitea behind traefik, set up for a code forge:
|
gitea behind traefik, with the upload and byte limits raised for a code forge,
|
||||||
|
the Actions runners exempt from counting, and the default request limits, which
|
||||||
- the upload limits raised, in size and in time;
|
already leave room for API clients, kept. `traefik` is the network traefik is
|
||||||
- the response size limit and the byte limits raised, for clones over 1 GiB;
|
on, made outside this file.
|
||||||
- the Actions runners exempt from counting;
|
|
||||||
- the default request limits kept, since they already leave room for API
|
|
||||||
clients.
|
|
||||||
|
|
||||||
`traefik` is the network traefik is on, made outside this file.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -446,9 +435,6 @@ services:
|
|||||||
image: gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef
|
image: gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef
|
||||||
environment:
|
environment:
|
||||||
GITEA__server__ROOT_URL: https://git.example/
|
GITEA__server__ROOT_URL: https://git.example/
|
||||||
# The host and the port of the SSH clone URLs gitea shows.
|
|
||||||
GITEA__server__SSH_DOMAIN: git.example
|
|
||||||
GITEA__server__SSH_PORT: "2222"
|
|
||||||
# gitea's TCP peer is rproxy, on the private network.
|
# gitea's TCP peer is rproxy, on the private network.
|
||||||
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||||
volumes:
|
volumes:
|
||||||
@@ -468,15 +454,11 @@ services:
|
|||||||
# than a minute, are cut off at the defaults.
|
# than a minute, are cut off at the defaults.
|
||||||
SWRP_MAX_REQUEST_SIZE: 2G
|
SWRP_MAX_REQUEST_SIZE: 2G
|
||||||
SWRP_MAX_REQUEST_TIME_MS: "600000"
|
SWRP_MAX_REQUEST_TIME_MS: "600000"
|
||||||
# A clone larger than 1 GiB is cut off at the default.
|
|
||||||
SWRP_MAX_RESPONSE_SIZE: 4G
|
|
||||||
networks:
|
networks:
|
||||||
- site
|
- site
|
||||||
- traefik
|
- traefik
|
||||||
labels:
|
labels:
|
||||||
traefik.enable: "true"
|
traefik.enable: "true"
|
||||||
# rproxy is on two networks; traefik reaches it on this one.
|
|
||||||
traefik.docker.network: traefik
|
|
||||||
traefik.http.routers.git.rule: Host(`git.example`)
|
traefik.http.routers.git.rule: Host(`git.example`)
|
||||||
traefik.http.routers.git.entrypoints: websecure
|
traefik.http.routers.git.entrypoints: websecure
|
||||||
traefik.http.routers.git.tls.certresolver: letsencrypt
|
traefik.http.routers.git.tls.certresolver: letsencrypt
|
||||||
@@ -489,8 +471,8 @@ services:
|
|||||||
SWWAF_INSTANCE_NAME: fsn1/servewaf
|
SWWAF_INSTANCE_NAME: fsn1/servewaf
|
||||||
# The Actions runners ask for work all day; never count them.
|
# The Actions runners ask for work all day; never count them.
|
||||||
SWWAF_RATE_LIMIT_EXEMPT_NETS: 10.0.4.0/24
|
SWWAF_RATE_LIMIT_EXEMPT_NETS: 10.0.4.0/24
|
||||||
# Raised with rproxy's response limit, to four times the default
|
# A clone larger than 1 GiB needs the response limit and the
|
||||||
# each, so that one clone of that size breaks no byte limit.
|
# byte limits raised together.
|
||||||
SWWAF_BYTES_LIMIT_PER_MINUTE: 16G
|
SWWAF_BYTES_LIMIT_PER_MINUTE: 16G
|
||||||
SWWAF_BYTES_LIMIT_PER_HOUR: 80G
|
SWWAF_BYTES_LIMIT_PER_HOUR: 80G
|
||||||
SWWAF_BYTES_LIMIT_PER_DAY: 800G
|
SWWAF_BYTES_LIMIT_PER_DAY: 800G
|
||||||
@@ -516,15 +498,11 @@ request by request.
|
|||||||
|
|
||||||
### `rproxy` beside the app
|
### `rproxy` beside the app
|
||||||
|
|
||||||
The app is built on the `smallwebwaf` image, with `rproxy` inside its container.
|
The app built on the `smallwebwaf` image, with `rproxy` inside its container and
|
||||||
`app/` holds the `Dockerfile` and `app.run` from "`rproxy` in the app's
|
`503` for every request while `servewaf` is down; and a `servewaf` that observes
|
||||||
container". `rproxy` answers `503` to every request while `servewaf` is down,
|
before it enforces, looks addresses up in a local database rather than GeoJS,
|
||||||
and `servewaf`:
|
sends alerts to ntfy, and reads its token from a file. `app/` holds the
|
||||||
|
`Dockerfile` and `app.run` from "`rproxy` in the app's container".
|
||||||
- observes before it enforces;
|
|
||||||
- looks addresses up in a local database rather than GeoJS;
|
|
||||||
- sends alerts to ntfy;
|
|
||||||
- reads its ntfy token from a file.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -561,15 +539,13 @@ volumes:
|
|||||||
servewaf-state:
|
servewaf-state:
|
||||||
```
|
```
|
||||||
|
|
||||||
The database is mounted by its directory, not by file, so that a replaced
|
The database and the token are mounted by their directories, not by file, so
|
||||||
database is seen. A token file belongs to uid 65532, the `smallwebwaf` user,
|
that a replaced file is seen. A token file belongs to uid 65532, the
|
||||||
with mode `0400`. This one holds an access token the ntfy server issued, which
|
`smallwebwaf` user, with mode `0400`:
|
||||||
`ntfy token add <user>` prints there:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
mkdir -p /srv/smallwebwaf/tokens
|
mkdir -p /srv/smallwebwaf/tokens
|
||||||
# <token> is the access token that ntfy printed.
|
openssl rand -hex 32 > /srv/smallwebwaf/tokens/ntfy
|
||||||
echo '<token>' > /srv/smallwebwaf/tokens/ntfy
|
|
||||||
chown 65532:65532 /srv/smallwebwaf/tokens/ntfy
|
chown 65532:65532 /srv/smallwebwaf/tokens/ntfy
|
||||||
chmod 0400 /srv/smallwebwaf/tokens/ntfy
|
chmod 0400 /srv/smallwebwaf/tokens/ntfy
|
||||||
```
|
```
|
||||||
@@ -617,9 +593,8 @@ the request reaches the app.
|
|||||||
A refused visitor is answered with `SWWAF_BAN_RESPONSE`. A `block` rule or a
|
A refused visitor is answered with `SWWAF_BAN_RESPONSE`. A `block` rule or a
|
||||||
Core Rule Set match answers `403` and bans nobody, since the Core Rule Set has
|
Core Rule Set match answers `403` and bans nobody, since the Core Rule Set has
|
||||||
false positives; such refusals count toward `SWWAF_ERROR_BURST_THRESHOLD`
|
false positives; such refusals count toward `SWWAF_ERROR_BURST_THRESHOLD`
|
||||||
instead. A request over the header limits of step 12 is answered `431`, and bans
|
instead. `rproxy` itself answers `405` to a method it does not pass on, and the
|
||||||
nobody either. `rproxy` itself answers `405` to a method it does not pass on,
|
status for a size or time limit, such as `413` for a body too large.
|
||||||
and the status for a size or time limit, such as `413` for a body too large.
|
|
||||||
|
|
||||||
### Clients and bans
|
### Clients and bans
|
||||||
|
|
||||||
@@ -667,9 +642,10 @@ aside as `<name>.bad`, and never stops the running process (see
|
|||||||
`Dockerfile` installs a package with `nix-env -iA nixpkgs.<name>` and gets the
|
`Dockerfile` installs a package with `nix-env -iA nixpkgs.<name>` and gets the
|
||||||
same package each time it is built (see [`docs/image.md`](docs/image.md)).
|
same package each time it is built (see [`docs/image.md`](docs/image.md)).
|
||||||
- runit, a service supervisor, runs `smallwebwaf` as the `smallwebwaf` user (uid
|
- runit, a service supervisor, runs `smallwebwaf` as the `smallwebwaf` user (uid
|
||||||
and gid 65532) and starts it again when it exits. The `run` script of
|
and gid 65532) and starts it again when it exits. In an app's image built on
|
||||||
`servewaf` gives the state directory to the `smallwebwaf` user at each start,
|
the `smallwebwaf` image, it runs `rproxy` and the app side by side, each as
|
||||||
so a volume needs no change of owner.
|
its own user. The `run` script of `servewaf` gives the state directory to the
|
||||||
|
`smallwebwaf` user at each start, so a volume needs no change of owner.
|
||||||
- `rproxy` and `servewaf` use no token between them. `servewaf`'s own endpoints,
|
- `rproxy` and `servewaf` use no token between them. `servewaf`'s own endpoints,
|
||||||
under `/_smallwebwaf/`, are for your admin tools and your metrics collector,
|
under `/_smallwebwaf/`, are for your admin tools and your metrics collector,
|
||||||
at its own address on the private network; nothing routes the internet there.
|
at its own address on the private network; nothing routes the internet there.
|
||||||
@@ -700,15 +676,12 @@ aside as `<name>.bad`, and never stops the running process (see
|
|||||||
## Rationale
|
## Rationale
|
||||||
|
|
||||||
Small self-hosted sites receive a great deal of traffic nobody asked for:
|
Small self-hosted sites receive a great deal of traffic nobody asked for:
|
||||||
|
scrapers that ignore `robots.txt` and crawl every commit of every repository on
|
||||||
- scrapers that ignore `robots.txt` and crawl every commit of every repository
|
a public git server, scanners walking through lists of WordPress and `.env`
|
||||||
on a public git server;
|
paths, and credential-guessing bots. Most of it comes from a small number of
|
||||||
- scanners walking through lists of WordPress and `.env` paths;
|
hosting networks and countries. A single-person operation has no abuse desk and
|
||||||
- credential-guessing bots.
|
no CDN contract. It needs something small that can be put in front of its
|
||||||
|
services and left alone.
|
||||||
Most of it comes from a small number of hosting networks and countries. A
|
|
||||||
single-person operation has no abuse desk and no CDN contract. It needs
|
|
||||||
something small that can be put in front of its services and left alone.
|
|
||||||
|
|
||||||
The existing tools each solve part of this. Rule-based firewalls catch attack
|
The existing tools each solve part of this. Rule-based firewalls catch attack
|
||||||
payloads but do not limit request rates. Rate limiters count requests but cannot
|
payloads but do not limit request rates. Rate limiters count requests but cannot
|
||||||
@@ -727,10 +700,8 @@ The tests and the linter run in Docker, as phases of the `Dockerfile`, and never
|
|||||||
on the host.
|
on the host.
|
||||||
|
|
||||||
- `script/bootstrap`: installs on the host what the other scripts need, with
|
- `script/bootstrap`: installs on the host what the other scripts need, with
|
||||||
`make bootstrap`:
|
`make bootstrap`: `make`, `git` and `curl`; Go, for `gofmt`; node, yarn and
|
||||||
- `make`, `git` and `curl`;
|
prettier.
|
||||||
- Go, for `gofmt`;
|
|
||||||
- node, yarn and prettier.
|
|
||||||
- `script/setup`: readies a fresh clone: `script/bootstrap`, then
|
- `script/setup`: readies a fresh clone: `script/bootstrap`, then
|
||||||
`script/install-precommit`. `make setup`.
|
`script/install-precommit`. `make setup`.
|
||||||
- `script/projectname`: prints `smallwebwaf`, which the other scripts tag their
|
- `script/projectname`: prints `smallwebwaf`, which the other scripts tag their
|
||||||
@@ -757,34 +728,26 @@ on the host.
|
|||||||
`make build`.
|
`make build`.
|
||||||
- `script/run`: builds `bin/smallwebwaf` and runs `servewaf` and `rproxy` on the
|
- `script/run`: builds `bin/smallwebwaf` and runs `servewaf` and `rproxy` on the
|
||||||
host, with the settings in your environment. `make run`. Unless your settings
|
host, with the settings in your environment. `make run`. Unless your settings
|
||||||
say otherwise:
|
say otherwise, `rproxy` listens on port 8080, in front of an app at
|
||||||
- `rproxy` listens on port 8080, in front of an app at
|
`http://127.0.0.1:8081`; `servewaf` listens on port 8090; its state files go
|
||||||
`http://127.0.0.1:8081`;
|
in `bin/state`; its rule files are those of `share/rules.d`.
|
||||||
- `servewaf` listens on port 8090;
|
|
||||||
- its state files go in `bin/state`;
|
|
||||||
- its rule files are those of `share/rules.d`.
|
|
||||||
- `script/example-app`: builds the image and the example app
|
- `script/example-app`: builds the image and the example app
|
||||||
(`deploy/example-app`) on it, runs the app with `rproxy` in its container and
|
(`deploy/example-app`) on it, runs the app with `rproxy` in its container and
|
||||||
`servewaf` in another, and checks them. Then it removes the containers, the
|
`servewaf` in another, and checks them: the health checks; a request passed to
|
||||||
network, the volumes and the images. Needs network access. `make example-app`.
|
the app; a ban for a broken limit, and a ban for a probe made permanent; that
|
||||||
It checks:
|
only `servewaf`'s container changes the state directory's owner; with
|
||||||
- the health checks;
|
`servewaf` stopped, the app still answering through `rproxy`; with `servewaf`
|
||||||
- a request passed to the app;
|
started again, bans applying again; orderly stops with `sv stop` and
|
||||||
- a ban for a broken limit, and a ban for a probe made permanent;
|
`docker stop`; bans surviving a new container on the same volume; a setting
|
||||||
- that only `servewaf`'s container changes the state directory's owner;
|
given as a file. Then it removes the containers, the network, the volumes and
|
||||||
- with `servewaf` stopped, the app still answering through `rproxy`;
|
the images. Needs network access. `make example-app`.
|
||||||
- with `servewaf` started again, bans applying again;
|
|
||||||
- orderly stops with `sv stop` and `docker stop`;
|
|
||||||
- bans surviving a new container on the same volume;
|
|
||||||
- a setting given as a file.
|
|
||||||
- `script/gitea-requests`: builds the image and runs gitea 1.27.3 behind
|
- `script/gitea-requests`: builds the image and runs gitea 1.27.3 behind
|
||||||
`rproxy` and `servewaf`, three containers on one network. Every setting is at
|
`rproxy` and `servewaf`, three containers on one network, every setting at its
|
||||||
its default, but for those that join the containers and
|
default but those that join the containers. It sends through them a sample of
|
||||||
`SWWAF_LOG_REQUEST_HEADERS`, which adds the header that labels each request.
|
gitea's ordinary use and the requests the Core Rule Set is known to refuse,
|
||||||
It sends through them a sample of gitea's ordinary use and the requests the
|
and prints what `servewaf` did with each. Needs network access, and a docker
|
||||||
Core Rule Set is known to refuse, and prints what `servewaf` did with each.
|
daemon that pushes to a registry on `localhost` over plain HTTP.
|
||||||
Needs network access, and a docker daemon that pushes to a registry on
|
`make gitea-requests`.
|
||||||
`localhost` over plain HTTP. `make gitea-requests`.
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user