check / check (push) Waiting to run
The Gitea setup runs the Core Rule Set in block mode. Rules that refused ordinary Gitea requests are switched off only where they misfired: - deploy/gitea/crs-exclusions.conf, loaded through the new setting SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE before the Core Rule Set's rules, covers Gitea's search, file-name and branch-name fields, Referer, file paths, API attachment names and Actions artifact uploads; - smallwebwaf's defaults gain a seventh change: the registry upload path's from parameter skips 932260. A file that cannot be read, does not parse or names an id from 900000 to 900999 stops servewaf at start, in every mode. internal/waf's tests and script/gitea-setup send the recorded requests in internal/waf/testdata/gitea-requests.txt and three attacks. Model: opus-5-5
207 lines
9.2 KiB
Markdown
207 lines
9.2 KiB
Markdown
# smallwebwaf in front of gitea
|
|
|
|
`deploy/gitea/compose.yaml` runs gitea behind traefik, with `rproxy` and
|
|
`servewaf` in front of it. This document says what that setup sets and why, and
|
|
what the defaults would do without it. [`SPEC.md`](../SPEC.md) says how
|
|
`smallwebwaf` behaves.
|
|
|
|
## The setup
|
|
|
|
Four containers share one Docker network, the shared network:
|
|
|
|
- traefik, which routes the site's HTTPS requests to `rproxy`, and redirects
|
|
plain HTTP to HTTPS;
|
|
- `rproxy`, in front of gitea;
|
|
- gitea 1.27.3, as its image comes;
|
|
- `servewaf`, with a volume for its state files, and with
|
|
`deploy/gitea/crs-exclusions.conf` mounted read-only.
|
|
|
|
To start it:
|
|
|
|
1. In `deploy/gitea/compose.yaml`, replace `git.example.org` with the site's
|
|
host name.
|
|
2. Replace each `10.99.0` with the first three numbers of a `/24` subnet that no
|
|
other network on the host uses. The `/24` must lie inside `10.0.0.0/8`,
|
|
`172.16.0.0/12` or `192.168.0.0/16`, because `rproxy` believes
|
|
`X-Forwarded-For` only from those by default. Replace each `fd10:99` with the
|
|
first two hextets of an unused `/64` subnet whose third and fourth hextets
|
|
are 0, as in `fd10:99::/64`.
|
|
3. Set `SMALLWEBWAF_IMAGE` to the `smallwebwaf` image, named by digest.
|
|
4. Set `GITEA_SECRET_KEY` to the output of
|
|
`docker run --rm <gitea's image> gitea generate secret SECRET_KEY`, with
|
|
gitea's image as `deploy/gitea/compose.yaml` names it. Do this once per
|
|
install, and keep the key.
|
|
5. Run `docker compose up --detach` in `deploy/gitea`, with
|
|
`crs-exclusions.conf` beside `compose.yaml`.
|
|
6. Make gitea's first user with `gitea admin user create`, in gitea's container,
|
|
as the user `git`.
|
|
|
|
Sign-up is off: accounts are made with `gitea admin user create`. gitea starts
|
|
at its defaults, with SQLite. traefik answers with a certificate it made itself
|
|
until you add a certificate resolver, as for your other sites. SSH to gitea does
|
|
not pass through `smallwebwaf`; publish gitea's SSH port yourself if you use it.
|
|
|
|
`make gitea-setup` checks the setup. It builds the image, brings the setup up
|
|
under a name of its own, and checks:
|
|
|
|
- that a page load, a push and a clone sent through traefik pass through
|
|
`rproxy`, and that `servewaf` checks them;
|
|
- that gitea logs the visitor's address, not `rproxy`'s;
|
|
- that a request from a container on the shared network skips the checks;
|
|
- that the Core Rule Set refuses none of the ordinary gitea requests in
|
|
`internal/waf/testdata/gitea-requests.txt`, sent by a signed-in user;
|
|
- that it refuses none of the API calls with JSON bodies;
|
|
- that it refuses no fetch;
|
|
- that it refuses SQL injection, path traversal and script injection in a query
|
|
string;
|
|
- that with `servewaf` stopped, gitea still answers through `rproxy`.
|
|
|
|
## Uploads and downloads
|
|
|
|
An upload is a request:
|
|
|
|
- a git push;
|
|
- an LFS object;
|
|
- a container image layer;
|
|
- a package file;
|
|
- a release attachment.
|
|
|
|
The setup lets an upload of up to 2 GiB, sent in up to 30 minutes, through:
|
|
|
|
- `SWRP_MAX_REQUEST_SIZE` is `2G`. At the default, an upload over 100 MiB is cut
|
|
off.
|
|
- `SWRP_MAX_REQUEST_TIME_MS` is `1800000`, 30 minutes. At the default, an upload
|
|
that takes over 60 seconds is cut off.
|
|
- traefik's `readTimeout` is 30 minutes. traefik's own default of 60 seconds for
|
|
a whole request would cut off an upload first.
|
|
|
|
A clone or a download is a response:
|
|
|
|
- `SWRP_MAX_RESPONSE_TIME_MS` is `900000`, 15 minutes, half the default. A clone
|
|
or download that takes longer is cut off.
|
|
- `SWRP_MAX_RESPONSE_SIZE` stays at its default, 1 GiB. A repository whose clone
|
|
is larger needs it raised.
|
|
|
|
The byte limits stay at their defaults: no minute limit, 50 GiB an hour and 100
|
|
GiB a day.
|
|
|
|
## The checks
|
|
|
|
The Core Rule Set runs in `block` mode, the default, with gitea's exclusions
|
|
(see "The Core Rule Set's exclusions" below).
|
|
|
|
Every other check stays at its default. A client may make 120 requests a minute,
|
|
1,500 an hour and 6,000 a day. Archive downloads and blame and history pages are
|
|
what scrapers hammer, and these limits do most of the work there.
|
|
|
|
## The shared network
|
|
|
|
Containers on the shared network skip `servewaf`'s checks:
|
|
|
|
- The shared network's subnets are `10.99.0.0/24` and `fd10:99::/64`. Docker
|
|
gives its containers addresses in the upper half of each, `10.99.0.128/25` and
|
|
`fd10:99:0:0:8000::/65`, which `SWWAF_ALLOW_NETS` names.
|
|
- traefik is known on the shared network by the site's host name. So a container
|
|
there that asks for the site reaches traefik from its own address. An
|
|
automation fleet in such a container skips the checks.
|
|
- Each subnet's gateway, `10.99.0.1` and `fd10:99::1`, is the host's own address
|
|
on it. Requests docker passes on from a published port can come from the
|
|
gateway, so the gateways are left out of `SWWAF_ALLOW_NETS`. The network has
|
|
IPv6, so a visitor over IPv6 reaches traefik from its own address, and
|
|
`servewaf` checks it as itself.
|
|
- A container on the shared network that asks for the site at the host's public
|
|
address arrives from the gateway. It is checked like any visitor.
|
|
- A fleet on another host can have its address added to `SWWAF_ALLOW_NETS`.
|
|
Without that, it is counted like any client.
|
|
- traefik replaces the `X-Forwarded-For` a visitor sends, by default. Keep it
|
|
so: otherwise a visitor could name an address on the shared network, and skip
|
|
the checks.
|
|
- Anything on the shared network can reach gitea and `servewaf` around `rproxy`.
|
|
Put only containers you trust on it.
|
|
|
|
## gitea's settings
|
|
|
|
- `REVERSE_PROXY_TRUSTED_PROXIES` names `rproxy`'s fixed addresses, `10.99.0.2`
|
|
and `fd10:99::2`. gitea then takes each visitor's address from the `X-Real-Ip`
|
|
header, which traefik sets and `rproxy` passes on. Without it, gitea logs
|
|
every visitor as `rproxy`.
|
|
- `INSTALL_LOCK` is `true`, as "The setup" above says.
|
|
|
|
## Methods
|
|
|
|
`rproxy` passes on only these methods: `GET`, `HEAD`, `POST`, `OPTIONS`, `PUT`,
|
|
`DELETE`, `PATCH` and `QUERY`. gitea 1.27.3 needs no other. Its routes use:
|
|
|
|
- pages and the API: `GET`, `HEAD`, `POST`, `PUT`, `PATCH` and `DELETE`;
|
|
- git's push, clone and fetch over HTTP: `GET`, `POST` and `OPTIONS`;
|
|
- LFS: `GET`, `POST` and `PUT`;
|
|
- the container registry and the other package registries: `GET`, `HEAD`,
|
|
`POST`, `PUT`, `PATCH` and `DELETE`;
|
|
- Actions runners: `POST`.
|
|
|
|
## The log
|
|
|
|
`rproxy` and `servewaf` write each query string to the log whole. A token sent
|
|
in the query string, as `?token=`, is then in the log. Send it in the
|
|
`Authorization` header instead.
|
|
|
|
## Actions runners
|
|
|
|
A runner on the shared network skips the checks. One on another host is counted
|
|
like any client. A runner older than version 0.4 (April 2026) asks for work
|
|
every 2 seconds, 1,800 requests an hour. While it runs a job, it reports the
|
|
job's log and state every second instead, two requests a second.
|
|
|
|
So one idle runner makes 1,800 requests an hour, more than the hour limit of
|
|
1,500. A busy one, at two requests a second, passes the minute limit of 120
|
|
within two minutes. Its address is then banned, its 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`.
|
|
|
|
## The Core Rule Set's exclusions
|
|
|
|
`deploy/gitea/crs-exclusions.conf` switches single Core Rule Set rules off where
|
|
they refused ordinary gitea requests. `servewaf` loads it from
|
|
`SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE`, before the Core Rule Set's rules. Each
|
|
exclusion covers one rule, in these places only:
|
|
|
|
- `q`, the text of every search;
|
|
- the query parameters that hold a file name: `file`, `path`, `template`, `body`
|
|
and `redirect_to`;
|
|
- the query parameters that hold a branch or tag name: `name`, `tag`,
|
|
`rule_name` and `refSubUrl`;
|
|
- `Referer`, on a page opened from search results or from a file's page;
|
|
- the path of a file's page, raw file, blame, history and edit page, and of the
|
|
API's call for its contents;
|
|
- `name`, the file name of an attachment uploaded through the API;
|
|
- the header `Content-Range` of an Actions artifact upload.
|
|
|
|
gitea reads these as text to search for, as names in a repository, or, for
|
|
`Content-Range`, as the part of a file an upload carries. It never runs them as
|
|
SQL, a command or a page's script. So an attack that only the excluded rules
|
|
would catch there reaches gitea and does nothing. Every other field and path
|
|
keeps every rule.
|
|
|
|
To add an exclusion for a request the Core Rule Set refuses:
|
|
|
|
1. Find the request in `servewaf`'s log. Its line gives the path and query, and
|
|
names the rules in `waf_rule_ids`.
|
|
2. For each rule, add a line with `ctl:ruleRemoveTargetById` for the field it
|
|
matched. For a field on every path, copy one of the file's `SecAction` lines.
|
|
For some paths only, copy one of its `SecRule REQUEST_FILENAME` lines. Give
|
|
it a new id. An id from 900000 to 900999 stops the start.
|
|
3. Above it, write a one-line comment naming the request it refused.
|
|
4. Add the request to `internal/waf/testdata/gitea-requests.txt`, and run
|
|
`make check`.
|
|
5. Restart `servewaf`.
|
|
|
|
The Core Rule Set still refuses the page of a file in a directory named `...`,
|
|
which reads as path traversal.
|
|
|
|
With `SWWAF_WAF_BODY_LIMIT` set, it also refuses issue text, comments, wiki
|
|
pages and files saved in the web editor that hold shell commands or code.
|
|
|
|
`make gitea-requests` runs gitea behind `rproxy` and `servewaf` at the defaults,
|
|
without these exclusions. It reports each request the Core Rule Set refuses
|
|
there.
|