`smallwebwaf` with no command now prints the commands and exits with status 2. The image runs rproxy, or servewaf with SMALLWEBWAF_COMMAND=servewaf. The passing-on code, its settings and its log fields and metric go. internal/proxy keeps the checks, answers, reports and own endpoints; its tests run through the real rproxy. The documents describe the pair. Judgement call: clients.json keeps token_refused, since state files refuse unknown fields. Deviation: script/gitea-requests sets SWWAF_LOG_REQUEST_HEADERS, to label requests. Model: opus-5-5
9.3 KiB
The request log's fields
The fields of the JSON objects rproxy and servewaf write about each request.
"Request log" and "The log" in SPEC.md say when a line is
written and how the log is sent to a syslog server. A field that does not apply
to a request is left out.
servewaf's lines
smallwebwaf servewaf writes two lines for each question it answers, joined by
request_id. The field servewaf_line says which of the two a line is:
decision: the line it writes once it has answered the question;report: the line it writes whenrproxy's report comes, with everything known. The repetition is intended.
A request to its own endpoints writes one line, without servewaf_line. A
report writes no line but the report line of the request it names.
These questions write no line:
- a question refused with
400(see "The question" under "servewaf" inSPEC.md); - a question whose body stopped arriving, which gets no answer.
A report line:
{"type":"request","servewaf_line":"report","time":"2026-10-03T12:00:00.123Z","instance":"fsn1/servewaf","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"waf_score":0,"duration_total":3.217,"duration_checks":0.041,"duration_waf":0.029,"duration_upstream_total":3.104}
Every line
Every one of these lines has:
type:request(the process's own messages haveprocess);- the standard web log fields;
request_id,peer_ipandclient_group;asn,as_nameandcountry;actionandduration_total.
Standard web log fields
time: whenservewafhad read the question's body, RFC 3339 with milliseconds, in UTC. Thereportline gives the same time as thedecisionline.instance:SWWAF_INSTANCE_NAME.client_ip: the visitor's address, asrproxyworked it out.method,pathandquery: the visitor's request.scheme: how the visitor reachedrproxyor the trusted proxy in front of it.host: the site the visitor asked for.protocol: the question's own,HTTP/1.1.status:- on the
decisionline, whatservewafanswered:200forallow, the status ofdeny, or0forclose, as the visitor is then sent nothing; - on the
reportline, what the visitor got,0if nothing.
- on the
request_bytes:- on the
decisionline, how much of the question's body the Core Rule Set read; - on the
reportline, the bytes of the visitor's body thatrproxyread, with what a WebSocket carried from the visitor.
- on the
response_bytes:- on the
decisionline, the body ofservewaf's answer; - on the
reportline, the bytes of the answer's body, with what a WebSocket carried from the app.
- on the
refereranduser_agent.
Request detail
request_id: the idrproxymade for the request when it arrived.peer_ip: the address ofrproxy's TCP peer, normally traefik.forwarded_for: theX-Forwarded-Forheader the visitor's request came with.client_group: the client as rate limiting counts it, an IPv4 address as a /32 or an IPv6 group.asn,as_nameandcountry: the client's, as looked up. Each is empty when:- lookups are off;
- the client is not looked up;
- the source cannot place the client;
- GeoJS had not answered by the time
servewafanswered.
content_type: the visitor'sContent-Type.content_length: the body length the visitor announced.request_headers: the headers listed inSWWAF_LOG_REQUEST_HEADERS, by name in lower case.has_authorizationandhas_cookie: true when the request had those headers. Their values are never logged.
Response detail
response_content_type: the type ofservewaf's own answer.aborted: true whenrproxywent away before the answer.
Decision
-
action: what was done:forward: answeredallow, so thatrproxypasses it to the app;denied: refused forSWWAF_DENY_NETS, or for a reputation source with the actiondeny;banned: refused under a ban, or for abanrule, a trap path or a CrowdSec decision, each of which bans the client;country_denied,rate_limited,rule_blockedandwaf_blocked: refused by that check;too_large: over a header limit;admin: answered at one ofservewaf's own endpoints, or404for a path under/_smallwebwaf/.
-
would_action: inobservemode, whatenforcemode would have done with a request it would have refused.actionthen names what was done, normallyforward. -
limit_percentandlimit_percent_setting: the client's percentage of the rate limits and the setting that lowered it, when one did.bytes_percentandbytes_percent_settingdo the same for the byte limits. -
counts: the client's totals as rate limiting counts them, this request included. It is left out when rate limiting did not count the request. It holds:minute,hourandday: requests;minute_bytes,hour_bytesandday_bytes: bytes.
A count can have a fraction (see "Counting method" in
SPEC.md). The bytes of an allowed request are counted when its report comes. So on thedecisionline, the bytes are without this request's. -
limit_hit: the limit broken:minute,hourordayfor a rate limit;minute_bytes,hour_bytesorday_bytesfor a byte limit;error_burst.
A request whose bytes broke a limit was passed on whole, so its
actionstaysforward. Only itsreportline gives that limit. -
rule_ids: the rule file rules that matched, in order, up to the one that refused. -
waf_scoreandwaf_rule_ids: the Core Rule Set's score and the rules that matched, for a request it inspected. -
reputation: the sources the client is on, in this order:- blocklist URLs;
- the CrowdSec decision list's URL;
- DNS blocklist zones;
abuseipdb.
-
offence:limit, for a request that broke a limit. -
ban_expires: when the client's ban ends, for a request that made one or was refused under one: a time, orpermanent.
Timings
All in milliseconds:
duration_total:- on the
decisionline, fromservewafhaving read the question's body to the line; - on the
reportline, fromrproxyhaving read the request's headers to the end of the answer.
- on the
duration_checks: to the end of the checks;duration_waf: the part of that the Core Rule Set took;duration_upstream_total: on thereportline of an allowed request, from the hand-over to the app to the end of the answer.
Requests to its own endpoints
The line of a request to servewaf's own endpoints has these fields, with these
meanings where they differ:
client_ipandpeer_ip: the request's TCP peer.request_id: a new random one of 26 letters and digits.scheme:http.time: when the request's headers had been read.request_bytes: the bytes read of a ban to add, and otherwise0.action:admin.duration_total: from the request's headers being read to the line.
rproxy's line
smallwebwaf rproxy writes one short line for each request. request_id joins
it to servewaf's two. It has these fields and no others:
type:request.time: when the request arrived, RFC 3339 with milliseconds, in UTC.request_id: the idrproxymade for the request when it arrived.client_ip: the visitor's address.hostandmethod.pathandquery: as the visitor sent them.status: what the visitor got,0if nothing.request_bytes: the bytes of the visitor's body thatrproxyread, with what a WebSocket carried from the visitor.response_bytes: the bytes of the answer's body, with what a WebSocket carried from the app.duration_request: milliseconds from the request's headers being read until the app had been sent the whole request, or the answer started if that was sooner. It includes the wait forservewaf.duration_response: milliseconds from then until the end of the answer.disposition: what was done with the request:allow,denyorclose. It isservewaf's answer, or the value ofSWRP_DEFAULT_DISPOSITION. It is left out when:servewafwas not asked, as for the health check;- the visitor went away while
servewafwas being asked; servewaf's answer had no validX-Smallwebwaf-Disposition;rproxycould not pass onservewaf's answer, and answered502.
default_disposition:truewhenSWRP_DEFAULT_DISPOSITIONgavedisposition, and otherwise left out.
For example:
{"type":"request","time":"2026-10-03T12:00:00.123Z","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","client_ip":"203.0.113.9","host":"app.example","method":"GET","path":"/","query":"","status":200,"request_bytes":0,"response_bytes":5120,"duration_request":1.204,"duration_response":3.104,"disposition":"allow"}