Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35054fe94b |
@@ -17,17 +17,15 @@ reference documents under [`docs/`](docs/) are listed under "Documents" below.
|
||||
## What it protects against
|
||||
|
||||
- Scrapers that fetch every page of a site, all day. Rate limiting gives each
|
||||
client 300 requests a minute, 30,000 an hour and 300,000 a day. That leaves
|
||||
room for programmatic use of an API, such as an automation fleet on another
|
||||
host. The minute limit catches, within seconds, a client firing requests as
|
||||
fast as it can. With the minute limit raised or off, the hour limit catches
|
||||
one that keeps up 10 a second. The day limit catches one that keeps up 4 a
|
||||
second around the clock. Counting cannot catch a client that stays under all
|
||||
three limits, such as one that keeps up 3 a second; only the protections below
|
||||
can. Byte limits give each client 4 GiB a minute, 20 GiB an hour and 200 GiB a
|
||||
day. A client over a limit is refused and banned. The first ban lasts an hour.
|
||||
Each repeat within a day makes the next ban three times as long. A ban that
|
||||
would last more than seven days is permanent.
|
||||
client 30,000 requests an hour and 300,000 a day, with no limit per minute.
|
||||
That leaves room for programmatic use of an API, such as an automation fleet
|
||||
on another host. The hour limit catches a crawler that keeps up 10 requests a
|
||||
second. The day limit catches one that keeps up 4 a second around the clock.
|
||||
Counting cannot catch a client slower than about 3.5 requests a second; only
|
||||
the protections below can. Byte limits give each client 4 GiB a minute, 20 GiB
|
||||
an hour and 200 GiB a day. A client over a limit is refused and banned. The
|
||||
first ban lasts an hour. Each repeat within a day makes the next ban three
|
||||
times as long. A ban that would last more than seven days is permanent.
|
||||
- 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
|
||||
next request during the ban makes the ban permanent. The default rule file
|
||||
|
||||
@@ -15,8 +15,9 @@ to them.
|
||||
all. Every setting has a default chosen for a service facing the internet
|
||||
in 2026. A setting that needs an alert destination, an account key or a token
|
||||
stays off until the operator supplies it.
|
||||
- It limits each client's requests and bytes per minute, hour and day. It bounds
|
||||
the size and duration of every request and answer. It detects attacks.
|
||||
- It limits each client's requests per hour and day, and its bytes per minute,
|
||||
hour and day. It bounds the size and duration of every request and answer. It
|
||||
detects attacks.
|
||||
- It bans abusers: briefly at first, for seven days on a clear sign of attack,
|
||||
and permanently when they keep at it.
|
||||
- It looks up each client's AS number and country. It can refuse whole countries
|
||||
@@ -376,31 +377,25 @@ Conventions:
|
||||
### Request rate limits, per client
|
||||
|
||||
Breaking one bans the client. The defaults suit programmatic use of an API, such
|
||||
as an automation fleet on another host, and still stop abusive bots. A fleet of
|
||||
20 agents, each making a request every 10 seconds at its busiest, makes 120
|
||||
requests a minute and 7,200 an hour. Working around the clock, it makes 172,800
|
||||
a day. It stays under all three limits.
|
||||
as an automation fleet on another host, and still stop abusive bots. They count
|
||||
per hour and per day, so a short burst of requests breaks no limit on its own.
|
||||
|
||||
- Per minute: 300, 5 requests a second. A client firing requests as fast as it
|
||||
can passes it within seconds. It leaves room for the fleet and a few quick
|
||||
first visits with an empty cache. A Gitea page opened with an empty cache
|
||||
loads about 40 files.
|
||||
- Per hour: 30,000, about 8 requests a second for a whole hour. At the default
|
||||
minute limit no client reaches it, since 300 a minute is at most 18,000 an
|
||||
hour. With the minute limit raised or off, a crawler that keeps up 10 a second
|
||||
makes 36,000 an hour, and passes it.
|
||||
- Per minute: `off`.
|
||||
- Per hour: 30,000, about 8 requests a second for a whole hour. A crawler that
|
||||
keeps up 10 a second makes 36,000 an hour, and passes it.
|
||||
- Per day: 300,000, about 3.5 requests a second around the clock. A crawler that
|
||||
keeps up 4 a second makes 345,600 a day, and passes it. At 240 a minute and
|
||||
14,400 an hour, it stays under the other two limits.
|
||||
keeps up 4 a second makes 345,600 a day, and passes it.
|
||||
|
||||
Counting cannot catch a client that stays under all three limits, such as one
|
||||
that keeps up 3 requests a second. 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
|
||||
keeps up more than 5 requests a second passes the minute limit. One that keeps
|
||||
up more than about 3.5 a second all day passes the day limit, as two busy
|
||||
Actions runners on one address do. Such a machine's address belongs in
|
||||
`SWWAF_RATE_LIMIT_EXEMPT_NETS`.
|
||||
A fleet of 20 agents, each making a request every 10 seconds, makes 7,200
|
||||
requests an hour. Working around the clock, it makes 172,800 a day. Both stay
|
||||
under the limits.
|
||||
|
||||
Counting cannot catch a client slower than these limits. 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 keeps up more than about 3.5 requests a second all day
|
||||
passes the day limit, as two busy Actions runners on one address do. Its address
|
||||
belongs in `SWWAF_RATE_LIMIT_EXEMPT_NETS`.
|
||||
|
||||
`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
|
||||
|
||||
+10
-10
@@ -35,34 +35,34 @@ on one line:
|
||||
```json
|
||||
{
|
||||
"instance": "fsn1app1/gitea",
|
||||
"time": "2026-10-06T12:00:30.123461Z",
|
||||
"time": "2026-10-06T12:50:00.123461Z",
|
||||
"event": "ban",
|
||||
"client": "203.0.113.9",
|
||||
"netblock": "203.0.113.9/32",
|
||||
"asn": "",
|
||||
"as_name": "",
|
||||
"country": "",
|
||||
"reason": "requests per minute over the limit of 300",
|
||||
"reason": "requests per hour over the limit of 30000",
|
||||
"detail": {
|
||||
"ban_expires": "2026-10-06T13:00:30.123Z",
|
||||
"ban_expires": "2026-10-06T13:50:00.123Z",
|
||||
"cause": "limit",
|
||||
"notes": {
|
||||
"asn": "",
|
||||
"as_name": "",
|
||||
"country": "",
|
||||
"kind": "requests",
|
||||
"limit": 300,
|
||||
"window": "minute",
|
||||
"count": 301,
|
||||
"limit": 30000,
|
||||
"window": "hour",
|
||||
"count": 30001,
|
||||
"request": {
|
||||
"time": "2026-10-06T12:00:30.123456789Z",
|
||||
"time": "2026-10-06T12:50:00.123456789Z",
|
||||
"method": "GET",
|
||||
"host": "app.example",
|
||||
"path": "/owner/repo/commits/branch/main?page=301",
|
||||
"path": "/owner/repo/commits/branch/main?page=812",
|
||||
"status": 403,
|
||||
"user_agent": "scraper/1.0"
|
||||
},
|
||||
"requests": 301,
|
||||
"requests": 30001,
|
||||
"refused": 0,
|
||||
"earlier_bans": {
|
||||
"limit": 0,
|
||||
@@ -89,7 +89,7 @@ on one line:
|
||||
- `asn`, `as_name` and `country`: the client's, as known when the alert was
|
||||
raised. They are empty when GeoJS had not answered yet.
|
||||
- `reason`: a short sentence, such as
|
||||
`requests per minute over the limit of 300` or `listed by a blocklist`.
|
||||
`requests per hour over the limit of 30000` or `listed by a blocklist`.
|
||||
- `detail`: what is particular to the event:
|
||||
- for a ban, its `cause`, `ban_expires` and `notes` (the notes are listed
|
||||
under `bans.json` in [`state-files.md`](state-files.md));
|
||||
|
||||
+5
-6
@@ -34,12 +34,11 @@ checked again when gitea or the Core Rule Set changes version.
|
||||
day. One older than version 0.4 (April 2026) asks for work every 2 seconds,
|
||||
43,200 requests a day. While it runs a job, it reports the job's log and state
|
||||
every second instead, two requests a second. So one runner busy all day sends
|
||||
120 requests a minute and 172,800 a day, under the request limits. Two such
|
||||
runners on one address, each busy all day, make 345,600 and pass the day limit
|
||||
of 300,000. Three busy at once make 360 a minute and pass the minute limit
|
||||
of 300. Their address is then banned, their running jobs fail, and each 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`.
|
||||
172,800 requests, under the request limits. Two such runners on one address,
|
||||
each busy all day, make 345,600 and pass the day limit of 300,000; one stays
|
||||
under it. Their address is then banned, their running jobs fail, and each
|
||||
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
|
||||
|
||||
|
||||
+7
-9
@@ -99,20 +99,18 @@ that take `off`, the `_FILE` form, and what each group does and why.
|
||||
|
||||
## Request rate limits, per client
|
||||
|
||||
- `SWWAF_RATE_LIMIT_PER_MINUTE` (default `300`, 5 a second): the most requests a
|
||||
client may make in a minute. A count or `off`. The default catches, within
|
||||
seconds, a client firing requests as fast as it can. It leaves room for an
|
||||
automation fleet's busiest minute and a few quick first visits with an empty
|
||||
cache.
|
||||
- `SWWAF_RATE_LIMIT_PER_MINUTE` (default `off`): the most requests a client may
|
||||
make in a minute. A count or `off`. It is off by default so that a short burst
|
||||
of API requests breaks no limit on its own.
|
||||
- `SWWAF_RATE_LIMIT_PER_HOUR` (default `30000`, about 8 a second): the most
|
||||
requests a client may make in an hour. A count or `off`. The default leaves
|
||||
room for an automation fleet at work. With the minute limit raised or off, it
|
||||
catches a crawler that keeps up 10 a second.
|
||||
room for an automation fleet at work, and catches a crawler that keeps up 10 a
|
||||
second.
|
||||
- `SWWAF_RATE_LIMIT_PER_DAY` (default `300000`, about 3.5 a second): the most
|
||||
requests a client may make in a day. A count or `off`. The default catches a
|
||||
crawler that keeps up 4 a second around the clock.
|
||||
- Counting cannot catch a client that stays under all three limits (see "Request
|
||||
rate limits, per client" in [`SPEC.md`](../SPEC.md)).
|
||||
- Counting cannot catch a client slower than these limits (see "Request rate
|
||||
limits, per client" in [`SPEC.md`](../SPEC.md)).
|
||||
- `SWWAF_RATE_LIMIT_EXEMPT_PATHS` (default empty): path prefixes exempt from
|
||||
rate limiting and the byte limits, such as `/assets/`.
|
||||
|
||||
|
||||
@@ -506,7 +506,7 @@ func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
||||
AllowNets: env.netblocks("SWWAF_ALLOW_NETS", ""),
|
||||
RateLimitExemptNets: env.netblocks("SWWAF_RATE_LIMIT_EXEMPT_NETS", ""),
|
||||
DenyNets: env.netblocks("SWWAF_DENY_NETS", ""),
|
||||
RateLimitPerMinute: env.count("SWWAF_RATE_LIMIT_PER_MINUTE", "300"),
|
||||
RateLimitPerMinute: env.count("SWWAF_RATE_LIMIT_PER_MINUTE", off),
|
||||
RateLimitPerHour: env.count("SWWAF_RATE_LIMIT_PER_HOUR", "30000"),
|
||||
RateLimitPerDay: env.count("SWWAF_RATE_LIMIT_PER_DAY", "300000"),
|
||||
RateLimitExemptPaths: env.pathPrefixes("SWWAF_RATE_LIMIT_EXEMPT_PATHS", ""),
|
||||
|
||||
@@ -262,7 +262,7 @@ func TestDefaults(t *testing.T) {
|
||||
UpstreamResponseTimeout: 30 * time.Minute,
|
||||
RequestMaxBytes: 100 << 20,
|
||||
ResponseMaxBytes: 1 << 30,
|
||||
RateLimitPerMinute: 300,
|
||||
RateLimitPerMinute: 0,
|
||||
RateLimitPerHour: 30000,
|
||||
RateLimitPerDay: 300000,
|
||||
BanResponse: 403,
|
||||
@@ -2375,7 +2375,7 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
|
||||
allowNets: "",
|
||||
rateLimitExemptNets: "",
|
||||
denyNets: "",
|
||||
rateLimitPerMinute: "300",
|
||||
rateLimitPerMinute: off,
|
||||
rateLimitPerHour: "30000",
|
||||
rateLimitPerDay: "300000",
|
||||
rateLimitExemptPaths: "",
|
||||
|
||||
@@ -54,6 +54,8 @@ const (
|
||||
greeting = "hello from the app"
|
||||
// placed is the client the tests' lookup databases place.
|
||||
placed = "203.0.113.9"
|
||||
// off is the value that switches a setting off.
|
||||
off = "off"
|
||||
)
|
||||
|
||||
// output collects what smallwebwaf writes on stdout.
|
||||
@@ -118,7 +120,7 @@ func run(ctx context.Context, env map[string]string, out *output) int {
|
||||
LookupEnv: func(name string) (string, bool) {
|
||||
value, ok := env[name]
|
||||
if !ok && name == "SWWAF_LOOKUP_SOURCE" {
|
||||
return "off", true
|
||||
return off, true
|
||||
}
|
||||
|
||||
return value, ok
|
||||
@@ -502,7 +504,7 @@ func TestRuleFileAddedWhileRunningTakesEffect(t *testing.T) {
|
||||
rulesDir: dir,
|
||||
// The requests sent until the rule takes effect must not break a
|
||||
// rate limit, whose ban would refuse them too.
|
||||
"SWWAF_RATE_LIMIT_PER_MINUTE": "off",
|
||||
"SWWAF_RATE_LIMIT_PER_MINUTE": off,
|
||||
}
|
||||
|
||||
out := runUntilStopped(t, env, func(url string) {
|
||||
@@ -1244,7 +1246,7 @@ func wantStartingLine(t *testing.T, line map[string]any, appURL, dir string) {
|
||||
"SWWAF_ALLOW_NETS": "",
|
||||
rateLimitExemptNets: "",
|
||||
"SWWAF_DENY_NETS": "",
|
||||
"SWWAF_RATE_LIMIT_PER_MINUTE": "300",
|
||||
"SWWAF_RATE_LIMIT_PER_MINUTE": off,
|
||||
"SWWAF_RATE_LIMIT_PER_HOUR": "30000",
|
||||
rateLimitPerDay: "300000",
|
||||
"SWWAF_RATE_LIMIT_EXEMPT_PATHS": "",
|
||||
|
||||
Reference in New Issue
Block a user