<!-- SPDX-License-Identifier: AGPL-3.0-only -->
# http-nginx

Web traffic in the **combined** log format, which nginx and Apache both
write by default:

```
203.0.113.9 - - [14/Aug/2026:11:16:32 +0200] "POST /xmlrpc.php HTTP/1.1" 403 146 "-" "Mozilla/5.0 ..."
```

`log: http`.

## Which layer to read

Behind a panel like Plesk, nginx proxies to Apache. Measured on a Plesk
host: nginx logs into `proxy_access*` only what it serves ITSELF —
static files and the fully-proxied vhosts — while everything handed to
Apache is logged by Apache alone, in `access_ssl_log` (three to a
hundred times the nginx side per vhost, and where the probes live);
port 80 is the mirror image, nginx answers and redirects itself. So
**read both**: they are disjoint except on fully-proxied vhosts, where
a doubled line can double-count a threshold rule. On a plain nginx or
Apache host, read that server's access log.

The path may be a pattern, and on a machine hosting sites it should be:

```yaml
logs:
  - {path: /var/www/vhosts/system/*/logs/proxy_access_log, log: http, ports: [80, 443]}
  - {path: /var/www/vhosts/system/*/logs/proxy_access_ssl_log, log: http, ports: [80, 443]}
  - {path: /var/www/vhosts/system/*/logs/access_log, log: http, ports: [80, 443]}
  - {path: /var/www/vhosts/system/*/logs/access_ssl_log, log: http, ports: [80, 443]}
```

## No parser reads every line

There is no base parser here. Every one is gated on a literal that
appears in a small fraction of traffic — `" 404 `, `/.`, `../`, `://`,
`xmlrpc.php` — so ordinary requests are rejected by a substring search
and never reach a regular expression. On a busy site that is the
difference between a rule set that costs nothing and one that costs a
core.

The price is that a status-based rule needs one parser per status, and a
list-based one needs its list to share a literal. That is why there is
`http-traversal-encoded` *and* `http-traversal-encoded-upper`: `%2e` and
`%2E` are different literals.

## Missing assets are cancelled, not excluded

`http-static-miss` matches the same lines as `http-404` and `http-403`
when the request was for an image, stylesheet, font or media file. Give
it a negative weight and a broken page's four dead images net to zero,
while four requests for `/wp-config.php.bak` net to four. That is how
"ignore static misses" is said here — the evaluator scores evidence, so
the exclusion is a weight rather than a condition.

## What a 403 means depends on the machine

A 404 is the site saying "no such thing". A 403 is something *deciding*
to refuse: a web application firewall, a bot-trap plugin, a rate
limiter. On the machine this pack was written from, search-engine
crawlers collect 403s all day on real article URLs. Score 403 below 404,
or keep the window short enough that a crawler's slow drip never
accumulates — a scanner produces its refusals in seconds, a crawler over
hours.

## The lists that are not here

CrowdSec ships curated lists — thousands of web-shell filenames,
hundreds of bad user agents, admin-panel paths. They are not translated
here, for a reason worth keeping: matching known-bad names is a game of
catch-up, and the probing rule catches the same scanner on its second
unknown path whatever it is called. What IS worth an instant ban is the
handful of requests that have no innocent reading at all, which is what
these parsers are.
