# 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. 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 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`. 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 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, and the byte limits with it ("Byte limits, per client" in `SPEC.md`). The byte limits stay at their defaults. Two uploads of 2 GiB from one address in one minute pass the minute's limit of 4 GiB, and ban the address. ## The checks `SWWAF_WAF_MODE` is `detect`. The Core Rule Set then logs a match, with its rule ids, and refuses nothing. In `block` mode, the default, it refuses everyday gitea searches and file names. The section "What the Core Rule Set refuses in `block` mode" below lists them. Each such refusal counts toward the error burst, so 31 in one minute ban the address. Every other check stays at its default. A client may make 500 requests a minute, 20,000 an hour and 300,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, the default request limits are sized for such a fleet. - 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, 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. Several runners on one address can pass them: - Two busy all day make 345,600 requests, and pass the day limit of 300,000. - Three busy at once make 21,600 an hour, and pass the hour limit of 20,000. - Five busy at once make 600 a minute, and pass the minute limit of 500. 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 refuses in `block` mode These notes record what the Core Rule Set does in front of gitea in `block` mode, at the other defaults. They are observations, not requirements. `script/gitea-requests` runs gitea behind `rproxy` and `servewaf` at the defaults. It sends through them a sample of gitea's ordinary use and the requests named below. It reports each request the Core Rule Set refuses, so that these notes can be checked again when gitea or the Core Rule Set changes version. In `detect` mode, as the setup runs it, the same requests are logged with their rule ids, and pass. Each refusal answers only that request ("Bans" in `SPEC.md` says what it counts toward). The request log names the rule in `waf_rule_ids`, which `SWWAF_WAF_DISABLED_RULES` can switch off. ### What it lets through The Core Rule Set lets gitea's ordinary use through, apart from the refusals in the sections after this one: - browsing and views of files in a repository, with their history, blame and the file tree; - diffs, including their hidden lines and large files, and pull request review; - git's clone, fetch and push over HTTP; - signing in, including the return to the page a visitor came from, and sign-in with Git Credential Manager, git-credential-oauth or tea; - the API's calls for a file and its commits; - pushing and pulling container images and packages; - Actions runners, and artifacts uploaded with `actions/upload-artifact@v4`; - posting issues, pull requests, comments, wiki pages and files saved in the web editor, code included, since no body is read; - the page gitea shows after a change, with its message naming what was done. ### A query string that reads as an attack Most often this is a search. A search term is a query parameter, which the Core Rule Set checks against its lists of system files and commands (see the fourth change under "Attack detection" in `SPEC.md`). A release attachment uploaded through the API as `docker-compose.yml` is refused the same way. Examples: - a name on those lists, such as `package.json`, `.gitignore` or `docker-compose.yml`; - text that starts with a command name, such as `python3` or `ssh key`, or has one right after a `/` in its first word, such as `feat/docker-support`; - a shell command with its options or a system path (`ls -la`, `sed -i`, `/bin/sh`), or a command in backticks; - script code (`fetch(`, `${VAR}`, `process.env`), HTML (`