@@ -172,6 +172,18 @@ the runner. More than 30 such refusals from its address within a minute, as when
|
||||
many jobs upload at once, ban it through the error burst.
|
||||
`actions/upload-artifact@v4` does not send the header.
|
||||
|
||||
## What body inspection refuses
|
||||
|
||||
With `SWWAF_WAF_BODY_LIMIT` set, the Core Rule Set reads request bodies too. In
|
||||
front of gitea it then refuses these, until their rule ids, named in the request
|
||||
log, are added to `SWWAF_WAF_DISABLED_RULES`:
|
||||
|
||||
- issue and comment text, wiki pages and files saved in the web editor that hold
|
||||
shell commands or code (932125, 932235, 932250 and others);
|
||||
- package descriptions that show code;
|
||||
- PyPI uploads (922130);
|
||||
- attachments named like `debug.log` or `config.yml` (932180).
|
||||
|
||||
## What `script/gitea-requests` sends
|
||||
|
||||
Of the requests above, the script sends:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# What the image holds
|
||||
|
||||
The `smallwebwaf` image is the base of each app's own image. The requirements
|
||||
are under "Deployment" in [`SPEC.md`](../SPEC.md); this file lists what the
|
||||
image holds, how each part is pinned, and why.
|
||||
|
||||
## Contents
|
||||
|
||||
- Ubuntu 26.04 LTS, the newest long-term support release of Ubuntu, pinned by
|
||||
digest. The image moves to the next LTS release when that ships.
|
||||
- `ca-certificates`, `nix-bin` and `runit` from Ubuntu, installed from a dated
|
||||
snapshot of its archive and checked by hash. The Ubuntu image has no CA
|
||||
certificates; without them Nix cannot download packages and `smallwebwaf`
|
||||
cannot reach GeoJS.
|
||||
- Nix and nixpkgs, fixed at one release. Root uses Nix directly; no Nix daemon
|
||||
runs. `nix-bin` creates no build users, so `build-users-group =` in
|
||||
`/etc/nix/nix.conf` lets root build without them.
|
||||
- runit, and `runsvinit` as the entrypoint, as the owner's code style guide asks
|
||||
for service containers. No package carries `runsvinit`, so the image builds it
|
||||
from its archived source (`github.com/peterbourgon/runsvinit`, unchanged
|
||||
since 2015) at a fixed commit hash, with a `go.mod` the build writes.
|
||||
`runsvinit` starts runit's `runsvdir`, which runs the `run` script of each
|
||||
directory under `/etc/service` and runs it again whenever it exits. Ubuntu's
|
||||
`sv` looks for services there too.
|
||||
- The `smallwebwaf` binary, and a user of its own, `smallwebwaf` (uid and gid
|
||||
65532).
|
||||
- The service directory `/etc/service/smallwebwaf`. Its `run` script waits one
|
||||
second (`sleep 1`), makes `SWWAF_STATE_DIR` and every file in it belong to the
|
||||
`smallwebwaf` user, and starts `smallwebwaf` as that user with runit's
|
||||
`chpst`.
|
||||
- The directory `/var/lib/smallwebwaf` for the state files, and
|
||||
`/etc/smallwebwaf/rules.d` with the default rule file.
|
||||
- Port 8080 declared (`EXPOSE 8080`), and the health check (`HEALTHCHECK`) that
|
||||
"The health check" in `SPEC.md` describes.
|
||||
|
||||
## Pinning
|
||||
|
||||
- Ubuntu's packages come from Ubuntu's snapshot service, the archive as it was
|
||||
at the moment the Dockerfile names (`--snapshot 20261001T000000Z`), never
|
||||
earlier than the pinned Ubuntu image's date. The Dockerfile names the SHA-256
|
||||
hash of each of the snapshot's `InRelease` files, which cover every package
|
||||
list and so every package, and the build checks them before installing. The
|
||||
hashes are the amd64 archive's, so the image is built for amd64 only. The
|
||||
snapshot is fetched over HTTPS with the CA certificates of the Go image that
|
||||
builds `smallwebwaf`, itself pinned by digest, since the Ubuntu image has
|
||||
none.
|
||||
- nixpkgs comes from a release file on `releases.nixos.org` (`nixexprs.tar.xz`
|
||||
of `nixos-26.05.11045.774debe7a0d1`), named with its SHA-256 hash, not from a
|
||||
GitHub archive, whose bytes can change. It is set up for root as `nixpkgs`.
|
||||
Root's Nix profile, `/nix/var/nix/profiles/default/bin`, is last on the
|
||||
`PATH`, so that no package hides the image's own commands; busybox's `sv`, for
|
||||
one, looks for services elsewhere. Unpacked, nixpkgs takes about 500 MiB.
|
||||
- An app built on the image gets the same packages each time it is built.
|
||||
Security fixes from Ubuntu and nixpkgs come with a newer `smallwebwaf` image,
|
||||
which an app takes by changing the digest in its `FROM` line. The image moves
|
||||
to each new nixpkgs release, every six months.
|
||||
@@ -0,0 +1,83 @@
|
||||
# The metrics
|
||||
|
||||
`GET /_smallwebwaf/metrics` answers in the Prometheus text format. The
|
||||
requirements are under "Metrics endpoint" in [`SPEC.md`](../SPEC.md); this file
|
||||
lists the metrics. Every metric carries the label `instance`, set to
|
||||
`SWWAF_INSTANCE_NAME`; Prometheus keeps it as `exported_instance` unless the
|
||||
scrape sets `honor_labels: true`. No metric carries a client's address.
|
||||
|
||||
## Traffic
|
||||
|
||||
- `smallwebwaf_requests_total`, `smallwebwaf_request_bytes_total` and
|
||||
`smallwebwaf_response_bytes_total`, by `status_class` such as `2xx` and by
|
||||
`action`;
|
||||
- `smallwebwaf_request_duration_seconds` and
|
||||
`smallwebwaf_upstream_duration_seconds`, as histograms;
|
||||
- `smallwebwaf_requests_in_flight`.
|
||||
|
||||
## Limits and bans
|
||||
|
||||
- `smallwebwaf_rate_limit_hits_total`, by `window` and by `kind` (`requests`,
|
||||
`bytes` or `refusals`);
|
||||
- `smallwebwaf_size_and_time_limit_hits_total`, by `limit`, the setting passed;
|
||||
- `smallwebwaf_offences_total`, by `kind`;
|
||||
- `smallwebwaf_bans_made_total`, by `cause`;
|
||||
- `smallwebwaf_active_bans` and `smallwebwaf_permanent_bans`.
|
||||
|
||||
## Attack detection
|
||||
|
||||
- `smallwebwaf_rule_matches_total`, by `rule_id` and `action`, and
|
||||
`smallwebwaf_rules_loaded`;
|
||||
- `smallwebwaf_waf_matches_total`, by `mode` and `rule_id`, for the rules that
|
||||
fired.
|
||||
|
||||
## Countries and AS numbers
|
||||
|
||||
For the `SWWAF_METRICS_TOP_N` busiest of each, the rest as `other`:
|
||||
|
||||
- `smallwebwaf_country_requests_total`,
|
||||
`smallwebwaf_country_request_bytes_total`,
|
||||
`smallwebwaf_country_response_bytes_total` and
|
||||
`smallwebwaf_country_list_refusals_total`, by `country`;
|
||||
- `smallwebwaf_asn_requests_total`, `smallwebwaf_asn_request_bytes_total` and
|
||||
`smallwebwaf_asn_response_bytes_total`, by `asn`.
|
||||
|
||||
## Lookups
|
||||
|
||||
- `smallwebwaf_geojs_requests_total` and `smallwebwaf_geojs_failures_total`;
|
||||
- `smallwebwaf_geojs_unanswered_total`: the requests that needed an answer and
|
||||
went on without it;
|
||||
- with the file source,
|
||||
`smallwebwaf_lookup_database_last_read_timestamp_seconds` and
|
||||
`smallwebwaf_lookup_database_read_failures_total`.
|
||||
|
||||
## Reputation sources
|
||||
|
||||
By `source`: each list's URL, each zone, and `abuseipdb`:
|
||||
|
||||
- `smallwebwaf_reputation_hits_total`, `smallwebwaf_reputation_queries_total`
|
||||
and `smallwebwaf_reputation_failures_total`;
|
||||
- `smallwebwaf_reputation_last_fetch_timestamp_seconds`, for a list;
|
||||
- `smallwebwaf_reputation_daily_budget_remaining`, for AbuseIPDB.
|
||||
|
||||
## State files
|
||||
|
||||
By `file`:
|
||||
|
||||
- `smallwebwaf_state_file_writes_total`,
|
||||
`smallwebwaf_state_file_write_failures_total`,
|
||||
`smallwebwaf_state_file_last_write_timestamp_seconds` and
|
||||
`smallwebwaf_state_file_size_bytes`;
|
||||
- `smallwebwaf_state_file_edits_taken_in_total` and
|
||||
`smallwebwaf_state_file_edits_set_aside_total`;
|
||||
- `smallwebwaf_tracked_clients`: the clients in the table.
|
||||
|
||||
## Remote log, alerts and the process
|
||||
|
||||
- while a syslog server is set, `smallwebwaf_remote_log_lines_sent_total`,
|
||||
`smallwebwaf_remote_log_lines_dropped_total` and
|
||||
`smallwebwaf_remote_log_buffer_depth`;
|
||||
- by `destination`, `smallwebwaf_alerts_sent_total`,
|
||||
`smallwebwaf_alerts_failed_total`, `smallwebwaf_alerts_suppressed_total` and
|
||||
`smallwebwaf_alerts_dropped_total`;
|
||||
- Go's own `go_` metrics and the process's `process_` metrics.
|
||||
@@ -0,0 +1,128 @@
|
||||
# The request log's fields
|
||||
|
||||
`smallwebwaf` writes one JSON object per line on stdout for every request. The
|
||||
requirements are under "Request log" in [`SPEC.md`](../SPEC.md); this file lists
|
||||
the fields. A field that does not apply to a request is left out.
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
{"type":"request","time":"2026-10-03T12:00:00.123Z","instance":"fsn1app1/gitea","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":"*/*"},"response_content_type":"text/html; charset=utf-8","upstream_status":200,"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"duration_total":3.217,"duration_checks":0.041,"duration_upstream_connect":0.052,"duration_upstream_first_byte":2.874,"duration_upstream_total":3.104}
|
||||
```
|
||||
|
||||
## Standard web log fields
|
||||
|
||||
- `time`: when the request arrived, RFC 3339 with milliseconds, in UTC;
|
||||
- `instance`: `SWWAF_INSTANCE_NAME`;
|
||||
- `client_ip`, `method`, `scheme`, `host`, `path`, `query`, `protocol`;
|
||||
- `status`: what the client was sent, `0` if nothing;
|
||||
- `request_bytes` and `response_bytes`: body bytes each way;
|
||||
- `referer` and `user_agent`.
|
||||
|
||||
## Request detail
|
||||
|
||||
- `request_id`: the `X-Request-ID` a trusted proxy sent, or a new random one of
|
||||
26 letters and digits. It goes to the app in `X-Request-ID`.
|
||||
- `peer_ip`: the TCP peer, normally traefik.
|
||||
- `forwarded_for`: the `X-Forwarded-For` header as received.
|
||||
- `client_group`: the client as rate limiting counts it, an IPv4 address as a
|
||||
/32 or an IPv6 group.
|
||||
- `asn`, `as_name` and `country`: 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 the request went on.
|
||||
- `content_type` and `content_length`: the request's own.
|
||||
- `request_headers`: the headers listed in `SWWAF_LOG_REQUEST_HEADERS`, by name
|
||||
in lower case.
|
||||
- `has_authorization` and `has_cookie`: true when the request had those headers.
|
||||
Their values are never logged.
|
||||
- `websocket`: true when the app switched the connection to another protocol.
|
||||
|
||||
## Response detail
|
||||
|
||||
- `upstream_status`: what the app answered. It differs from `status` when
|
||||
`smallwebwaf` answered itself.
|
||||
- `response_content_type`, `cache_control` and `location`: the answer's headers.
|
||||
- `aborted`: true when the client went away early.
|
||||
|
||||
## Decision
|
||||
|
||||
- `action`: what was done:
|
||||
- `forward`: passed to the app;
|
||||
- `denied`: refused for `SWWAF_DENY_NETS`, or for a reputation source with
|
||||
the action `deny`;
|
||||
- `banned`: refused under a ban, or for a `ban` rule, a trap path or a
|
||||
CrowdSec decision, each of which bans the client;
|
||||
- `country_denied`, `rate_limited`, `rule_blocked` and `waf_blocked`:
|
||||
refused by that check;
|
||||
- `too_large` and `timed_out`: a size or time limit was passed;
|
||||
- `upstream_error`: the app could not be reached, or its answer broke off;
|
||||
- `admin`: answered at one of `smallwebwaf`'s own endpoints.
|
||||
- `would_action`: in `observe` mode, what `enforce` mode would have done with a
|
||||
request it would have refused. `action` then names what was done, normally
|
||||
`forward`.
|
||||
- `limit_percent` and `limit_percent_setting`: the client's percentage of the
|
||||
rate limits and the setting that lowered it, when one did. `bytes_percent` and
|
||||
`bytes_percent_setting` do the same for the byte limits.
|
||||
- `counts`: the client's requests in the minute, hour and day as rate limiting
|
||||
counts them, this request included, as `minute`, `hour` and `day`; its bytes
|
||||
as `minute_bytes`, `hour_bytes` and `day_bytes`. A count can have a fraction,
|
||||
since each window is counted in two buckets with the earlier one weighted.
|
||||
`counts` is left out when rate limiting did not count the request.
|
||||
- `limit_hit`: the limit broken: `minute`, `hour` or `day` for a rate limit;
|
||||
`minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit; or
|
||||
`error_burst`. A request whose bytes broke a limit was passed on whole, so its
|
||||
`action` stays `forward`.
|
||||
- `rule_ids`: the rule file rules that matched, in order, up to the one that
|
||||
refused.
|
||||
- `waf_score` and `waf_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`: the kind of offence recorded, when one was.
|
||||
- `ban_expires`: when the client's ban ends, for a request that made one or was
|
||||
refused under one: a time, or `permanent`.
|
||||
|
||||
## Timings
|
||||
|
||||
All in milliseconds:
|
||||
|
||||
- `duration_total`: from the request's headers being read to its log line;
|
||||
- `duration_checks`: to the end of the checks, before forwarding;
|
||||
- `duration_waf`: the part of that the Core Rule Set took;
|
||||
- `duration_upstream_connect`: from the hand-over to the app until there was a
|
||||
connection;
|
||||
- `duration_upstream_first_byte`: until the first byte of the answer;
|
||||
- `duration_upstream_total`: until the end of the answer.
|
||||
|
||||
## Every line
|
||||
|
||||
Every line has `type`, the standard web log fields, `request_id`, `peer_ip`,
|
||||
`client_group`, `asn`, `as_name`, `country`, `action` and `duration_total`. The
|
||||
process's own messages share the stream with `"type":"process"`; request lines
|
||||
carry `"type":"request"`.
|
||||
|
||||
## Sending the log to a syslog server
|
||||
|
||||
While `SWWAF_LOG_REMOTE_URL` is set, every line on stdout is also sent to that
|
||||
server, as the message of an RFC 5424 syslog record. Over UDP each record is one
|
||||
datagram. Over TCP and TLS each record follows its length in bytes and a space.
|
||||
stdout is unchanged. The record carries:
|
||||
|
||||
- the facility from `SWWAF_LOG_REMOTE_FACILITY`;
|
||||
- the severity informational;
|
||||
- the time;
|
||||
- the host's name;
|
||||
- the app name from `SWWAF_LOG_REMOTE_APP_NAME`.
|
||||
|
||||
Lines wait in a buffer of `SWWAF_LOG_REMOTE_BUFFER` lines, so a server that is
|
||||
slow or down never holds up a request. When the buffer is full, the oldest line
|
||||
is dropped. A failed connection is tried again a second later, and twice as long
|
||||
after each further failure, up to a minute. A line too long for one UDP datagram
|
||||
is dropped alone. As `smallwebwaf` stops, it spends at most two seconds sending
|
||||
the lines still waiting, and drops the rest. stdout has carried them.
|
||||
Reference in New Issue
Block a user