Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdabf4974b | ||
|
|
a9dc59546b | ||
|
|
2ac2b671f6 | ||
|
|
f408f42b28 | ||
|
|
acceecc7f8 |
No files matched your search
@@ -8,8 +8,7 @@ scanners and other abusers before they reach the app.
|
|||||||
A web application firewall reads each request on its way to a web app and
|
A web application firewall reads each request on its way to a web app and
|
||||||
refuses the ones that look like abuse. `smallwebwaf` is also a reverse proxy: it
|
refuses the ones that look like abuse. `smallwebwaf` is also a reverse proxy: it
|
||||||
accepts requests on the app's behalf, passes them to the app, and passes the
|
accepts requests on the app's behalf, passes them to the app, and passes the
|
||||||
app's answers back. A request it does not refuse reaches the app as it came,
|
app's answers back.
|
||||||
apart from a few headers (see "Design" below).
|
|
||||||
|
|
||||||
It needs no setting. Every setting has a default chosen for a service on the
|
It needs no setting. Every setting has a default chosen for a service on the
|
||||||
open internet. [`SPEC.md`](SPEC.md) says how `smallwebwaf` behaves and why. The
|
open internet. [`SPEC.md`](SPEC.md) says how `smallwebwaf` behaves and why. The
|
||||||
@@ -18,10 +17,10 @@ reference documents under [`docs/`](docs/) are listed under "Documents" below.
|
|||||||
## What it protects against
|
## What it protects against
|
||||||
|
|
||||||
- Scrapers that fetch every page of a site, all day. Rate limiting gives each
|
- Scrapers that fetch every page of a site, all day. Rate limiting gives each
|
||||||
client 1000 requests a minute, 10,000 an hour and 50,000 a day. Byte limits
|
client 6000 requests a minute, 100,000 an hour and 1,000,000 a day. Byte
|
||||||
give it 10 GiB a minute, 20 GiB an hour and 50 GiB a day. A client over a
|
limits give it 4 GiB a minute, 20 GiB an hour and 200 GiB a day. A client over
|
||||||
limit is refused and banned. The first ban lasts an hour. Each repeat within a
|
a limit is refused and banned. The first ban lasts an hour. Each repeat within
|
||||||
day makes the next ban three times as long. A ban that would last more than
|
a day makes the next ban three times as long. A ban that would last more than
|
||||||
seven days is permanent.
|
seven days is permanent.
|
||||||
- Scanners that probe for secrets, such as `/.env` or `/.git/config`. The
|
- Scanners that probe for secrets, such as `/.env` or `/.git/config`. The
|
||||||
default rule file bans such a client for seven days at its first probe. Its
|
default rule file bans such a client for seven days at its first probe. Its
|
||||||
@@ -30,8 +29,8 @@ reference documents under [`docs/`](docs/) are listed under "Documents" below.
|
|||||||
of your own, or an entry in `SWWAF_TRAP_PATHS`, bans those the same way.
|
of your own, or an entry in `SWWAF_TRAP_PATHS`, bans those the same way.
|
||||||
- Attacks carried in a request's URL or headers, such as SQL injection and path
|
- Attacks carried in a request's URL or headers, such as SQL injection and path
|
||||||
traversal. The OWASP Core Rule Set is a published set of rules that recognise
|
traversal. The OWASP Core Rule Set is a published set of rules that recognise
|
||||||
common web attacks. Coraza, a Go engine, runs those rules (step 11 under "What
|
common web attacks. Coraza, a Go engine, runs those rules (see "Attack
|
||||||
happens to a request" below). A request the rules take for an attack is
|
detection" in [`SPEC.md`](SPEC.md)). A request the rules take for an attack is
|
||||||
refused.
|
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 netblocks and countries;
|
- refuse whole netblocks and countries;
|
||||||
@@ -142,89 +141,32 @@ while working on it, see "Entrypoints" below.
|
|||||||
|
|
||||||
## What happens to a request
|
## What happens to a request
|
||||||
|
|
||||||
Two terms recur below. A clear sign of attack is a request that matches a rule
|
`smallwebwaf` handles each request in fifteen steps, in this order. Once a step
|
||||||
whose action is `ban`, or asks for a trap path (steps 9 and 10). An offence is a
|
refuses a request, the steps after it are skipped, apart from step 15, and
|
||||||
request that counts against its client in its history. It is one of:
|
nothing of the request reaches the app. "What happens to a request" in
|
||||||
|
[`SPEC.md`](SPEC.md) gives each step in full, under the same numbers.
|
||||||
|
|
||||||
- a clear sign of attack;
|
1. The health check, `GET /_smallwebwaf/healthz`, is answered at once.
|
||||||
- a broken limit;
|
2. The client is identified.
|
||||||
- a refusal by a rule, by the Core Rule Set or for a missing or wrong token.
|
3. The static lists, `SWWAF_ALLOW_NETS` and `SWWAF_DENY_NETS`.
|
||||||
|
4. The bans.
|
||||||
|
5. The lookup of the client's AS number and country.
|
||||||
|
6. The country lists.
|
||||||
|
7. The reputation sources.
|
||||||
|
8. The client's limit percentage.
|
||||||
|
9. The rate limits.
|
||||||
|
10. The trap paths.
|
||||||
|
11. The rule files.
|
||||||
|
12. The Core Rule Set.
|
||||||
|
13. `smallwebwaf`'s own endpoints, under `/_smallwebwaf/`.
|
||||||
|
14. The request is passed to the app.
|
||||||
|
15. After the answer: the byte limits, the error burst and the client's history.
|
||||||
|
|
||||||
`smallwebwaf` handles each request in this order. Once a step refuses a request,
|
A refused visitor is answered with `SWWAF_BAN_RESPONSE`, `403` by default (see
|
||||||
the steps after it are skipped, apart from step 13. Nothing of a refused request
|
"Ban lengths, response and scope" in [`SPEC.md`](SPEC.md)). A `block` rule or a
|
||||||
reaches the app. "What happens to a request" in [`SPEC.md`](SPEC.md) gives each
|
Core Rule Set match answers `403`. A missing or wrong token gets `401`. A
|
||||||
step in full.
|
request over a size or time limit gets the status for that limit, such as `413`
|
||||||
|
for a body too large (see "Size and time limits" in [`SPEC.md`](SPEC.md)).
|
||||||
1. `GET /_smallwebwaf/healthz` is answered `200` with `ok` at once, before any
|
|
||||||
check and without asking the app.
|
|
||||||
2. The client is worked out. A netblock is a range of addresses, written as an
|
|
||||||
address and a prefix length: `203.0.113.0/24`. `SWWAF_TRUSTED_PROXIES` lists
|
|
||||||
the netblocks traefik comes from, by default the private address ranges. For
|
|
||||||
a TCP peer inside them, `X-Forwarded-For` is read from the right, and the
|
|
||||||
first address outside those netblocks is the client. A peer outside them is
|
|
||||||
the client itself. An IPv6 client is its group: the whole netblock around its
|
|
||||||
address, a /64 by default; `SWWAF_IPV6_GROUP_PREFIX` sets its length.
|
|
||||||
3. A client in `SWWAF_ALLOW_NETS` skips steps 4 to 11, the byte limits and the
|
|
||||||
error burst, but still needs a token at step 12. A client in
|
|
||||||
`SWWAF_DENY_NETS` is refused.
|
|
||||||
4. A client under a ban is refused. If the ban was made for a clear sign of
|
|
||||||
attack, this request makes it permanent.
|
|
||||||
5. The client's AS number and country are looked up, unless
|
|
||||||
`SWWAF_LOOKUP_SOURCE` is `off`. An AS number names the network an address
|
|
||||||
belongs to, such as `AS64496`.
|
|
||||||
6. The country lists are checked. A client from a country in
|
|
||||||
`SWWAF_DENIED_COUNTRIES` is refused. When
|
|
||||||
`SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` is set, a client from any other country
|
|
||||||
is refused, and so is a client without a country. Such a refusal is not an
|
|
||||||
offence.
|
|
||||||
7. The reputation sources are checked: the blocklists, the CrowdSec decision
|
|
||||||
list, the DNS blocklists and AbuseIPDB. A client on the CrowdSec decision
|
|
||||||
list is banned until the decision ends. Any other source refuses the client,
|
|
||||||
lowers its limits or only notes it, as `SWWAF_BLOCKLIST_ACTION` and
|
|
||||||
`SWWAF_REPUTATION_ACTION` say. No request waits for a query.
|
|
||||||
8. Rate limiting counts the request, unless the client or the path is exempt. A
|
|
||||||
request that takes the client over a limit is refused and bans the client.
|
|
||||||
9. A request for a path in `SWWAF_TRAP_PATHS` is refused and bans the client, as
|
|
||||||
a clear sign of attack.
|
|
||||||
10. The rule files are checked. A `log` rule notes the match. A `block` rule
|
|
||||||
refuses the request. A `ban` rule refuses it and bans the client, as a clear
|
|
||||||
sign of attack.
|
|
||||||
11. The Core Rule Set inspects the request's method, URL and headers, and, once
|
|
||||||
`SWWAF_WAF_BODY_LIMIT` is set, form, JSON and XML bodies. A request it takes
|
|
||||||
for an attack is refused while `SWWAF_WAF_MODE` is `block`, the default, and
|
|
||||||
let through while it is `detect`. It bans no one by itself.
|
|
||||||
12. A request under `/_smallwebwaf/` is answered here by `smallwebwaf`, after
|
|
||||||
its token is checked. Any other request is passed to the app. The app's
|
|
||||||
answer is streamed back within the size and time limits.
|
|
||||||
13. Once the answer has ended, a request passed to the app has its bytes counted
|
|
||||||
for the byte limits. If a rule, a trap path, the Core Rule Set or a token
|
|
||||||
check refused the request, the refusal is counted for the error burst. The
|
|
||||||
client's history and the anomaly counters are updated. More than
|
|
||||||
`SWWAF_ERROR_BURST_THRESHOLD` refusals of one client in a minute ban it.
|
|
||||||
|
|
||||||
In `observe` mode (`SWWAF_MODE=observe`), `smallwebwaf` makes no ban of its own.
|
|
||||||
It refuses a request only for a size or time limit, or for a missing or wrong
|
|
||||||
token. A request it would otherwise have refused is passed to the app, and its
|
|
||||||
log line says what `enforce` mode would have done. Observe mode is for trying a
|
|
||||||
configuration before enforcing it.
|
|
||||||
|
|
||||||
What a refused visitor sees:
|
|
||||||
|
|
||||||
- `SWWAF_BAN_RESPONSE`, by default `403` with the plain text `Forbidden`. It can
|
|
||||||
be `429` instead, or `close`, which closes the connection, so that traefik
|
|
||||||
answers `502`. It answers:
|
|
||||||
- a ban or a broken rate limit;
|
|
||||||
- a `ban` rule or a trap path;
|
|
||||||
- `SWWAF_DENY_NETS` and the country lists;
|
|
||||||
- a reputation source whose action is `deny`.
|
|
||||||
- `403` for a `block` rule and the Core Rule Set.
|
|
||||||
- For a size or time limit:
|
|
||||||
- `431` for headers over their limit;
|
|
||||||
- `413` for a body over its limit;
|
|
||||||
- `408` for a client too slow to send its body;
|
|
||||||
- `502` or `504` when the app passes a limit or cannot be reached.
|
|
||||||
- `401` from an endpoint of `smallwebwaf`'s own, for a request without its
|
|
||||||
token.
|
|
||||||
|
|
||||||
## Settings you are likely to touch
|
## Settings you are likely to touch
|
||||||
|
|
||||||
@@ -233,8 +175,11 @@ Each setting is an environment variable of the app's container, such as
|
|||||||
[`docs/settings.md`](docs/settings.md) lists every setting, its default and the
|
[`docs/settings.md`](docs/settings.md) lists every setting, its default and the
|
||||||
values it takes. The ones most deployments touch:
|
values it takes. The ones most deployments touch:
|
||||||
|
|
||||||
- `SWWAF_MODE=observe`: log what would have been refused, refuse almost nothing,
|
- `SWWAF_MODE=observe`: `smallwebwaf` makes no ban of its own, and refuses a
|
||||||
ban no one. For trying a configuration.
|
request only for a size or time limit or a missing or wrong token. A request
|
||||||
|
it would have refused is passed to the app, and its log line says what
|
||||||
|
`enforce` mode would have done. For trying a configuration before enforcing
|
||||||
|
it.
|
||||||
- `SWWAF_ALLOW_NETS`: netblocks you trust, such as your monitoring. Their
|
- `SWWAF_ALLOW_NETS`: netblocks you trust, such as your monitoring. Their
|
||||||
clients are refused only for a size or time limit or a missing or wrong token.
|
clients are refused only for a size or time limit or a missing or wrong token.
|
||||||
- `SWWAF_RATE_LIMIT_EXEMPT_NETS`: netblocks exempt from rate limiting and the
|
- `SWWAF_RATE_LIMIT_EXEMPT_NETS`: netblocks exempt from rate limiting and the
|
||||||
@@ -376,50 +321,32 @@ nixpkgs fixed at one release. An app's `Dockerfile` installs a package with
|
|||||||
[`docs/image.md`](docs/image.md) lists what the image holds and how each part is
|
[`docs/image.md`](docs/image.md) lists what the image holds and how each part is
|
||||||
pinned.
|
pinned.
|
||||||
|
|
||||||
- The container's first process is `runsvinit`, which starts runit, a service
|
- runit, a service supervisor, runs `smallwebwaf` and the app side by side, each
|
||||||
supervisor. runit runs the `run` script of each directory under
|
as its own user, and starts either again when it exits. The app cannot touch
|
||||||
`/etc/service`, and runs it again a second after its program exits.
|
the state files or the process of `smallwebwaf`.
|
||||||
`smallwebwaf` runs as the `smallwebwaf` user (uid 65532) and the app as a user
|
- `smallwebwaf` listens on port 8080 and passes requests to the app on
|
||||||
of its own. So the app cannot touch the state files or the process of
|
`127.0.0.1:8081`. The app listens on `127.0.0.1` only, so nothing reaches it
|
||||||
`smallwebwaf`.
|
except through `smallwebwaf`. `smallwebwaf`'s own endpoints are under
|
||||||
- `smallwebwaf` listens on port 8080, on every address. traefik sends the app's
|
`/_smallwebwaf/`, on the same port.
|
||||||
requests there. `smallwebwaf` passes them to the app on `127.0.0.1:8081`. The
|
- The app sees the headers traefik sent, with traefik's address added to
|
||||||
app listens on `127.0.0.1` only, so nothing reaches it except through
|
`X-Forwarded-For`, from a TCP peer on `127.0.0.1`. So the app must trust
|
||||||
`smallwebwaf`. The health check, the metrics and the ban management are on the
|
`127.0.0.1` and `::1` for forwarded headers, besides the private address
|
||||||
same port, under `/_smallwebwaf/`. A request for that prefix never reaches the
|
ranges.
|
||||||
app.
|
- Both processes start with the container's environment variables, so every
|
||||||
- The app sees what it would see from traefik directly: the same `Host`, the
|
setting of `smallwebwaf` starts with `SWWAF_`, and the app can read a token
|
||||||
same `X-Forwarded-Proto`, and `X-Forwarded-For` with traefik's address added
|
given as one. A token given as a file that only the `smallwebwaf` user can
|
||||||
at the end. The app's TCP peer is `smallwebwaf` on `127.0.0.1`. So the app
|
read is out of its reach (see "Settings given as files" in
|
||||||
must trust `127.0.0.1` and `::1` for forwarded headers, besides the private
|
|
||||||
address ranges. An app that keeps its usual trusted proxies sees every visitor
|
|
||||||
as `127.0.0.1`. Each request also carries its id in `X-Request-ID`. Any
|
|
||||||
`X-Client-ASN` or `X-Client-Country` header a client sent is removed.
|
|
||||||
- Both processes start with the container's environment variables. That is why
|
|
||||||
every setting of `smallwebwaf` starts with `SWWAF_`. A token given as an
|
|
||||||
environment variable is readable by the app. A token given as a file that only
|
|
||||||
the `smallwebwaf` user can read is not (see "Settings given as files" in
|
|
||||||
[`docs/settings.md`](docs/settings.md)).
|
[`docs/settings.md`](docs/settings.md)).
|
||||||
- The state files live in `/var/lib/smallwebwaf`. Without a volume there, they
|
- The state files live in `/var/lib/smallwebwaf`. A volume mounted there keeps
|
||||||
survive a restart of the container and are lost when a deploy replaces it. The
|
them across a deploy.
|
||||||
`run` script gives that directory and every file in it to the `smallwebwaf`
|
- `docker stop` stops both processes at once. `smallwebwaf` gives the requests
|
||||||
user at each start, so a host directory mounted there needs no change of
|
in progress five seconds to finish, so the app must keep answering for five
|
||||||
owner.
|
|
||||||
- The image's health check passes while `smallwebwaf` answers
|
|
||||||
`GET /_smallwebwaf/healthz` and the app accepts connections. traefik sends a
|
|
||||||
container no requests until it is healthy.
|
|
||||||
- When `smallwebwaf` or the app exits, runit starts it again a second later. The
|
|
||||||
other process keeps running. While either is down, the client gets `502`. A
|
|
||||||
`smallwebwaf` that stops at start, as on an invalid setting, is started again
|
|
||||||
every second until the cause is fixed, and logs its message each time.
|
|
||||||
- `docker stop` has runit send `SIGTERM` to both processes at once.
|
|
||||||
`smallwebwaf` gives the requests in progress five seconds to finish and writes
|
|
||||||
its state files. To let them finish, the app must keep answering for five
|
|
||||||
seconds after `SIGTERM`.
|
seconds after `SIGTERM`.
|
||||||
|
|
||||||
"Deployment" in [`SPEC.md`](SPEC.md) has the rest: moving `smallwebwaf` to
|
"Deployment" in [`SPEC.md`](SPEC.md) has the rest: the health check, what
|
||||||
another port, the health check's timing, and the forwarded headers.
|
happens when a process stops, moving `smallwebwaf` to another port, and the
|
||||||
"Architecture" in [`SPEC.md`](SPEC.md) lists the packages and what each does.
|
forwarded headers. "Architecture" in [`SPEC.md`](SPEC.md) lists the packages and
|
||||||
|
what each does.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ its rules.
|
|||||||
thresholds are evaluated.
|
thresholds are evaluated.
|
||||||
|
|
||||||
In `SWWAF_MODE=observe`, every refusal above is logged as what would have
|
In `SWWAF_MODE=observe`, every refusal above is logged as what would have
|
||||||
happened, nothing is refused, and no ban is made. The size and time limits and
|
happened, nothing is refused, and `smallwebwaf` makes no ban of its own. The
|
||||||
the token check still apply.
|
size and time limits and the token check still apply.
|
||||||
|
|
||||||
## Counting method
|
## Counting method
|
||||||
|
|
||||||
@@ -183,8 +183,8 @@ the token check still apply.
|
|||||||
GeoJS answers, the reputation verdicts and scores, the DNS queries under way
|
GeoJS answers, the reputation verdicts and scores, the DNS queries under way
|
||||||
and the anomaly counters have fixed bounds, listed under "Fixed limits" in
|
and the anomaly counters have fixed bounds, listed under "Fixed limits" in
|
||||||
`docs/settings.md`. Past its bound, the GeoJS answer or anomaly counter used
|
`docs/settings.md`. Past its bound, the GeoJS answer or anomaly counter used
|
||||||
longest ago goes first, and the verdict or score fetched longest ago. Past
|
longest ago goes first, and the verdict or score fetched longest ago. Past the
|
||||||
that bound, a zone is asked at the client's next request.
|
bound on DNS queries under way, a zone is asked at the client's next request.
|
||||||
|
|
||||||
## Bans
|
## Bans
|
||||||
|
|
||||||
@@ -215,9 +215,12 @@ to a ban, each by its own rule:
|
|||||||
permanent this way.
|
permanent this way.
|
||||||
- Once that ban has run out, the netblock is served like any other. Its next
|
- Once that ban has run out, the netblock is served like any other. Its next
|
||||||
clear sign of attack bans it permanently at once.
|
clear sign of attack bans it permanently at once.
|
||||||
- A broken limit: a request over a rate limit, a response that takes the
|
- A broken limit, one of:
|
||||||
client's byte total past a byte limit, or more than
|
- a request over a rate limit;
|
||||||
`SWWAF_ERROR_BURST_THRESHOLD` refusals within a minute.
|
- a response that takes the client's byte total past a byte limit;
|
||||||
|
- more than `SWWAF_ERROR_BURST_THRESHOLD` refusals within a minute.
|
||||||
|
|
||||||
|
The bans for a broken limit:
|
||||||
- The first such ban lasts `SWWAF_LIMIT_BAN_DURATION` (default `1h`), with
|
- The first such ban lasts `SWWAF_LIMIT_BAN_DURATION` (default `1h`), with
|
||||||
the cause `limit`. So does one that comes more than
|
the cause `limit`. So does one that comes more than
|
||||||
`SWWAF_LIMIT_BAN_REPEAT_WINDOW` (default `24h`) after the last such ban
|
`SWWAF_LIMIT_BAN_REPEAT_WINDOW` (default `24h`) after the last such ban
|
||||||
@@ -233,7 +236,7 @@ to a ban, each by its own rule:
|
|||||||
A Core Rule Set match refuses only the request it matched, with `403` and
|
A Core Rule Set match refuses only the request it matched, with `403` and
|
||||||
without a ban, because the Core Rule Set has false positives. A `block` rule
|
without a ban, because the Core Rule Set has false positives. A `block` rule
|
||||||
does the same. Those refusals count toward the error burst, so a client that
|
does the same. Those refusals count toward the error burst, so a client that
|
||||||
keeps setting them off is banned under the second rule.
|
keeps setting them off is banned for a broken limit.
|
||||||
|
|
||||||
A client on the CrowdSec decision list is banned until the decision ends, with
|
A client on the CrowdSec decision list is banned until the decision ends, with
|
||||||
the cause `crowdsec`. Only a client that sends a request is banned, so a long
|
the cause `crowdsec`. Only a client that sends a request is banned, so a long
|
||||||
@@ -328,8 +331,8 @@ Conventions:
|
|||||||
- `SWWAF_ADMIN_TOKEN` switches on the ban endpoints and
|
- `SWWAF_ADMIN_TOKEN` switches on the ban endpoints and
|
||||||
`/_smallwebwaf/clients/<ip>`. The metrics are off until `SWWAF_METRICS_TOKEN`
|
`/_smallwebwaf/clients/<ip>`. The metrics are off until `SWWAF_METRICS_TOKEN`
|
||||||
is set, since without a token they would be open to anyone on the internet.
|
is set, since without a token they would be open to anyone on the internet.
|
||||||
The two are separate, so a scraper that holds the metrics token cannot manage
|
The two are separate, so the metrics collector, which holds the metrics token,
|
||||||
bans.
|
cannot manage bans.
|
||||||
- A token is a long random value, since the endpoints can be reached from the
|
- A token is a long random value, since the endpoints can be reached from the
|
||||||
internet. One shorter than 32 characters stops the start. The app starts with
|
internet. One shorter than 32 characters stops the start. The app starts with
|
||||||
the same environment variables as `smallwebwaf`, so it can read a token set as
|
the same environment variables as `smallwebwaf`, so it can read a token set as
|
||||||
@@ -365,16 +368,23 @@ Conventions:
|
|||||||
- `SWWAF_ALLOW_NETS` skips the checks step 3 names, for monitoring or the
|
- `SWWAF_ALLOW_NETS` skips the checks step 3 names, for monitoring or the
|
||||||
owner's own networks. `SWWAF_RATE_LIMIT_EXEMPT_NETS` bypasses the request and
|
owner's own networks. `SWWAF_RATE_LIMIT_EXEMPT_NETS` bypasses the request and
|
||||||
byte limits only; the error burst, attack detection and bans still apply.
|
byte limits only; the error burst, attack detection and bans still apply.
|
||||||
- Many clients share one address (mobile carriers, offices, Tor), and so share
|
- Many people can share one address, as in an office, on a mobile carrier or
|
||||||
its limits and its bans. `SWWAF_ALLOW_NETS` and `SWWAF_RATE_LIMIT_EXEMPT_NETS`
|
through Tor. They are one client, and share its limits and its bans. The
|
||||||
take known shared addresses out.
|
default limits are sized for about 40 busy people on one address. A known
|
||||||
|
shared address busier than that belongs in `SWWAF_ALLOW_NETS` or
|
||||||
|
`SWWAF_RATE_LIMIT_EXEMPT_NETS`.
|
||||||
|
|
||||||
### Request rate limits, per client
|
### Request rate limits, per client
|
||||||
|
|
||||||
Breaking one bans the client, so the defaults sit several times above what one
|
Breaking one bans the client, so the defaults sit about twice above what 40
|
||||||
busy person produces. A browser loading a heavy page makes a few hundred
|
people busy with heavy web apps make together on one address. Each such person
|
||||||
requests, and a git clone a handful. A machine that talks to the app all day can
|
makes about 75 requests in a busy minute, and 1,250 an hour for ten hours a day.
|
||||||
still pass the day limit. Its address belongs in `SWWAF_RATE_LIMIT_EXEMPT_NETS`.
|
|
||||||
|
A client slower than those 40 people cannot be told from them by counting. Only
|
||||||
|
the other checks can catch an abuser that slow. They include the trap paths, the
|
||||||
|
rule files, the Core Rule Set and the blocklists. An abuser that trips none of
|
||||||
|
them is not caught. A machine that talks to the app all day can still pass the
|
||||||
|
day limit. Its address belongs in `SWWAF_RATE_LIMIT_EXEMPT_NETS`.
|
||||||
|
|
||||||
`SWWAF_RATE_LIMIT_EXEMPT_PATHS` takes path prefixes out of the count, such as
|
`SWWAF_RATE_LIMIT_EXEMPT_PATHS` takes path prefixes out of the count, such as
|
||||||
static assets or health checks; their bytes are not counted either. A prefix is
|
static assets or health checks; their bytes are not counted either. A prefix is
|
||||||
@@ -387,9 +397,12 @@ matched the same way.
|
|||||||
|
|
||||||
### Byte limits, per client
|
### Byte limits, per client
|
||||||
|
|
||||||
A response's bytes are counted when it ends (see step 15). So every default sits
|
Each of the same 40 people moves about 256 MiB an hour, for ten hours a day. The
|
||||||
above the largest request and answer together, and no single download breaks a
|
hour and day limits sit about twice above what they move together. A response's
|
||||||
limit.
|
bytes are counted when it ends (see step 15). So the minute limit holds two of
|
||||||
|
the largest responses, with their requests, ending in the same minute. No single
|
||||||
|
download then breaks a limit. An operator who raises `SWWAF_RESPONSE_MAX_BYTES`
|
||||||
|
raises the byte limits with it.
|
||||||
|
|
||||||
### Size and time limits
|
### Size and time limits
|
||||||
|
|
||||||
@@ -453,8 +466,8 @@ service that keeps its visitors' addresses on its own host.
|
|||||||
Addresses are asked about together, up to 200 in one request
|
Addresses are asked about together, up to 200 in one request
|
||||||
(`https://get.geojs.io/v1/ip/geo.json?ip=a,b,c`), one request at a time, which
|
(`https://get.geojs.io/v1/ip/geo.json?ip=a,b,c`), one request at a time, which
|
||||||
keeps the number of requests to GeoJS low. Each answer's `country_code`, `asn`
|
keeps the number of requests to GeoJS low. Each answer's `country_code`, `asn`
|
||||||
and `organization_name` are read. An answer without a `country_code`, or with
|
and `organization_name` are read. An answer without a `country_code` leaves
|
||||||
the `asn` `64512`, counts as unknown.
|
the country unknown, and the `asn` `64512` leaves the AS number unknown.
|
||||||
- Each answer is kept for seven days, in memory and in `lookups.json`. Answers
|
- Each answer is kept for seven days, in memory and in `lookups.json`. Answers
|
||||||
are kept apart from the table of clients, so that an answer outlasts a client
|
are kept apart from the table of clients, so that an answer outlasts a client
|
||||||
dropped from that table. The answer reaches the client's history and ban notes
|
dropped from that table. The answer reaches the client's history and ban notes
|
||||||
@@ -528,12 +541,12 @@ service's abuse comes from.
|
|||||||
fetched and kept like a blocklist (see "Reputation"). Where it and
|
fetched and kept like a blocklist (see "Reputation"). Where it and
|
||||||
`SWWAF_ASN_LIMIT_PERCENT` both give a percentage, the lower applies.
|
`SWWAF_ASN_LIMIT_PERCENT` both give a percentage, the lower applies.
|
||||||
- A client's limit percentage is the lowest of those that apply to it: its AS
|
- A client's limit percentage is the lowest of those that apply to it: its AS
|
||||||
number's, its country's, or a reputation hit's `limit:<percent>`. A client the
|
number's, its country's, or a reputation hit's `limit:<percent>`. A client
|
||||||
lookup cannot place gets `SWWAF_UNKNOWN_LIMIT_PERCENT` in place of an AS
|
without a country gets `SWWAF_UNKNOWN_LIMIT_PERCENT` in place of a country's.
|
||||||
number's or a country's. The percentage is 100 if none applies. Clients given
|
The percentage is 100 if none applies. Clients given a lower percentage are
|
||||||
a lower percentage are banned after fewer requests, first for a while and
|
banned after fewer requests, first for a while and then, for repeated abuse,
|
||||||
then, for repeated abuse, permanently. The log line of each counted request,
|
permanently. The log line of each counted request, and the notes of each ban,
|
||||||
and the notes of each ban, say which setting lowered the limit.
|
say which setting lowered the limit.
|
||||||
- No budget is shared by a whole country or AS number. One abuser could use it
|
- No budget is shared by a whole country or AS number. One abuser could use it
|
||||||
up and lock out everyone else there: a denial of service nobody chose, and the
|
up and lock out everyone else there: a denial of service nobody chose, and the
|
||||||
one this tool exists to prevent. Refusing a whole country is left to the
|
one this tool exists to prevent. Refusing a whole country is left to the
|
||||||
@@ -544,15 +557,14 @@ service's abuse comes from.
|
|||||||
By default the Core Rule Set reads the method, the URL with its query string,
|
By default the Core Rule Set reads the method, the URL with its query string,
|
||||||
and the headers of every request. That is where automated attacks show:
|
and the headers of every request. That is where automated attacks show:
|
||||||
injection in query strings, path traversal, attacks carried in headers,
|
injection in query strings, path traversal, attacks carried in headers,
|
||||||
scanners' user agents. It reads no request body. On a code forge, the example
|
scanners' user agents. It reads no request body. In front of a code forge such
|
||||||
deployment, bodies carry what people write and publish, such as issue text and
|
as gitea, bodies carry what people write and publish, such as issue text and
|
||||||
files saved in the web editor. The Core Rule Set takes the shell commands and
|
files saved in the web editor. The Core Rule Set takes the shell commands and
|
||||||
code in them for attacks. The default gives up refusing an attack carried in a
|
code in them for attacks. The default gives up refusing an attack carried in a
|
||||||
body. The rule files, the limits and the bans still apply to the client that
|
body. The rule files, the limits and the bans still apply to the client that
|
||||||
sends it.
|
sends it.
|
||||||
|
|
||||||
- The Core Rule Set skips a path under `SWWAF_WAF_EXEMPT_PATHS`. An engine error
|
- The Core Rule Set skips a path under `SWWAF_WAF_EXEMPT_PATHS`.
|
||||||
on a request forwards the request, and is logged and counted.
|
|
||||||
- `SWWAF_WAF_PARANOIA_LEVEL` and `SWWAF_WAF_ANOMALY_THRESHOLD` are the Core Rule
|
- `SWWAF_WAF_PARANOIA_LEVEL` and `SWWAF_WAF_ANOMALY_THRESHOLD` are the Core Rule
|
||||||
Set's own two tuning values, at its own defaults. Each level up runs more
|
Set's own two tuning values, at its own defaults. Each level up runs more
|
||||||
rules, which find more attacks and take more ordinary requests for attacks.
|
rules, which find more attacks and take more ordinary requests for attacks.
|
||||||
@@ -633,12 +645,12 @@ every app, and no setting undoes them:
|
|||||||
- `ref`, `sha`, `branch` and `workflow`;
|
- `ref`, `sha`, `branch` and `workflow`;
|
||||||
- `artifactName` and `redirect_to`.
|
- `artifactName` and `redirect_to`.
|
||||||
|
|
||||||
gitea puts file paths, branch and workflow names, and the page to return to
|
gitea puts into them file paths, branch and workflow names, and the page to
|
||||||
after signing in into them. It reads these values as names in a repository
|
return to after signing in. It reads these values as names in a repository
|
||||||
or in its own records, or as a page of its own site, never as a command. Any
|
or in its own records, or as a page of its own site, never as a command. Any
|
||||||
name on those lists can be an ordinary file or branch, such as `.gitignore`
|
name on those lists can be an ordinary file or branch, such as `.gitignore`
|
||||||
or `docker-build`. In these parameters, anything only these three rules
|
or `docker-build`. In these parameters, what only these three rules would
|
||||||
catch reaches the app, commands such as `|cat /etc/passwd` included. Path
|
catch reaches the app, such as the command `|cat /etc/passwd`. Path
|
||||||
traversal, SQL and script injection, and PHP, Java and Node.js code are
|
traversal, SQL and script injection, and PHP, Java and Node.js code are
|
||||||
still refused there. Every other parameter keeps all three rules. An app
|
still refused there. Every other parameter keeps all three rules. An app
|
||||||
that uses one of these parameters as a file on the server, or passes it to a
|
that uses one of these parameters as a file on the server, or passes it to a
|
||||||
@@ -647,8 +659,8 @@ every app, and no setting undoes them:
|
|||||||
5. Two cookies are left out, and `Referer` keeps fewer rules:
|
5. Two cookies are left out, and `Referer` keeps fewer rules:
|
||||||
- The Core Rule Set reads the request without the `gitea_flash` and
|
- The Core Rule Set reads the request without the `gitea_flash` and
|
||||||
`redirect_to` cookies. gitea puts a message naming what was just done into
|
`redirect_to` cookies. gitea puts a message naming what was just done into
|
||||||
`gitea_flash`. It puts the page to return to after signing in into
|
`gitea_flash`. It puts into `redirect_to` the page to return to after
|
||||||
`redirect_to`. The Core Rule Set takes file names in them, such as
|
signing in. The Core Rule Set takes file names in them, such as
|
||||||
`package.json`, for attacks. A browser sends such a cookie with every
|
`package.json`, for attacks. A browser sends such a cookie with every
|
||||||
request until gitea replaces it. gitea cannot replace it while those
|
request until gitea replaces it. gitea cannot replace it while those
|
||||||
requests are refused, so one refusal would keep the browser out of the
|
requests are refused, so one refusal would keep the browser out of the
|
||||||
@@ -772,7 +784,8 @@ too close together.
|
|||||||
client gets a quarter of every request and byte limit rather than a refusal.
|
client gets a quarter of every request and byte limit rather than a refusal.
|
||||||
- Verdicts and scores are kept for `SWWAF_REPUTATION_CACHE_TTL`, in memory and
|
- Verdicts and scores are kept for `SWWAF_REPUTATION_CACHE_TTL`, in memory and
|
||||||
in `reputation.json`. A source that is down or over quota gives no verdict.
|
in `reputation.json`. A source that is down or over quota gives no verdict.
|
||||||
The service continues, with one `source_failure` alert per cooldown.
|
The service continues. Each failure raises a `source_failure` alert (see
|
||||||
|
"Alerting").
|
||||||
|
|
||||||
### Alerting
|
### Alerting
|
||||||
|
|
||||||
@@ -861,11 +874,11 @@ and the default file.
|
|||||||
wrong for every app, with its path rules anchored at the site root. In front
|
wrong for every app, with its path rules anchored at the site root. In front
|
||||||
of gitea, `/.env` is a probe, while
|
of gitea, `/.env` is a probe, while
|
||||||
`/<owner>/<repo>/src/branch/main/.env.example` is a file in a repository that
|
`/<owner>/<repo>/src/branch/main/.env.example` is a file in a repository that
|
||||||
any visitor may open. `ban` rules of your own follow the same rule. Anything
|
any visitor may open. Anchor the paths of your own `ban` rules at the site
|
||||||
app-specific, such as a rule banning WordPress probes in front of gitea,
|
root too. Anything app-specific, such as a rule banning WordPress probes in
|
||||||
belongs in a file the app's Dockerfile adds or the deployer mounts. A
|
front of gitea, belongs in a file the app's Dockerfile adds or the deployer
|
||||||
directory mounted over `SWWAF_RULES_DIR` replaces the default file; single
|
mounts. A directory mounted over `SWWAF_RULES_DIR` replaces the default file;
|
||||||
files mounted into it add to it.
|
single files mounted into it add to it.
|
||||||
- Rules meant to be edited while `smallwebwaf` runs belong in a mounted
|
- Rules meant to be edited while `smallwebwaf` runs belong in a mounted
|
||||||
directory, not a mounted file. Many editors save a file by replacing it, and a
|
directory, not a mounted file. Many editors save a file by replacing it, and a
|
||||||
container keeps seeing the old one when the file alone is mounted. The lookup
|
container keeps seeing the old one when the file alone is mounted. The lookup
|
||||||
@@ -874,11 +887,11 @@ and the default file.
|
|||||||
|
|
||||||
## Persistent state
|
## Persistent state
|
||||||
|
|
||||||
All state lives in memory, and the files in `SWWAF_STATE_DIR` hold a copy of all
|
All state lives in memory, and the files in `SWWAF_STATE_DIR` hold a copy of it,
|
||||||
of it, so an orderly stop loses nothing (the remote log is the exception; see
|
so an orderly stop loses nothing. The files are meant for people as well: an
|
||||||
"Request log"). The files are meant for people as well: an admin can read or
|
admin can read or edit them at any time, and the running `smallwebwaf` takes the
|
||||||
edit them at any time, and the running `smallwebwaf` takes the edit in.
|
edit in. `docs/state-files.md` lists the files, what each holds, and their
|
||||||
`docs/state-files.md` lists the files, what each holds, and their sizes.
|
sizes.
|
||||||
|
|
||||||
- Each file holds one kind of state, as JSON with:
|
- Each file holds one kind of state, as JSON with:
|
||||||
- a top-level `version` number;
|
- a top-level `version` number;
|
||||||
@@ -1019,14 +1032,14 @@ what the image holds and how each part is pinned.
|
|||||||
digest.
|
digest.
|
||||||
- nixpkgs comes from a release file on `releases.nixos.org`, named with its
|
- nixpkgs comes from a release file on `releases.nixos.org`, named with its
|
||||||
hash, not from a GitHub archive, whose bytes can change. Root uses Nix
|
hash, not from a GitHub archive, whose bytes can change. Root uses Nix
|
||||||
directly; no Nix daemon runs, and since `nix-bin` creates no build users,
|
directly; no Nix daemon runs. `nix-bin` creates no build users, so
|
||||||
`build-users-group =` in `/etc/nix/nix.conf` lets root build without them.
|
`/etc/nix/nix.conf` sets `build-users-group =`, which lets root build without
|
||||||
Root's Nix profile is last on the `PATH`, so that no package hides the image's
|
them. Root's Nix profile is last on the `PATH`, so that no package hides the
|
||||||
own commands; busybox's `sv`, for one, looks for services elsewhere. An app
|
image's own commands; busybox's `sv`, for one, looks for services elsewhere.
|
||||||
built on the image gets the same packages each time. Security fixes from
|
An app built on the image gets the same packages each time. Security fixes
|
||||||
Ubuntu and nixpkgs come with a newer `smallwebwaf` image, which an app takes
|
from Ubuntu and nixpkgs come with a newer `smallwebwaf` image, which an app
|
||||||
by changing the digest in its `FROM` line. The image moves to each new nixpkgs
|
takes by changing the digest in its `FROM` line. The image moves to each new
|
||||||
release, every six months.
|
nixpkgs release, every six months.
|
||||||
- runit supervises the two processes, with `runsvinit` as the entrypoint, as the
|
- runit supervises the two processes, with `runsvinit` as the entrypoint, as the
|
||||||
owner's code style guide asks for service containers. No package carries
|
owner's code style guide asks for service containers. No package carries
|
||||||
`runsvinit`, so the image builds it from its archived source, unchanged since
|
`runsvinit`, so the image builds it from its archived source, unchanged since
|
||||||
@@ -1075,10 +1088,10 @@ It sets no `ENTRYPOINT` or `USER` of its own. The container must start
|
|||||||
- an unwritable `SWWAF_STATE_DIR`;
|
- an unwritable `SWWAF_STATE_DIR`;
|
||||||
- a `SWWAF_RULES_DIR` that does not exist or cannot be read;
|
- a `SWWAF_RULES_DIR` that does not exist or cannot be read;
|
||||||
- a `SWWAF_LISTEN_ADDR` it cannot listen on.
|
- a `SWWAF_LISTEN_ADDR` it cannot listen on.
|
||||||
- A `smallwebwaf` that stops at start is therefore started again every second,
|
- A `smallwebwaf` that stops at start logs its message at each try until the
|
||||||
logging its message each time, until the cause is fixed; the health check
|
cause is fixed; the health check fails meanwhile. Once running, a failing
|
||||||
fails meanwhile. Once running, a failing GeoJS, reputation source or alert
|
GeoJS, reputation source or alert destination, or a broken edit, never takes
|
||||||
destination, or a broken edit, never takes the protected service down.
|
the protected service down.
|
||||||
- `docker stop` sends `SIGTERM` to `runsvinit`, which has runit stop both
|
- `docker stop` sends `SIGTERM` to `runsvinit`, which has runit stop both
|
||||||
services at once, each with `SIGTERM`, and exits once they have stopped.
|
services at once, each with `SIGTERM`, and exits once they have stopped.
|
||||||
`smallwebwaf` stops taking requests, gives those in progress five seconds to
|
`smallwebwaf` stops taking requests, gives those in progress five seconds to
|
||||||
@@ -1147,9 +1160,10 @@ all: `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/32,::1/128`.
|
|||||||
|
|
||||||
### upaas
|
### upaas
|
||||||
|
|
||||||
An app deployed by upaas takes this shape with no change to upaas. upaas builds
|
An app deployed by upaas, which rebuilds and restarts an app's Docker container
|
||||||
the app's Dockerfile and runs the one container as it runs any app. The traefik
|
when its repository changes, takes this shape with no change to upaas. upaas
|
||||||
labels route to port 8080, and any `SWWAF_` settings go with the app's
|
builds the app's Dockerfile and runs the one container as it runs any app. The
|
||||||
|
traefik labels route to port 8080, and any `SWWAF_` settings go with the app's
|
||||||
environment variables. The volume for `/var/lib/smallwebwaf` and the directory
|
environment variables. The volume for `/var/lib/smallwebwaf` and the directory
|
||||||
of any token file go beside the app's own volumes. gitea is built on the image
|
of any token file go beside the app's own volumes. gitea is built on the image
|
||||||
like any other app; `docs/gitea.md` records what the defaults do in front of it,
|
like any other app; `docs/gitea.md` records what the defaults do in front of it,
|
||||||
|
|||||||
+5
-5
@@ -42,7 +42,7 @@ on one line:
|
|||||||
"asn": "",
|
"asn": "",
|
||||||
"as_name": "",
|
"as_name": "",
|
||||||
"country": "",
|
"country": "",
|
||||||
"reason": "requests per minute over the limit of 1000",
|
"reason": "requests per minute over the limit of 6000",
|
||||||
"detail": {
|
"detail": {
|
||||||
"ban_expires": "2026-10-06T13:00:00.123Z",
|
"ban_expires": "2026-10-06T13:00:00.123Z",
|
||||||
"cause": "limit",
|
"cause": "limit",
|
||||||
@@ -51,9 +51,9 @@ on one line:
|
|||||||
"as_name": "",
|
"as_name": "",
|
||||||
"country": "",
|
"country": "",
|
||||||
"kind": "requests",
|
"kind": "requests",
|
||||||
"limit": 1000,
|
"limit": 6000,
|
||||||
"window": "minute",
|
"window": "minute",
|
||||||
"count": 1001,
|
"count": 6001,
|
||||||
"request": {
|
"request": {
|
||||||
"time": "2026-10-06T12:00:00.123456789Z",
|
"time": "2026-10-06T12:00:00.123456789Z",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
@@ -62,7 +62,7 @@ on one line:
|
|||||||
"status": 403,
|
"status": 403,
|
||||||
"user_agent": "scraper/1.0"
|
"user_agent": "scraper/1.0"
|
||||||
},
|
},
|
||||||
"requests": 5210,
|
"requests": 8210,
|
||||||
"refused": 0,
|
"refused": 0,
|
||||||
"earlier_bans": {
|
"earlier_bans": {
|
||||||
"limit": 0,
|
"limit": 0,
|
||||||
@@ -89,7 +89,7 @@ on one line:
|
|||||||
- `asn`, `as_name` and `country`: the client's, as known when the alert was
|
- `asn`, `as_name` and `country`: the client's, as known when the alert was
|
||||||
raised. They are empty when GeoJS had not answered yet.
|
raised. They are empty when GeoJS had not answered yet.
|
||||||
- `reason`: a short sentence, such as
|
- `reason`: a short sentence, such as
|
||||||
`requests per minute over the limit of 1000` or `listed by a blocklist`.
|
`requests per minute over the limit of 6000` or `listed by a blocklist`.
|
||||||
- `detail`: what is particular to the event:
|
- `detail`: what is particular to the event:
|
||||||
- for a ban, its `cause`, `ban_expires` and `notes` (the notes are listed
|
- for a ban, its `cause`, `ban_expires` and `notes` (the notes are listed
|
||||||
under `bans.json` in [`state-files.md`](state-files.md));
|
under `bans.json` in [`state-files.md`](state-files.md));
|
||||||
|
|||||||
+11
-8
@@ -11,8 +11,10 @@ checked again when gitea or the Core Rule Set changes version.
|
|||||||
|
|
||||||
- SSH access to gitea does not pass through `smallwebwaf` and is not protected
|
- SSH access to gitea does not pass through `smallwebwaf` and is not protected
|
||||||
by it.
|
by it.
|
||||||
- A clone is a response. It fits the defaults of 30 minutes and 5 GiB for all
|
- A clone is a response. It fits the defaults of 30 minutes and 1 GiB unless the
|
||||||
but the largest repositories and slowest links.
|
repository is large or the link slow. A repository whose clone is larger than
|
||||||
|
1 GiB needs `SWWAF_RESPONSE_MAX_BYTES` raised, and the byte limits with it
|
||||||
|
("Byte limits, per client" in `SPEC.md`).
|
||||||
- A push is a request, and so is every other upload:
|
- A push is a request, and so is every other upload:
|
||||||
- a git push;
|
- a git push;
|
||||||
- an LFS object;
|
- an LFS object;
|
||||||
@@ -30,12 +32,13 @@ checked again when gitea or the Core Rule Set changes version.
|
|||||||
request limits do most of the work there.
|
request limits do most of the work there.
|
||||||
- An Actions runner that reaches gitea through `smallwebwaf` sends requests all
|
- An Actions runner that reaches gitea through `smallwebwaf` sends requests all
|
||||||
day. One older than version 0.4 (April 2026) asks for work every 2 seconds,
|
day. One older than version 0.4 (April 2026) asks for work every 2 seconds,
|
||||||
43,200 requests a day, and reports a running job's log and state every second.
|
43,200 requests a day. While it runs a job, it reports the job's log and state
|
||||||
So it passes the day limit of 50,000 after about an hour and a quarter of
|
every second instead, two requests a second. So one runner busy all day sends
|
||||||
jobs. It is then banned, the job it is running fails, and each repeat within a
|
172,800 requests, under the request limits. Six such runners on one address,
|
||||||
day makes the ban longer. Newer runners ask less often, but a busy one, or
|
each busy all day, pass the day limit of 1,000,000 within two days; five stay
|
||||||
several on one address, can still pass it. Put the runners' addresses in
|
under it. Their address is then banned, their running jobs fail, and each
|
||||||
`SWWAF_RATE_LIMIT_EXEMPT_NETS`.
|
repeat within a day makes the ban longer. Newer runners ask for work less
|
||||||
|
often. Put the runners' addresses in `SWWAF_RATE_LIMIT_EXEMPT_NETS`.
|
||||||
|
|
||||||
## What the Core Rule Set lets through
|
## What the Core Rule Set lets through
|
||||||
|
|
||||||
|
|||||||
+7
-8
@@ -99,16 +99,16 @@ that take `off`, the `_FILE` form, and what each group does and why.
|
|||||||
|
|
||||||
## Request rate limits, per client
|
## Request rate limits, per client
|
||||||
|
|
||||||
- `SWWAF_RATE_LIMIT_PER_MINUTE` (default `1000`), `SWWAF_RATE_LIMIT_PER_HOUR`
|
- `SWWAF_RATE_LIMIT_PER_MINUTE` (default `6000`), `SWWAF_RATE_LIMIT_PER_HOUR`
|
||||||
(default `10000`) and `SWWAF_RATE_LIMIT_PER_DAY` (default `50000`): the most
|
(default `100000`) and `SWWAF_RATE_LIMIT_PER_DAY` (default `1000000`): the
|
||||||
requests a client may make in each window. A count or `off`.
|
most requests a client may make in each window. A count or `off`.
|
||||||
- `SWWAF_RATE_LIMIT_EXEMPT_PATHS` (default empty): path prefixes exempt from
|
- `SWWAF_RATE_LIMIT_EXEMPT_PATHS` (default empty): path prefixes exempt from
|
||||||
rate limiting and the byte limits, such as `/assets/`.
|
rate limiting and the byte limits, such as `/assets/`.
|
||||||
|
|
||||||
## Byte limits, per client
|
## Byte limits, per client
|
||||||
|
|
||||||
- `SWWAF_BYTES_LIMIT_PER_MINUTE` (default `10G`), `SWWAF_BYTES_LIMIT_PER_HOUR`
|
- `SWWAF_BYTES_LIMIT_PER_MINUTE` (default `4G`), `SWWAF_BYTES_LIMIT_PER_HOUR`
|
||||||
(default `20G`) and `SWWAF_BYTES_LIMIT_PER_DAY` (default `50G`): the most
|
(default `20G`) and `SWWAF_BYTES_LIMIT_PER_DAY` (default `200G`): the most
|
||||||
bytes counted for a client in each window. A size or `off`.
|
bytes counted for a client in each window. A size or `off`.
|
||||||
- `SWWAF_BYTES_COUNT` (default `both`): which body bytes the byte limits count:
|
- `SWWAF_BYTES_COUNT` (default `both`): which body bytes the byte limits count:
|
||||||
`response`, `request` or `both`.
|
`response`, `request` or `both`.
|
||||||
@@ -131,7 +131,7 @@ that take `off`, the `_FILE` form, and what each group does and why.
|
|||||||
to send its whole answer. A duration or `off`.
|
to send its whole answer. A duration or `off`.
|
||||||
- `SWWAF_REQUEST_MAX_BYTES` (default `100M`): the largest request body. A size
|
- `SWWAF_REQUEST_MAX_BYTES` (default `100M`): the largest request body. A size
|
||||||
or `off`.
|
or `off`.
|
||||||
- `SWWAF_RESPONSE_MAX_BYTES` (default `5G`): the largest response body. A size
|
- `SWWAF_RESPONSE_MAX_BYTES` (default `1G`): the largest response body. A size
|
||||||
or `off`.
|
or `off`.
|
||||||
|
|
||||||
## Lookup of AS number and country
|
## Lookup of AS number and country
|
||||||
@@ -196,8 +196,7 @@ that take `off`, the `_FILE` form, and what each group does and why.
|
|||||||
- `SWWAF_WAF_BODY_LIMIT` (default `off`): `off`, or how much of a request body
|
- `SWWAF_WAF_BODY_LIMIT` (default `off`): `off`, or how much of a request body
|
||||||
the Core Rule Set reads, a size of at most `1G`.
|
the Core Rule Set reads, a size of at most `1G`.
|
||||||
- `SWWAF_ERROR_BURST_THRESHOLD` (default `30`): the most refusals of one client
|
- `SWWAF_ERROR_BURST_THRESHOLD` (default `30`): the most refusals of one client
|
||||||
in a minute for a rule match, a trap path, a Core Rule Set match or a failed
|
in a minute (see "Attack detection" in `SPEC.md`). A count or `off`.
|
||||||
token check. A count or `off`.
|
|
||||||
|
|
||||||
## Ban lengths, response and scope
|
## Ban lengths, response and scope
|
||||||
|
|
||||||
|
|||||||
@@ -502,17 +502,17 @@ func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
|||||||
UpstreamRequestTimeout: env.duration("SWWAF_UPSTREAM_REQUEST_TIMEOUT", "60s"),
|
UpstreamRequestTimeout: env.duration("SWWAF_UPSTREAM_REQUEST_TIMEOUT", "60s"),
|
||||||
UpstreamResponseTimeout: env.duration("SWWAF_UPSTREAM_RESPONSE_TIMEOUT", "30m"),
|
UpstreamResponseTimeout: env.duration("SWWAF_UPSTREAM_RESPONSE_TIMEOUT", "30m"),
|
||||||
RequestMaxBytes: env.size("SWWAF_REQUEST_MAX_BYTES", "100M"),
|
RequestMaxBytes: env.size("SWWAF_REQUEST_MAX_BYTES", "100M"),
|
||||||
ResponseMaxBytes: env.size("SWWAF_RESPONSE_MAX_BYTES", "5G"),
|
ResponseMaxBytes: env.size("SWWAF_RESPONSE_MAX_BYTES", "1G"),
|
||||||
AllowNets: env.netblocks("SWWAF_ALLOW_NETS", ""),
|
AllowNets: env.netblocks("SWWAF_ALLOW_NETS", ""),
|
||||||
RateLimitExemptNets: env.netblocks("SWWAF_RATE_LIMIT_EXEMPT_NETS", ""),
|
RateLimitExemptNets: env.netblocks("SWWAF_RATE_LIMIT_EXEMPT_NETS", ""),
|
||||||
DenyNets: env.netblocks("SWWAF_DENY_NETS", ""),
|
DenyNets: env.netblocks("SWWAF_DENY_NETS", ""),
|
||||||
RateLimitPerMinute: env.count("SWWAF_RATE_LIMIT_PER_MINUTE", "1000"),
|
RateLimitPerMinute: env.count("SWWAF_RATE_LIMIT_PER_MINUTE", "6000"),
|
||||||
RateLimitPerHour: env.count("SWWAF_RATE_LIMIT_PER_HOUR", "10000"),
|
RateLimitPerHour: env.count("SWWAF_RATE_LIMIT_PER_HOUR", "100000"),
|
||||||
RateLimitPerDay: env.count("SWWAF_RATE_LIMIT_PER_DAY", "50000"),
|
RateLimitPerDay: env.count("SWWAF_RATE_LIMIT_PER_DAY", "1000000"),
|
||||||
RateLimitExemptPaths: env.pathPrefixes("SWWAF_RATE_LIMIT_EXEMPT_PATHS", ""),
|
RateLimitExemptPaths: env.pathPrefixes("SWWAF_RATE_LIMIT_EXEMPT_PATHS", ""),
|
||||||
BytesLimitPerMinute: env.size("SWWAF_BYTES_LIMIT_PER_MINUTE", "10G"),
|
BytesLimitPerMinute: env.size("SWWAF_BYTES_LIMIT_PER_MINUTE", "4G"),
|
||||||
BytesLimitPerHour: env.size("SWWAF_BYTES_LIMIT_PER_HOUR", "20G"),
|
BytesLimitPerHour: env.size("SWWAF_BYTES_LIMIT_PER_HOUR", "20G"),
|
||||||
BytesLimitPerDay: env.size("SWWAF_BYTES_LIMIT_PER_DAY", "50G"),
|
BytesLimitPerDay: env.size("SWWAF_BYTES_LIMIT_PER_DAY", "200G"),
|
||||||
BytesCount: env.bytesCount("SWWAF_BYTES_COUNT", "both"),
|
BytesCount: env.bytesCount("SWWAF_BYTES_COUNT", "both"),
|
||||||
LookupSource: env.lookupSource("SWWAF_LOOKUP_SOURCE", "geojs"),
|
LookupSource: env.lookupSource("SWWAF_LOOKUP_SOURCE", "geojs"),
|
||||||
LookupDBPath: env.value("SWWAF_LOOKUP_DB_PATH", ""),
|
LookupDBPath: env.value("SWWAF_LOOKUP_DB_PATH", ""),
|
||||||
|
|||||||
@@ -261,10 +261,10 @@ func TestDefaults(t *testing.T) {
|
|||||||
UpstreamRequestTimeout: time.Minute,
|
UpstreamRequestTimeout: time.Minute,
|
||||||
UpstreamResponseTimeout: 30 * time.Minute,
|
UpstreamResponseTimeout: 30 * time.Minute,
|
||||||
RequestMaxBytes: 100 << 20,
|
RequestMaxBytes: 100 << 20,
|
||||||
ResponseMaxBytes: 5 << 30,
|
ResponseMaxBytes: 1 << 30,
|
||||||
RateLimitPerMinute: 1000,
|
RateLimitPerMinute: 6000,
|
||||||
RateLimitPerHour: 10000,
|
RateLimitPerHour: 100000,
|
||||||
RateLimitPerDay: 50000,
|
RateLimitPerDay: 1000000,
|
||||||
BanResponse: 403,
|
BanResponse: 403,
|
||||||
LimitBanDuration: time.Hour,
|
LimitBanDuration: time.Hour,
|
||||||
LimitBanRepeatWindow: 24 * time.Hour,
|
LimitBanRepeatWindow: 24 * time.Hour,
|
||||||
@@ -284,8 +284,8 @@ func TestDefaults(t *testing.T) {
|
|||||||
LookupSource: defaultLookupSource, LookupTimeout: time.Second,
|
LookupSource: defaultLookupSource, LookupTimeout: time.Second,
|
||||||
})
|
})
|
||||||
wantByteLimitSettings(t, cfg, config.Config{
|
wantByteLimitSettings(t, cfg, config.Config{
|
||||||
BytesLimitPerMinute: 10 << 30, BytesLimitPerHour: 20 << 30,
|
BytesLimitPerMinute: 4 << 30, BytesLimitPerHour: 20 << 30,
|
||||||
BytesLimitPerDay: 50 << 30, BytesCount: "both",
|
BytesLimitPerDay: 200 << 30, BytesCount: "both",
|
||||||
})
|
})
|
||||||
|
|
||||||
if cfg.UpstreamURL.String() != "http://127.0.0.1:8081" {
|
if cfg.UpstreamURL.String() != "http://127.0.0.1:8081" {
|
||||||
@@ -2371,17 +2371,17 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
|
|||||||
upstreamRequestTimeout: "60s",
|
upstreamRequestTimeout: "60s",
|
||||||
upstreamResponseTimeout: "30m",
|
upstreamResponseTimeout: "30m",
|
||||||
requestMaxBytes: "100M",
|
requestMaxBytes: "100M",
|
||||||
responseMaxBytes: "5G",
|
responseMaxBytes: "1G",
|
||||||
allowNets: "",
|
allowNets: "",
|
||||||
rateLimitExemptNets: "",
|
rateLimitExemptNets: "",
|
||||||
denyNets: "",
|
denyNets: "",
|
||||||
rateLimitPerMinute: "1000",
|
rateLimitPerMinute: "6000",
|
||||||
rateLimitPerHour: "10000",
|
rateLimitPerHour: "100000",
|
||||||
rateLimitPerDay: "50000",
|
rateLimitPerDay: "1000000",
|
||||||
rateLimitExemptPaths: "",
|
rateLimitExemptPaths: "",
|
||||||
bytesLimitPerMinute: "10G",
|
bytesLimitPerMinute: "4G",
|
||||||
bytesLimitPerHour: "20G",
|
bytesLimitPerHour: "20G",
|
||||||
bytesLimitPerDay: "50G",
|
bytesLimitPerDay: "200G",
|
||||||
bytesCount: "both",
|
bytesCount: "both",
|
||||||
lookupSource: defaultLookupSource,
|
lookupSource: defaultLookupSource,
|
||||||
lookupDBPath: "",
|
lookupDBPath: "",
|
||||||
|
|||||||
@@ -60,8 +60,19 @@ func TestHealthCheck(t *testing.T) {
|
|||||||
}, 0, "")
|
}, 0, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Close()
|
// Another test can take a closed server's port at once, so the app's
|
||||||
wantHealthCheck(t, env, 1, "unhealthy: connect to the app: ")
|
// address here is a multicast address, to which TCP cannot connect.
|
||||||
|
wantHealthCheck(t, map[string]string{
|
||||||
|
listenAddr: ":" + port,
|
||||||
|
upstreamURL: "http://224.0.0.1",
|
||||||
|
}, 1, "unhealthy: connect to the app: ")
|
||||||
|
|
||||||
|
// Listening on port 0 takes any free port, so nothing listens on port 0
|
||||||
|
// itself.
|
||||||
|
wantHealthCheck(t, map[string]string{listenAddr: ":0"}, 1,
|
||||||
|
"unhealthy: ask smallwebwaf: ")
|
||||||
|
wantHealthCheck(t, map[string]string{listenAddr: "8080"}, 1,
|
||||||
|
"unhealthy: invalid setting: SWWAF_LISTEN_ADDR: ")
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
|
|
||||||
@@ -70,10 +81,6 @@ func TestHealthCheck(t *testing.T) {
|
|||||||
case <-time.After(waitLimit):
|
case <-time.After(waitLimit):
|
||||||
t.Fatal("still running after being told to stop")
|
t.Fatal("still running after being told to stop")
|
||||||
}
|
}
|
||||||
|
|
||||||
wantHealthCheck(t, env, 1, "unhealthy: ask smallwebwaf: ")
|
|
||||||
wantHealthCheck(t, map[string]string{listenAddr: "8080"}, 1,
|
|
||||||
"unhealthy: invalid setting: SWWAF_LISTEN_ADDR: ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHealthCheckRefusesAnArgument(t *testing.T) {
|
func TestHealthCheckRefusesAnArgument(t *testing.T) {
|
||||||
|
|||||||
@@ -1240,13 +1240,13 @@ func wantStartingLine(t *testing.T, line map[string]any, appURL, dir string) {
|
|||||||
"SWWAF_UPSTREAM_REQUEST_TIMEOUT": "60s",
|
"SWWAF_UPSTREAM_REQUEST_TIMEOUT": "60s",
|
||||||
"SWWAF_UPSTREAM_RESPONSE_TIMEOUT": "30m",
|
"SWWAF_UPSTREAM_RESPONSE_TIMEOUT": "30m",
|
||||||
"SWWAF_REQUEST_MAX_BYTES": "100M",
|
"SWWAF_REQUEST_MAX_BYTES": "100M",
|
||||||
"SWWAF_RESPONSE_MAX_BYTES": "5G",
|
"SWWAF_RESPONSE_MAX_BYTES": "1G",
|
||||||
"SWWAF_ALLOW_NETS": "",
|
"SWWAF_ALLOW_NETS": "",
|
||||||
rateLimitExemptNets: "",
|
rateLimitExemptNets: "",
|
||||||
"SWWAF_DENY_NETS": "",
|
"SWWAF_DENY_NETS": "",
|
||||||
"SWWAF_RATE_LIMIT_PER_MINUTE": "1000",
|
"SWWAF_RATE_LIMIT_PER_MINUTE": "6000",
|
||||||
"SWWAF_RATE_LIMIT_PER_HOUR": "10000",
|
"SWWAF_RATE_LIMIT_PER_HOUR": "100000",
|
||||||
rateLimitPerDay: "50000",
|
rateLimitPerDay: "1000000",
|
||||||
"SWWAF_RATE_LIMIT_EXEMPT_PATHS": "",
|
"SWWAF_RATE_LIMIT_EXEMPT_PATHS": "",
|
||||||
"SWWAF_DENIED_COUNTRIES": "",
|
"SWWAF_DENIED_COUNTRIES": "",
|
||||||
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES": "",
|
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES": "",
|
||||||
|
|||||||
Reference in new issue
Block a user