# 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 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.