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
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 withdeploy/gitea/crs-exclusions.confmounted read-only.
To start it:
- In
deploy/gitea/compose.yaml, replacegit.example.orgwith the site's host name. - Replace each
10.99.0with the first three numbers of a/24subnet that no other network on the host uses. The/24must lie inside10.0.0.0/8,172.16.0.0/12or192.168.0.0/16, becauserproxybelievesX-Forwarded-Foronly from those by default. Replace eachfd10:99with the first two hextets of an unused/64subnet whose third and fourth hextets are 0, as infd10:99::/64. - Set
SMALLWEBWAF_IMAGEto thesmallwebwafimage, named by digest. - Set
GITEA_SECRET_KEYto the output ofdocker run --rm <gitea's image> gitea generate secret SECRET_KEY, with gitea's image asdeploy/gitea/compose.yamlnames it. Do this once per install, and keep the key. - Run
docker compose up --detachindeploy/gitea, withcrs-exclusions.confbesidecompose.yaml. - Make gitea's first user with
gitea admin user create, in gitea's container, as the usergit.
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 thatservewafchecks 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
servewafstopped, gitea still answers throughrproxy.
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_SIZEis2G. At the default, an upload over 100 MiB is cut off.SWRP_MAX_REQUEST_TIME_MSis1800000, 30 minutes. At the default, an upload that takes over 60 seconds is cut off.- traefik's
readTimeoutis 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_MSis900000, 15 minutes, half the default. A clone or download that takes longer is cut off.SWRP_MAX_RESPONSE_SIZEstays 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/24andfd10:99::/64. Docker gives its containers addresses in the upper half of each,10.99.0.128/25andfd10:99:0:0:8000::/65, whichSWWAF_ALLOW_NETSnames. - 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.1andfd10: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 ofSWWAF_ALLOW_NETS. The network has IPv6, so a visitor over IPv6 reaches traefik from its own address, andservewafchecks 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-Fora 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
servewafaroundrproxy. Put only containers you trust on it.
gitea's settings
REVERSE_PROXY_TRUSTED_PROXIESnamesrproxy's fixed addresses,10.99.0.2andfd10:99::2. gitea then takes each visitor's address from theX-Real-Ipheader, which traefik sets andrproxypasses on. Without it, gitea logs every visitor asrproxy.INSTALL_LOCKistrue, 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,PATCHandDELETE; - git's push, clone and fetch over HTTP:
GET,POSTandOPTIONS; - LFS:
GET,POSTandPUT; - the container registry and the other package registries:
GET,HEAD,POST,PUT,PATCHandDELETE; - 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,bodyandredirect_to; - the query parameters that hold a branch or tag name:
name,tag,rule_nameandrefSubUrl; 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-Rangeof 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:
- Find the request in
servewaf's log. Its line gives the path and query, and names the rules inwaf_rule_ids. - For each rule, add a line with
ctl:ruleRemoveTargetByIdfor the field it matched. For a field on every path, copy one of the file'sSecActionlines. For some paths only, copy one of itsSecRule REQUEST_FILENAMElines. Give it a new id. An id from 900000 to 900999 stops the start. - Above it, write a one-line comment naming the request it refused.
- Add the request to
internal/waf/testdata/gitea-requests.txt, and runmake check. - 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.