Skip to content

logwatch ​

The logwatch plugin tails nginx or Apache access logs and looks for attacks on WordPress sites. It catches the WP2Shell batch/v1 exploit, webshells that actually execute, and xmlrpc brute-force bursts. A periodic snapshot can't see these, because they only show up in the per-request log. Turn it on for any host that serves WordPress.

The rules run on the agent, because they need the raw lines. The server turns each finding into its own alert.

Enable it ​

Add logwatch to the plugins array and list the logs to watch.

json
{
  "plugins": ["logwatch"],
  "logwatch": {
    "refreshInterval": 60000,
    "xmlrpcBurstThreshold": 20,
    "sources": [
      { "name": "shop access", "path": "/var/log/nginx/shop.example.com.access.log" },
      { "name": "blog access", "path": "/var/log/nginx/blog.example.com.access.log", "fromStart": false }
    ]
  }
}
OptionDefaultWhat it does
refreshInterval60000Milliseconds between reads. This is also the window the xmlrpc burst count covers.
xmlrpcBurstThreshold20POST /xmlrpc.php requests from one IP, within one window, that count as a burst.
sources[]Logs to watch. Each entry takes path (required), name, and fromStart.
sources[].namefile name of pathLabel shown in alerts and on the card.
sources[].fromStartfalseRead the whole file on first sight instead of starting at the end.
path, name, fromStartnoneShorthand for a single source, used when sources is empty.

What it reports ​

One payload per source, per interval, with unique id log-<name or path>.

  • name, source, path, and window in seconds.
  • counts for the window. total parsed lines, batchV1 (exploit hits that returned 207), batchAttempts, batchBlocked (403, 406 or 429), xmlrpcPosts, webshellHits, loginSuccess, and loginRedirectsAlways.
  • findings, the list of rule hits below. Each has rule, key, severity, ip, path, title and detail.
  • error, when reading the log failed.

When it alerts ​

RuleFires whenSeverity
wp-batch-207POST /wp-json/batch/v1 (or ?rest_route=/batch/v1) returns 207error
batch-then-loginthe same IP also got a 302 from POST /wp-login.php and then a 2xx from a logged-in wp-admin pageerror
webshell-execa .php under wp-content/uploads/ returns 2xx, or a .php under a plugin directory returns 2xx with a command-style or base64 queryerror
xmlrpc-burstone IP sends xmlrpcBurstThreshold or more POST /xmlrpc.php in a windowwarning, or error at 5 times the threshold

batch-then-login replaces wp-batch-207 for that IP. Only a 207 raises the batch alert. Blocked attempts count toward batchAttempts and batchBlocked but don't alert. Webshell probes that return 403 or 404 don't alert either.

Each finding becomes its own notification, keyed on the IP or path. Two different attackers produce two alerts. There is no one-report debounce for logwatch. A finding alerts on the report that contains it.

Each window starts fresh, so a finding that stops appearing in the next window is resolved. If the same finding comes back within 30 minutes of being resolved, the server records it but does not push it or post it to Slack.

On the dashboard ​

The dashboard has no dedicated logwatch card. The service shows as a plain card with its name and status. The counts are stored but not drawn.

A source that has not reported for 300 seconds shows OFFLINE.

Requirements ​

  • Read access to each log file for the agent user.
  • The log must record the real client IP. Behind Cloudflare, set nginx real_ip with set_real_ip_from and CF-Connecting-IP. Otherwise every finding names a Cloudflare edge address.

The parser takes the last IP before the [timestamp] as the client, and a non-IP first field as the vhost. That handles the standard combined format, vhost-first formats, and formats that log both the edge IP and $http_cf_connecting_ip.

Gotchas ​

  • By default the plugin starts at the end of each file, so old attacks in the log don't alert. Set fromStart to replay history.
  • Each read is capped at the last 5 MB of new data. On a very busy log, lines beyond that in one window are skipped.
  • A file that shrinks is treated as rotated, and reading restarts at the top.
  • Some sites hide or block wp-login.php, so every login request returns a redirect. Once the plugin sees a plain GET /wp-login.php answered with a 3xx, it stops trusting login 302s for that file. loginRedirectsAlways turns true and batch-then-login can no longer fire.
  • A missing file is skipped silently each cycle, and the source reports zero lines. A read error is sent as error, but the server's logwatch rules only look at findings. Either way the source shows as online.