Files
smallwebwaf/docs/gitea.md
T
clawbot 866b948ec0
check / check (push) Waiting to run
Gitea setup: Core Rule Set blocks attacks, not ordinary Gitea traffic (closes #198)
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
2026-11-06 20:17:07 +00:00

9.2 KiB

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