diff --git a/README.md b/README.md index bf6cdb6..e8c0427 100644 --- a/README.md +++ b/README.md @@ -192,9 +192,9 @@ account, a key, a token or an alert destination stays off until you give one. - Any `SWWAF_` setting can be given as a file instead, for secrets and long lists: `SWWAF_ADMIN_TOKEN_FILE` names the file that holds `SWWAF_ADMIN_TOKEN`. The file's contents are the value, less one newline at the end. Setting both - forms stops the start. `SWWAF_LOG_REMOTE_TLS_CA_FILE` already names a file, so - it has no `_FILE` form, and no `SWRP_` setting has one, since `rproxy` never - touches disk. + forms stops the start. `SWWAF_LOG_REMOTE_TLS_CA_FILE` and + `SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have no + `_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches disk. - A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`. - A size is a number of bytes with an optional `K`, `M` or `G`, in powers of 1024: `512K`, `100M`, `5G`. @@ -272,6 +272,7 @@ Every setting the code reads, with its default: | `SWWAF_WAF_DISABLED_RULES` | `920340,920420,920440,920640,930130,930140` | Core Rule Set rule ids switched off; a list you give replaces the default, so repeat these six to keep them off. | | `SWWAF_WAF_EXEMPT_PATHS` | empty | Path prefixes the Core Rule Set does not inspect. | | `SWWAF_WAF_BODY_LIMIT` | `off` | How much of a request body the Core Rule Set reads, a size up to `64K`; `off` reads none. | +| `SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE` | unset | A file of Core Rule Set exclusions, as Coraza directives, that switch rules off for some fields or paths only; `deploy/gitea/crs-exclusions.conf` is gitea's. | | `SWWAF_BLOCKLIST_URLS` | empty | Blocklists to fetch, text files of addresses and netblocks one to a line, such as `https://www.spamhaus.org/drop/drop.txt`. | | `SWWAF_BLOCKLIST_REFRESH` | `24h` | How long after a list was last tried it is fetched again, `1h` or more. | | `SWWAF_BLOCKLIST_ACTION` | `deny` | What a client on a blocklist gets: `deny`, `limit:` of every limit, or `log`. | @@ -636,10 +637,14 @@ and the status for a size or time limit, such as `413` for a body too large. headers of every request, and no body unless `SWWAF_WAF_BODY_LIMIT` is set. In front of a code forge, bodies carry what people write and publish, which the rules take for attacks. -- `smallwebwaf` changes the Core Rule Set in six ways so that it lets ordinary +- `smallwebwaf` changes the Core Rule Set in seven ways so that it lets ordinary requests to a code forge through, such as git over HTTP and sign-in through Git Credential Manager. No setting undoes them; "Attack detection" in [`SPEC.md`](SPEC.md) lists them. +- `SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's + own fields or paths. gitea's exclusions, in + `deploy/gitea/crs-exclusions.conf`, let its searches, file names and branch + names through. - Rule files are plain text, one rule to a line, edited by hand and read again while `servewaf` runs. The image ships one file of probes no real visitor sends (see [`docs/rules.md`](docs/rules.md)). diff --git a/SPEC.md b/SPEC.md index 875ba19..46ab4f5 100644 --- a/SPEC.md +++ b/SPEC.md @@ -36,7 +36,8 @@ to them. - the state files; - a lookup database; - a `SWWAF_` setting given as a file; - - the file `SWWAF_LOG_REMOTE_TLS_CA_FILE` names. + - the file `SWWAF_LOG_REMOTE_TLS_CA_FILE` names; + - the file `SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE` names. ## Non-goals @@ -81,7 +82,7 @@ to them. of bans is full. - `internal/rules`: reads the rule files at start and again when they change, and tells which rules a request matches. - - `internal/waf`: the Core Rule Set, with the six changes listed under + - `internal/waf`: the Core Rule Set, with the seven changes listed under "Attack detection", as Coraza directives. - `internal/lookup`: looks clients up through GeoJS or in the lookup database. @@ -325,9 +326,10 @@ Conventions: lists: `SWWAF_ADMIN_TOKEN_FILE` names the file that holds `SWWAF_ADMIN_TOKEN`. The file's contents are the value, less one newline at the end, and are checked as the value itself would be. Setting both forms stops the start, and - so does naming a file that cannot be read. `SWWAF_LOG_REMOTE_TLS_CA_FILE` - already holds a file name, so it has no `_FILE` form. No `SWRP_` setting has - one, since `rproxy` never touches disk. + so does naming a file that cannot be read. `SWWAF_LOG_REMOTE_TLS_CA_FILE` and + `SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE` already hold a file name, so they have + no `_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches + disk. - Settings, files included, are read once at start. Changing one means restarting the container. The files `servewaf` watches while it runs are its state files, its rule files and the lookup database. @@ -583,6 +585,24 @@ sends it. probes these rules caught (see "Rule files"). A list you give replaces the default, so name these six ids in it to keep them off. +- `SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE` names a file of exclusions, written as + Coraza directives. An exclusion switches one rule off for some fields or paths + only, where `SWWAF_WAF_DISABLED_RULES` switches it off everywhere. + `deploy/gitea/crs-exclusions.conf` holds gitea's. + - `servewaf` loads the file after `smallwebwaf`'s own setup rules and before + the Core Rule Set's rules. The Core Rule Set puts its own file of + exclusions that run before the rules in the same place. + - So the file holds only `SecAction` and `SecRule` lines that exclude with + `ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. These run with each + request, before the rules they exclude, those of phase 1 included. + - A rule that `SWWAF_WAF_DISABLED_RULES` switches off and the file names is + simply off. + - The start stops on a file that cannot be read or does not parse, whatever + `SWWAF_WAF_MODE` is. + - It stops too on a file that names a number from 900000 to 900999, the ids + of `smallwebwaf`'s own rules, alone or in a range. Comment lines are not + checked. + - `SWWAF_WAF_BODY_LIMIT` switches body inspection on, up to that size. It takes at most `64K`, since `servewaf` gets no more of a body than that (see "The answer"). Form data and multipart bodies are read up to the size. JSON and XML @@ -612,7 +632,7 @@ sends it. scanner from an ordinary client. In front of gitea, registry and git clients get `404` and `401` by design, hundreds of times a minute. -`smallwebwaf` changes the Core Rule Set 4.25.0 in six ways, since in front of +`smallwebwaf` changes the Core Rule Set 4.25.0 in seven ways, since in front of gitea it would otherwise refuse ordinary requests. The changes hold in front of every app, and no setting undoes them: @@ -669,6 +689,10 @@ every app, and no setting undoes them: must check it itself. 6. Responses are not inspected. A raw file from a repository, such as a shell script, looks to the response rules like source code leaking from the server. +7. The `from` parameter of a container registry's upload path, + `/v2//blobs/uploads/`, is not checked for command names (932260). When + a push reuses a layer, `from` names the image the layer comes from. An image + such as `owner/python3-app` would have that push refused. The parameter and cookie names in these changes are matched without regard to case, as Coraza matches them. diff --git a/deploy/gitea/compose.yaml b/deploy/gitea/compose.yaml index d5652a7..6a2f317 100644 --- a/deploy/gitea/compose.yaml +++ b/deploy/gitea/compose.yaml @@ -104,11 +104,12 @@ services: # address of an automation fleet on another host here too; without # that, it is counted like any client. SWWAF_ALLOW_NETS: 10.99.0.128/25,fd10:99:0:0:8000::/65 - # In block mode the Core Rule Set refuses everyday Gitea searches - # and file names, and 31 refusals in a minute ban the address. - SWWAF_WAF_MODE: detect + # The Core Rule Set's exclusions for Gitea's searches, file names + # and branch names. + SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE: /etc/smallwebwaf/crs-exclusions.conf volumes: - servewaf-state:/var/lib/smallwebwaf + - ./crs-exclusions.conf:/etc/smallwebwaf/crs-exclusions.conf:ro networks: - shared logging: *logging diff --git a/deploy/gitea/crs-exclusions.conf b/deploy/gitea/crs-exclusions.conf new file mode 100644 index 0000000..ea90507 --- /dev/null +++ b/deploy/gitea/crs-exclusions.conf @@ -0,0 +1,150 @@ +# The Core Rule Set's exclusions for Gitea, which servewaf loads from +# SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE. servewaf loads this file before the Core +# Rule Set's rules, so it holds only exclusions made while a request is checked: +# SecAction and SecRule lines with ctl:ruleRemoveTargetById or +# ctl:ruleRemoveById. Each switches one rule off for the Gitea fields or paths +# where it refused ordinary Gitea requests. A SecAction applies on every path, a +# SecRule on the paths it matches. Gitea sends these in the query string: +# - q: the text of every search; +# - file, path, template, body and redirect_to: a file name, alone or in +# JSON or a link; +# - name, tag, rule_name and refSubUrl: a branch or tag name. +# docs/gitea.md says how to add an exclusion. + +# Searches. +# 930110: a search for ../ +SecAction "id:10001,phase:1,pass,nolog,ctl:ruleRemoveTargetById=930110;ARGS:q" +# 931100: a search for http://127.0.0.1:8080 +SecAction "id:10002,phase:1,pass,nolog,ctl:ruleRemoveTargetById=931100;ARGS:q" +# 932140: a search for if (a == b) { +SecAction "id:10003,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932140;ARGS:q" +# 932235: a search for `make check` +SecAction "id:10004,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932235;ARGS:q" +# 932250: a search for ls -la +SecAction "id:10005,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932250;ARGS:q" +# 932380: a search for `whoami` +SecAction "id:10006,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932380;ARGS:q" +# 933100: a search for +SecAction "id:10011,phase:1,pass,nolog,ctl:ruleRemoveTargetById=941100;ARGS:q" +# 941110: a search for