🌐 Українська документація | Русская документация | 📖 Configuration Cookbook
Distributed security event pipeline for any HTTP server — from a single nginx VPS to a fleet of nodes.
~12 MB RAM · single binary · zero runtime deps · extends via exec+JSON plugins in any language.
Collect events on one machine, score them on another, ban on a third — over a built-in encrypted node mesh (QUIC · TLS 1.3 · Ed25519 · TOFU). No Redis, no Kafka, no VPN required. → Distributed Processing Guide
License: ArxSentinel is distributed under the Elastic License 2.0. Free use for your own infrastructure. Commercial use as a managed security or telemetry service, or as part of a managed service, requires a separate agreement. See LICENSE for details.
Built on arx-core. ArxSentinel's pipeline engine, plugin system (Source/Sink/Detector/Processor/Executor) and NCS bridge are powered by arx-core — a generic, line-oriented telemetry framework. Engine lifecycle, runtime contract, and core plugin interfaces live in
arx-core/docs/(architecture.md,contract.md,plugin-development.md). This README documents the ArxSentinel product layer: security detectors, threat scoring, NCS wiring, and Cloudflare/MikroTik/OpenWrt/OPNsense/nginx executors. See Architecture for the split.
╔══════════════════════════════════════════════════════════════════╗
║ SOURCES ║
║ nginx · Apache · Caddy · Traefik · HAProxy · LiteSpeed ║
║ file │ stdin │ syslog │ http (push/pull · Firehose/Loki/OTLP) ║
║ exec+JSON plugin (any language) ║
╚═══════════════════════════╤══════════════════════════════════════╝
│ parsed log entries (Direct Go Channels)
╔═══════════════════════════╧══════════════════════════════════════╗
║ PROCESSORS ║
║ ║
║ Whitelist ── custom IPs/CIDRs/UA · bot DNS verification ║
║ ChainGuard ─ proxy-chain IP integrity check ║
║ WAF rule-engine (processors: · pass / drop / tag) ║
║ ║
║ Detectors (core) Detectors (plugins) ║
║ ├─ probe score 25 └─ exec+JSON detector (any language) ║
║ ├─ bruteforce score 30 ║
║ ├─ crawler score 20 ║
║ ├─ noasset score 20 ║
║ ├─ rate score 25 ║
║ ├─ useragent score 40/20/15 ║
║ ├─ overflow score 30 ║
║ └─ badbot score 60 ║
║ ║
║ Scorer ── accumulates score → WARN (≥50) │ THREAT (≥80) ║
╚═══════════════════════════╤══════════════════════════════════════╝
│ threat events
╔═══════════════════════════╧══════════════════════════════════════╗
║ SINKS (Passive Logging) ║
║ file (fail2ban format) · stdout JSON · exec+JSON plugin ║
║ Grafana Loki · Splunk HEC · Datadog Logs API (SIEM forwarding) ║
╚═══════════════════════════╤══════════════════════════════════════╝
│ sentinel-threat sink → AttachWriter()
╔═══════════════════════════╧══════════════════════════════════════╗
║ NAMED CHANNEL SWITCH (Point-to-Point Work Queue) ║
║ memory │ bbolt (file) │ redis │ transport (QUIC node mesh) ║
╚═══════════════════════════╤══════════════════════════════════════╝
│ ncs://<channel-name> → AttachReader()
╔═══════════════════════════╧═══════════════════════════════════════════════════════════════════════╗
║ EXECUTORS (Stateful Active Response — optional) ║
║ Cloudflare IP Lists · MikroTik address-list · nginx blocklist · OpenWrt ipset · OPNsense alias ║
╚═══════════════════════════════════════════════════════════════════════════════════════════════════╝
ArxSentinel scales from a classic bare-metal VPS to a distributed Kubernetes cluster — each scenario below is a self-contained starting point.
One config file, no profile needed. Works out of the box:
streams:
- name: main
inputs:
- type: file
path: /var/log/nginx/access.log
parser: combined
outputs:
- type: file
path: /var/log/arxsentinel/threats.log
format: fail2banMount the nginx log volume; ArxSentinel reads it as a sidecar. See deploy/examples/docker/:
# docker-compose.yml — excerpt
services:
arxsentinel:
image: ghcr.io/mr-addams/arxsentinel:latest
volumes:
- nginx_logs:/var/log/nginx:ro
environment:
ARXSENTINEL_LOG_FILE: /var/log/nginx/access.logOne pod per node, reads host logs via hostPath. See deploy/examples/kubernetes/ and the Helm README:
helm install arxsentinel ./deploy/container/k8s/arxsentinel \
--set logVolume.hostPath=/var/log/nginxWatch multiple servers in one process — full IP-state isolation per stream:
streams:
- name: frontend
log_file: /var/log/nginx/access.log
- name: api
log_file: /var/log/apache2/api.log
profile: apacheWhen the logs live on different machines, don't mount or ship them — run a 12 MB collector next to each log and forward events over the built-in encrypted mesh instead (see use case 8).
API gateway, custom app log, any text format — supply a regex with named capture groups:
parser:
log_format: "custom"
custom_regex: '(?P<ip>\S+) \S+ \S+ \[.*?\] "\S+ (?P<path>\S+) \S+" (?P<status>\d+) (?P<size>\d+) "(?P<ua>[^"]*)"'Any script or binary as an additional detector; called per request over stdin/stdout in any language. See Plugin Development:
detectors:
plugins:
- name: ml-classifier
exec: /opt/plugins/classify.py
score: 45Route threats to any destination — SIEM, webhook, Telegram, custom script:
sinks:
- type: exec
exec: /opt/plugins/send-to-siem.shSend scored threat events straight into your log platform as a first-class sink — useful when you already operate a SIEM and want ArxSentinel as a feed alongside (or instead of) Fail2Ban/executor responders. JSON envelope is recommended for log-platform readability:
sinks:
- type: loki
format: json
loki_url: https://loki.example.com:3100
loki_labels:
job: arxsentinelThe same shape works for type: splunk (HEC JSON endpoint — needs
splunk_url + splunk_token) and type: datadog (Logs API v2 — needs
datadog_url with region, e.g. https://http-intake.logs.datadoghq.com,
plus datadog_api_key). TLS, mTLS, batching, gzip and multi-tenant fields
are available per sink — see docs/providers/observability/.
Quick-start recipes: cookbook/observability/.
The same binary becomes a collector, detector, or responder by config alone, connected over a built-in mutually-authenticated QUIC mesh — no message broker, no log shipper, no VPN:
Pi / VPS / NAS collectors detection box enforcement
┌────────────┐
│ nginx logs │──┐ "edge-raw" ┌───────────────┐ "scored" ┌───────────────────────────────────────┐
└────────────┘ ├──────────────▶│ 8 detectors · │──────────▶│ MikroTik · OpenWrt · OPNsense · nginx │
┌────────────┐ │ QUIC/TLS 1.3 │ WAF · scoring │ │ CF WAF · SIEM │
│ API logs │──┘ Ed25519+TOFU └───────────────┘ └───────────────────────────────────────┘
└────────────┘
# collector node — parse only, forward unscored (12 MB, runs on a Pi)
streams:
- pipelines:
- raw_forward: true
inputs: [{type: file, path: /var/log/nginx/access.log}]
outputs: [{type: sentinel-threat, name: edge-raw, format: raw-line,
queue: {type: transport, mode: send, peer: "brain:4097"}}]An attacker probing several of your services accumulates one combined score on the detector node — and gets banned everywhere at once. Full operator guide with 5 ready topologies (homelab → enterprise SIEM feed): docs/DISTRIBUTED.md · recipes: cookbook/distributed-ncs/ — every topology CI-tested with real containers.
Installs the package, enables the systemd service, and works with nginx immediately:
curl -fsSL https://raw.githubusercontent.com/mr-addams/arxsentinel/main/scripts/get.sh | sudo bashEdit config for your server, then reload without restart:
sudo nano /etc/arxsentinel/config.yaml
sudo systemctl kill -s HUP arxsentinelFor Docker, Kubernetes, and other installation methods — see Installation below.
Auto-detects your distro and architecture, downloads the correct package from GitHub Releases, installs it with your package manager, enables and starts the service:
curl -fsSL https://raw.githubusercontent.com/mr-addams/arxsentinel/main/scripts/get.sh | sudo bashWorks on Debian, Ubuntu, Fedora, RHEL, AlmaLinux, Rocky Linux, and Arch Linux.
Requires curl and sudo. Fail2Ban is installed automatically if missing (optional — see Executors for API-based alternatives).
The service starts immediately and works with nginx out of the box — no profile needed. Edit the config to switch to another server (apache, caddy, traefik, haproxy-http, litespeed, or a custom regex):
sudo nano /etc/arxsentinel/config.yaml
sudo systemctl kill -s HUP arxsentinel # reload without restartDownload the .deb package for your architecture from the Releases page and install it:
# amd64
sudo apt install ./arxsentinel_<version>_linux_amd64.deb
# arm64
sudo apt install ./arxsentinel_<version>_linux_arm64.deb
# arm/v7
sudo apt install ./arxsentinel_<version>_linux_armv7.deb
# riscv64
sudo apt install ./arxsentinel_<version>_linux_riscv64.deb
# i386
sudo apt install ./arxsentinel_<version>_linux_386.debapt install automatically resolves dependencies, installs the systemd unit, Fail2Ban filter/jail (optional — for bare-metal setups), logrotate config, and creates the arxsentinel system user.
After installation, edit the config and start the service:
sudo nano /etc/arxsentinel/config.yaml
sudo systemctl enable --now arxsentinelDownload the .rpm package for your architecture from the Releases page and install it:
# amd64
sudo dnf install ./arxsentinel_<version>_linux_amd64.rpm
# arm64
sudo dnf install ./arxsentinel_<version>_linux_arm64.rpm
# arm/v7
sudo dnf install ./arxsentinel_<version>_linux_armv7.rpm
# riscv64
sudo dnf install ./arxsentinel_<version>_linux_riscv64.rpm
# i386
sudo dnf install ./arxsentinel_<version>_linux_386.rpmdnf install resolves dependencies, installs the systemd unit to /usr/lib/systemd/system/, Fail2Ban filter/jail, logrotate config, and creates the arxsentinel system user.
After installation, edit the config and start the service:
sudo nano /etc/arxsentinel/config.yaml
sudo systemctl enable --now arxsentinelRHEL 8 / CentOS Stream 8: use
dnforrpm -idirectly. Fail2Ban may require the EPEL repository:sudo dnf install epel-release && sudo dnf install fail2ban
Download the .pkg.tar.zst package for your architecture from the Releases page and install it:
# amd64
sudo pacman -U arxsentinel_<version>_linux_amd64.pkg.tar.zst
# arm64
sudo pacman -U arxsentinel_<version>_linux_arm64.pkg.tar.zst
# arm/v7
sudo pacman -U arxsentinel_<version>_linux_armv7.pkg.tar.zst
# riscv64
sudo pacman -U arxsentinel_<version>_linux_riscv64.pkg.tar.zst
# i386
sudo pacman -U arxsentinel_<version>_linux_386.pkg.tar.zstThe package installs the systemd unit to /usr/lib/systemd/system/, Fail2Ban config files, logrotate config, and creates the arxsentinel system user.
After installation, edit the config and start the service:
sudo nano /etc/arxsentinel/config.yaml
sudo systemctl enable --now arxsentinelFail2Ban on Arch: install it with
sudo pacman -S fail2banbefore or after installing arxsentinel.
Not covered by the get.sh quick-install above (that script targets
Linux /etc/os-release distros only). Download the freebsd_<arch>
archive from the Releases
page and run the bundled installer:
fetch https://github.com/mr-addams/arxsentinel/releases/latest/download/arxsentinel_<version>_freebsd_<arch>.tar.gz
tar xzf arxsentinel_<version>_freebsd_<arch>.tar.gz
cd arxsentinel_<version>_freebsd_<arch>
sudo sh install.shinstall.sh creates the arxsentinel system user, installs the binary
and an rc.d service script, and seeds a config from the bundled example
(never overwrites an existing config on re-run). Enable and start:
sysrc arxsentinel_enable=YES
service arxsentinel startFull guide — FreeBSD path layout, rc.d service management, and running
your web server under podman on FreeBSD (storage driver, firewall
setup, container networking gotchas): FreeBSD Deployment Cookbook.
Requires Go 1.26+:
git clone https://github.com/mr-addams/arxsentinel
cd arxsentinel
sudo ./scripts/install.sh
sudo systemctl enable --now arxsentinelFor minimal / IoT edge / custom builds, see Build Profiles
(go build -tags "arx_tag minimal" ./... etc.).
Distroless image (~12 MB), runs as non-root uid 65532, exposes Prometheus metrics on :9117.
docker run -d \
-v /var/log/nginx/access.log:/var/log/nginx/access.log:ro \
-v /var/log/arxsentinel:/var/log/arxsentinel \
-p 127.0.0.1:9117:9117 \
ghcr.io/mr-addams/arxsentinel:latestSee README.docker.md for Docker Compose setup, volume mounts, env var overrides, and Fail2Ban integration.
DaemonSet topology — one pod per node, reads the node's access log via hostPath.
helm install arxsentinel ./deploy/container/k8s/arxsentinel \
--set logVolume.hostPath=/var/log/nginx \
--set threatLog.hostPath=/var/log/arxsentinelSee Kubernetes README for values reference, Prometheus Operator integration, and cloud deployment notes.
| Server | Profile | Setup required |
|---|---|---|
| nginx | (default — no profile needed) | None — nginx combined log format works out of the box |
| Apache | apache |
None — default CLF format |
| Traefik | traefik |
Add fields.headers.names.User-Agent/Referer: keep to accessLog — see deploy/examples/traefik/ |
| LiteSpeed / OpenLiteSpeed | litespeed |
None — default CLF format |
| Caddy | caddy |
xcaddy + transform-encoder plugin — see deploy/examples/caddy/ |
| HAProxy | haproxy-http |
http-request capture + custom log-format with UA — see deploy/examples/haproxy/ |
Each release includes a Tested product versions table with the exact server versions the build was validated against — see GitHub Releases.
nginx: no
profile:setting is needed. The default CombinedParser handles nginx combined log format out of the box. Set onlygeneral.log_filepointing to your access log.
Built-in profiles — no regex or field mapping required. Set parser.profile to the server name for Apache, Traefik, Caddy, HAProxy, LiteSpeed, or OpenLiteSpeed:
Example — Apache:
parser:
profile: "apache"
general:
log_file: /var/log/apache2/access.log
output:
threat_log: /var/log/arxsentinel/threats.logReady-made configs for each server are in deploy/examples/:
deploy/examples/
├── apache/ httpd.conf + sentinel-config.yaml
├── caddy/ Caddyfile + sentinel-config.yaml
├── traefik/ traefik.yml + sentinel-config.yaml
├── haproxy/ haproxy.cfg + sentinel-config.yaml
└── litespeed/ httpd_config.conf + sentinel-config.yaml
Note — LiteSpeed / OpenLiteSpeed: Both LSWS and OLS emit Apache CLF by default — no server-side changes required. Log path:
/usr/local/lsws/logs/access.log(server-wide) or/usr/local/lsws/logs/<vhostname>/access.log(per virtual host). Behind a reverse proxy: enable "Use Client IP in Header" in WebAdmin so%hlogs the real client IP. Seedeploy/examples/litespeed/for the full config.
Note — Caddy: Caddy v2's built-in JSON encoder outputs nested objects. The
caddyprofile requires the caddy-transform-encoder plugin to produce CLF output. Seedeploy/examples/caddy/Caddyfilefor the setup.
- 8 detectors: probe scanning, rate anomaly, suspicious User-Agent, bruteforce (404 ratio), sequential crawler, no-asset bot, URL overflow / WAF bypass, community bad-bot blocklist
- Chain Guard: detects Cloudflare/CDN edge IPs and bogon/RFC 1918/CGNAT addresses appearing as client IPs — signals a misconfigured proxy chain before ArxSentinel's detectors go blind
- Bot DNS verification: Googlebot, Bingbot, Yandex, DuckDuckGo and others are verified via rDNS/fDNS — legitimate crawlers are never banned
- Multi-stream + Multi-pipeline: watch multiple log files in one process; within each stream, define independent pipelines with their own detectors, sources, sinks and IP-state tracker (or share state via
tracker_group) - Observability sinks: forward threat events to Grafana Loki, Splunk HEC, or Datadog Logs API — SIEM forwarding as an alternative (or addition) to Fail2Ban/executor responders
- Distributed pipeline (Distributed NCS): span the pipeline across machines over a built-in encrypted node mesh (QUIC · TLS 1.3 · Ed25519 identity · TOFU pinning) — forward raw parsed entries to a central detector, or scored verdicts to remote responders; no broker, no log shipper, no VPN. See docs/DISTRIBUTED.md
- Whitelist: IPs, CIDRs, UA substrings — configurable exclusion lists
- Linear score decay: points decay over
observation_window, no false bans from old traffic - Prometheus metrics:
/metricson configurable port (default:9117), optional bcrypt basic auth; Grafana dashboard included - Health endpoint:
/healthalways returns200 {"status":"ok"}— no credentials required; ready for DockerHEALTHCHECK, k8s probes, and load balancers - JSON log format: switch to JSON log parsing via
parser.log_format: "json"— no recompilation needed - SIGHUP reload: config, scorer, parser and whitelist are rebuilt without restarting the daemon
- Graceful shutdown: line buffer is drained on SIGTERM
- Systemd + logrotate + Fail2Ban: ready-to-use deploy configs included
- Linux amd64 / arm64 / arm/v7 / riscv64 / i386 with systemd, or FreeBSD 386 / amd64 / arm / arm64 with rc.d
- Fail2Ban (optional — recommended for bare-metal; not needed with Cloudflare executor or other API-based integrations)
- An HTTP server writing access logs in a supported format (nginx, Apache, Caddy, Traefik, HAProxy, LiteSpeed, OpenLiteSpeed — or custom regex)
Config file: /etc/arxsentinel/config.yaml (created from config.yaml during installation).
Override path: ARXSENTINEL_CONFIG=/path/to/config.yaml.
Key parameters:
general:
log_file: /var/log/nginx/access.log # log file to watch (nginx example; see also: streams:)
stats_interval: 300s # STATS output interval to operational log
parser:
# profile: "apache" # set for non-nginx servers: apache | caddy | traefik | haproxy-http | litespeed
# # nginx combined log format works without any profile setting
scoring:
alert_threshold: 50 # score → WARN in threat log
ban_threshold: 80 # score → THREAT + Fail2Ban ban
observation_window: 300s # score accumulation / decay window
detectors:
probe:
enabled: true
score: 25
paths: [/.env, /.git/config, /wp-config.php, ...] # probe path list
rate:
enabled: true
threshold: 100 # requests per window
window: 60s
score: 25
useragent:
enabled: true
scanner_score: 40 # Nuclei, sqlmap, Nikto
grabber_score: 20 # wget, HTTrack
automation_score: 15 # python-requests, aiohttp
empty_ua_score: 30
bruteforce:
enabled: true
min_requests: 10
ratio_threshold: 0.6 # >60% of responses are 404
score: 30
crawler:
enabled: true
min_sequential: 5 # /page/1, /page/2, ... N in a row
score: 20
noasset:
enabled: true
min_page_requests: 3
asset_ratio_threshold: 0.1 # <10% of requests go to static assets
score: 20
overflow:
enabled: true
max_url_length: 2048
suspicious_params: [bypass, shell, cmd, exec, eval]
score: 30
badbot:
enabled: true
score: 60
check_ua: true
check_referrer: false # opt-in: also match the Referer header (~7108 referrer patterns)
blocklist:
storage: "" # "" = in-memory; file path = bbolt (survives restarts)
lists:
- name: badbot-ua
refresh_interval: 24h
sources:
- url: "https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master/_generator_lists/bad-user-agents.list"
format: plain_text
- name: badbot-ref
refresh_interval: 24h
sources:
- url: "https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master/_generator_lists/bad-referrer-words.list"
format: plain_text
whitelist:
fake_bot_score: 35 # penalty for a bot UA that fails DNS verification
dns_verify_timeout: 2s # DNS verification timeout per pipeline request
custom:
ips: [127.0.0.1]
cidrs: [10.0.0.0/8]
ua_substrings: [internal-monitor]
output:
threat_log: /var/log/arxsentinel/threats.log
operational_log: /var/log/arxsentinel/sentinel.logyaml.v3 limitation: if a section is present in config.yaml (e.g.,
scoring:), it must include all fields — any omitted fields will be zeroed out. Sections missing from the file entirely will use Go defaults.
| Detector | Trigger | Default score |
|---|---|---|
| probe | request to .env, .git, wp-config.php, etc. | 25 per request |
| rate | >100 requests per 60s | 25 |
| useragent | scanner / grabber / automation / empty UA | 15–40 |
| bruteforce | >60% of responses are 404 with ≥10 requests | 30 |
| crawler | ≥5 sequential numeric URLs (/page/1..N) | 20 |
| noasset | <10% requests to static assets with ≥3 page requests | 20 |
| overflow | URL >2048 chars or WAF bypass keywords | 30 |
| badbot | UA (or Referer) matches community blocklist (~685 patterns) | 60 |
Score accumulates with linear decay over observation_window. Reaching alert_threshold writes a WARN; reaching ban_threshold writes a THREAT and triggers Fail2Ban.
Processors run before the detector chain. They short-circuit the pipeline (skip), drop the event outright, or tag it for downstream scoring.
| Plugin | Description |
|---|---|
whitelist |
IP/CIDR/path/UA allowlist — short-circuits pipeline |
chaincheck |
Validates reverse-proxy IP chain integrity |
waf |
Rule-engine gate — pass / drop / tag with scorer integration |
Covered by the package installers above. Use systemctl to manage the service and kill -HUP for live reloads. See Management for the full command reference.
Covered by the FreeBSD installer above. Use service arxsentinel <start|stop|status> to manage the service; sysrc arxsentinel_enable=YES persists it across reboots. Full guide, including running your web server under podman on FreeBSD: FreeBSD Deployment Cookbook.
ArxSentinel runs as a sidecar alongside your HTTP server, reading shared log volumes.
Ready-to-use Compose file and config: deploy/examples/docker/.
Full Docker guide: README.docker.md.
DaemonSet (one pod per node, reads host logs) or sidecar (reads from an emptyDir shared with the app container).
Ready-to-use manifests: deploy/examples/kubernetes/.
Helm chart with values reference: Kubernetes README.
Executors are stateful action plugins that run after threat scoring. Unlike Sinks (passive log writers), Executors actively manage external resources: they maintain a local dedup map, apply TTL-based expiry, and track execution statistics.
Terminology note: "Executor" is the plugin/config term (
executors:in YAML,pkg/executorplugins/*in code) — used throughout this README and the codebase. In a Distributed NCS topology, the node that runs executors is called a Responder (paired with Collector and Detector as the three node roles) — same plugins, different vocabulary for describing where they run in a multi-machine deployment.
| Executor | Package | Description |
|---|---|---|
| cloudflare | pkg/executor/cloudflare |
Adds threat IPs to a Cloudflare IP List; auto-removes expired entries via TTL sweep |
| nginx | pkg/executor/nginx |
Writes banned IPs to a plain blocklist file (TTL auto-expiry, atomic writes, optional reload command); you include the file into nginx however suits your setup |
| mikrotik | pkg/executor/mikrotik |
Manages a RouterOS v7 firewall address-list over the REST API; TTL-based auto-unban, removes only arxsentinel-owned entries, CHR/ARM compatible |
| openwrt | pkg/executor/openwrt |
Manages an nftables ipset via a router's ubus (uhttpd-mod-ubus) endpoint using the standard uci/rc rpcd objects; batched UCI edit + single reload per cycle, TTL owned by the plugin (not nftables-native) |
| opnsense | pkg/executor/opnsense |
Manages a firewall alias over the OPNsense REST API (alias_util add/delete/list); independent point add/delete per event (no batching — the API applies immediately), TTL owned by the plugin via active sweep |
⚠️ NCS routing: Point-to-Point, not Broadcast. Executors receive events through the Named Channel Switch (NCS) — a Work Queue, not a pub/sub bus. Connecting two executors to the samencs://threatschannel results in round-robin distribution (each executor sees ~half the events). Use one dedicated output channel per executor:outputs: - type: sentinel-threat name: cf-threats # Cloudflare executor reads this - type: sentinel-threat name: mtk-threats # MikroTik executor reads this - type: sentinel-threat name: owrt-threats # OpenWrt executor reads this - type: sentinel-threat name: opns-threats # OPNsense executor reads this
See docs/executors.md for the framework overview and how to add custom executors. See docs/executor-cloudflare.md for Cloudflare-specific configuration and troubleshooting. See docs/executor-nginx.md for the nginx blocklist executor.
- FreeBSD support — native
386/amd64/arm/arm64builds, dedicated installer + rc.d service, CI-validated against all 6 supported web servers (nginx, Caddy, Traefik, HAProxy, Apache, LiteSpeed) including proxy-chain real-IP scenarios — see FreeBSD Deployment Cookbook arxsentinel validate— offline, topology-aware config validation using static plugin manifests; catches broken pipeline wiring before deploy- Pluggable queue backends — buffer executor events via in-memory, bbolt (file) or Redis queue; selectable per executor for bare-metal / single-host / multi-replica K8s
- Named Channel Switch — route threat events between independent pipelines by name (one pipeline detects, another enforces)
- Bot fast path —
verify_method: ua_only(User-Agent match, no DNS) and per-botexempt_detectorsto skip specific detectors for trusted crawlers - CLI —
arxsentinel cleanup --cf --dry-runto preview/clean stale executor entries
Source, Sink, and Detector plugins communicate with ArxSentinel over stdin/stdout JSON — write them in any language. The plugin receives a JSON object per log entry (or event) and returns a JSON response. ArxSentinel manages the subprocess lifecycle.
Full protocol spec and examples: docs/PLUGIN_DEV.md.
ArxSentinel provides automatic bot verification (search engines) and custom exclusion lists (IPs, CIDRs, User-Agent substrings). Whitelisted requests skip all detectors entirely.
See README.whitelist.md for configuration details and examples.
[Source: file] ─┐ FileSource (inotify, logrotate-aware)
[Source: stdin] ─┼──→ Merge() ──→ entries chan (*LogEntry)
[Source: http] ─┘ (Phase 2+)
│
whitelist.Matcher ──→ custom IP/CIDR/UA? → skip
│
chaincheck.Checker ──→ Cloudflare/bogon IP? → warnings.log
│
whitelist.Verifier ──→ bot UA? → rDNS/fDNS → verified? → skip
│ → fake bot? → +FakeBotScore
waf.RuleEngine ──→ signature match? → pass / drop / tag
│
tracker.Update(*IPState)
├── TotalRequests, Requests404
├── pathBuf (ring buffer, last 64 paths)
└── sliding window rate counters
│
scorer.Evaluate(ipState, entry)
├── decay accumulated score
├── run 8 detectors
└── determine verdict (score → level)
│
[Sink: Fail2Ban file] ──→ threats.log ──→ Fail2Ban ──→ iptables ban
[Sink: stdout JSON] ──→ log aggregator (Loki, Splunk, Datadog)
[Sink: sentinel-threat] ─┐
│ sentinel-threat sink → AttachWriter()
╔═════════════╧═══════════════════╗
║ Named Channel Switch ║
║ (ncs://threats · Work Queue) ║
╚═════════════╤═══════════════════╝
│ AttachReader() · Point-to-Point
╔═════════════╧═══════════════════╗
║ EXECUTORS (Stateful) ║
║ ├─ Cloudflare IP Lists API ║
║ ├─ MikroTik REST address-list ║
║ ├─ OpenWrt UCI ipset (ubus) ║
║ ├─ OPNsense REST alias ║
║ └─ nginx blocklist file ║
╚═════════════════════════════════╝
(dedup map · TTL expiry · auto-unban)
│
sentinel.log (operational)
The default configuration (Fail2Ban file sink) is fully backward compatible — existing
general.log_file and output.threat_log settings work unchanged.
Background goroutines:
- FileSource — file watching via fsnotify, handles mv/copytruncate logrotate
- GC — removes inactive IPs every
gc_interval(default 60s) - Stats — prints
STATS processed/tracked/threats/suspiciouseverystats_interval - SIGHUP listener — converts the signal into a channel event for the main loop
Full component hierarchy and data-flow diagrams: docs/ARCHITECTURE.md.
Run one sentinel process that watches multiple log files simultaneously — one pipeline per domain, full isolation.
streams:
- name: site1
log_file: /var/log/nginx/site1.access.log
threat_log: /var/log/arxsentinel/site1.threats.log
- name: site2
log_file: /var/log/apache2/site2.access.log
threat_log: /var/log/arxsentinel/site2.threats.log
profile: apacheNote:
streams:andgeneral.log_fileare mutually exclusive. Use one or the other.
Each stream gets its own tracker, scorer, whitelist state, and threat log. A crash or slow scan on one stream does not affect others.
The classic single-file config (general.log_file) keeps working — it is silently converted to a single unnamed stream (stream="" label on metrics). No config migration needed.
Within a single stream, define independent pipelines — each with its own Sources, Detectors, Sinks and IP-state tracker. Use tracker_group to share IP state between pipelines that watch related traffic.
streams:
- name: nginx-monitoring
pipelines:
- name: api-scanner
tracker_group: web # pipelines with the same group share IP state
inputs:
- type: file
path: /var/log/nginx/api.log
processors: # rule-engine plugins evaluated in array order
- plugin: waf # signature gate: drops SQLi/scanner before detectors
params:
waf_config:
rules:
- name: sqli_drop
expression: 'http.path contains "OR 1=1"'
action: drop
detectors:
probe:
enabled: true
rate:
enabled: true
threshold: 100
outputs:
- type: file
path: /var/log/arxsentinel/api-threats.log
- name: admin-watcher
tracker_group: web # shares IP state with api-scanner
inputs:
- type: file
path: /var/log/nginx/admin.log
detectors:
bruteforce:
enabled: true
badbot:
enabled: true
outputs:
- type: file
path: /var/log/arxsentinel/admin-threats.log
# see cookbook/rule-engine/ for full examplesTrackerGroup rules:
tracker_group: web— pipelines with the same group share one*state.Tracker; an attacker scored inapi-scanneris also tracked byadmin-watchertracker_group: ""(or omitted) — isolated; the pipeline'snameis used as the implicit group key- Legacy configs (no
pipelines:key) — auto-wrapped into one unnamed pipeline; behaviour is identical to previous versions
Prometheus metrics gain a pipeline label on all vectors (e.g. arx_sentinel_lines_processed_total{stream="nginx-monitoring", pipeline="api-scanner"}). Legacy pipelines use pipeline="" so existing Grafana dashboards work without changes.
Each stream writes its own threat_log file. Create one Fail2Ban jail per file:
# /etc/fail2ban/jail.d/arxsentinel-site1.conf
[arxsentinel-site1]
enabled = true
filter = arxsentinel
logpath = /var/log/arxsentinel/site1.threats.log
maxretry = 1
bantime = 86400
[arxsentinel-site2]
enabled = true
filter = arxsentinel
logpath = /var/log/arxsentinel/site2.threats.log
maxretry = 1
bantime = 86400The dashboard includes a Stream variable. Select one or multiple streams to filter all panels. Import deploy/grafana/arxsentinel-dashboard.json (v2).
Operational log (/var/log/arxsentinel/sentinel.log) — daemon's working log:
2026-04-02 14:33:10 [STARTUP] arxsentinel v1.0.0 started
2026-04-02 14:33:12 [THREAT] 45.134.26.8 score=85 modules=probe,rate reason="..."
2026-04-02 14:38:10 [STATS] processed=14320 tracked=87 threats=3 suspicious=12
Tags: STARTUP, SHUTDOWN, CONFIG, THREAT, WHITELIST, STATS, GC, ERROR, WARN.
Debug tags (PARSER, TAIL, DETECTOR, SCORER) are visible only when logging.debug: true.
Threat log (/var/log/arxsentinel/threats.log) — read by Fail2Ban:
2026-04-02T14:33:12Z THREAT 45.134.26.8 score=85 modules=probe,rate reason="probe:/.env,rate:142rps"
2026-04-02T14:35:01Z WARN 92.63.104.12 score=55 modules=useragent reason="ua:Nuclei/3.1.0"
Fail2Ban failregex: THREAT <HOST> score=\d+ (file deploy/fail2ban/filter.d/arxsentinel.conf).
Warnings log (chain_guard.warnings_log) — infrastructure misconfiguration alerts:
2026-05-20T12:34:56Z CHAIN_WARN cloudflare-ip-as-client ip=172.64.0.1 cidr=172.64.0.0/13 log=/var/log/nginx/access.log
2026-05-20T12:34:57Z CHAIN_WARN bogon-ip-as-client ip=10.0.0.1 cidr=10.0.0.0/8 log=/var/log/nginx/access.log
Warnings are distinct from threats: CHAIN_WARN means ArxSentinel cannot reliably identify
the real attacker IP. Fix the underlying infrastructure issue (see Chain Guard)
and the warnings will stop.
# Status and logs
systemctl status arxsentinel
journalctl -u arxsentinel -f
# Reload config without restart (SIGHUP)
kill -HUP $(cat /var/run/arxsentinel.pid)
# or
systemctl kill -s HUP arxsentinel
# Stop (graceful — drains the line buffer)
systemctl stop arxsentinel
# Manual ban/unban via Fail2Ban
fail2ban-client status arxsentinel
fail2ban-client set arxsentinel unbanip 1.2.3.4What is updated on SIGHUP: scorer (detectors + thresholds), whitelist matcher, debug/color flags, log file paths.
What is NOT updated: tracker (IP state), DNS cache, TailReader (access.log path requires a restart).
ArxSentinel supports three log format modes: combined (default nginx), JSON (no recompilation needed), and custom regex for arbitrary text formats.
See README.log-formats.md for full configuration examples, field mappings, and common mistakes.
Full guide for deployment behind a reverse proxy (HAProxy, Traefik, Caddy, nginx), including IP extraction configuration and Chain Guard (broken IP chain detection).
See deploy/examples/reverse-proxy/README.md.
Ready-made probe.paths overrides for the most common PHP stacks are in
deploy/examples/cms/. Copy the relevant paths into your config.yaml:
| File | Target |
|---|---|
wordpress.yaml |
WordPress — wp-login.php, xmlrpc.php, REST user enumeration |
laravel.yaml |
Laravel — .env, /storage/, /vendor/, Telescope, Horizon |
drupal.yaml |
Drupal — /user/login, settings.php, update.php |
joomla.yaml |
Joomla — /administrator/, configuration.php |
generic-php.yaml |
Custom PHP apps — phpinfo, phpMyAdmin, Adminer, backup files |
How to apply a CMS config:
- Open
deploy/examples/cms/<cms>.yamland copy thepaths:list. - Paste it into your
config.yamlunderdetectors.probe.paths:. - Reload without restart:
kill -HUP $(pgrep arxsentinel)— orsystemctl kill -s HUP arxsentinel.
The paths extend (not replace) the built-in sensitive-path list by default.
To use only your custom list, set detectors.probe.paths: to exactly the paths you want.
Enable metrics in config.yaml, configure Prometheus scraping, set up bcrypt password hashing, and import the Grafana dashboard.
Full guide: deploy/grafana/README.md
In active development for v2.x:
- AWS WAF executor — IP set updates for AWS WAF rule groups
- SSH source + detectors — parse
sshdauth logs (syslog/journald) and score brute-force/credential-stuffing patterns with dedicated detectors, reusing the same scoring/executor pipeline as HTTP - Alert sinks — push threats to Telegram, Slack and PagerDuty with deduplication and rate-limiting
Daemon fails to start — threat log error:
Check permissions on /var/log/arxsentinel/ — the directory must be owned by the arxsentinel user.
Fail2Ban is not banning — check log format:
fail2ban-regex /var/log/arxsentinel/threats.log /etc/fail2ban/filter.d/arxsentinel.confToo many false WARNs — reduce sensitivity:
Lower the score or raise thresholds (threshold, ratio_threshold) in the config, then kill -HUP.
Debug pipeline — enable debug mode:
logging:
debug: trueRestart or kill -HUP. The operational log will show [PARSER], [DETECTOR], [SCORER] lines for every request.
High memory usage:
Reduce state.max_tracked_ips (default 100000; each IP ≈ 2.5 KB → 100k ≈ 250 MB).
The badbot detector fetches its blocklists from nginx-ultimate-bad-bot-blocker, an outstanding community project created and maintained by Mitchell Krog (@mitchellkrogza) and its contributors. The project curates ~685 bad User-Agent patterns and ~7108 bad referrer words, updated almost daily — an enormous effort that benefits the entire web.
Licensed under MIT. The lists are downloaded at runtime and are not bundled with ArxSentinel.
Heartfelt thanks to Mitchell Krog and every contributor to that project — your dedication makes the web a safer place for everyone.