A lightweight, fast alternative to fail2ban written in Go.
Scanban monitors system logs for patterns that indicate malicious activity (brute force attempts, exploit scanning, etc.), extracts offending IP addresses and automatically bans them for a configurable period of time. It works by tailing log files and Docker container logs in real-time, matching lines against regular expression patterns, and executing custom actions (fully configurable, typically iptables or ipset commands) to block attackers.
With the correct config you can block attackers during recon, before an attack even takes place by assuming that if they are scanning for one exploit, they may scan for others.
- Easy configuration - Simple TOML format with sensible defaults
- Regular expression based matching - Flexible pattern matching for any log format
- Multiple log sources - Tail files and Docker container logs simultaneously
- Time-based banning - Automatically ban and unban IPs after a specified duration
- Customizable actions - Execute any shell command for ban/unban operations
- Threshold support - Require multiple offenses before banning
- Whitelisting - Never ban specific IPs or subnets
- Dry run mode - Test your configuration without taking any action
- Drop-in configuration - Modular config files in
/etc/scanban.d/ - Works great with ipset - Efficient IP blocking for high-volume scenarios
- Security hardened - Built-in protection against log injection attacks and rate limiting to prevent resource exhaustion
Before running scanban as a daemon, test your configuration against existing logs:
# Dry run against a specific log file
scanban -n -f /var/log/auth.log -c /etc/scanban.toml
# Scan entire file (not just new lines) with verbose output
scanban -n -a -v -f /var/log/auth.log
# Test from stdin
cat /var/log/auth.log | scanban -n -f -The dry run mode (-n) shows what would be banned without actually executing any actions.
If you installed scanban from the Debian package:
-
Configure scanban - Edit
/etc/scanban.tomlto define your rules and actions (see Configuration section below) -
Set secure permissions - The config file contains shell commands that will be executed so ensure only root has access:
sudo chmod 600 /etc/scanban.toml sudo chown root:root /etc/scanban.toml
-
Enable and start the service:
sudo systemctl enable scanban sudo systemctl start scanban -
Check the status:
sudo systemctl status scanban
-
View logs:
sudo journalctl -u scanban -f
In daemon mode, scanban continuously tails the configured log files and takes action in real-time as malicious activity is detected.
$ scanban -h
Usage of scanban:
-a scan the entirety of the file, not just new lines
-c string
config file (default "/etc/scanban.toml")
-d string
drop-in directory (default "/etc/scanban.d")
-f string
specific file to scan
-n dry run (show what would happen without taking action)
-s string
state file path for metrics (default "/var/run/scanban.state")
-t test complete merged config
-u string
unbanlist file (default "/var/lib/scanban/unbanlist.toml")
-v verbose output
-x dump complete merged config
Common usage patterns:
- Daemon mode:
scanban(uses config file to tail configured logs) - Test configuration:
scanban -n -a -v(dry run against all configured files) - Scan specific file:
scanban -n -f /var/log/auth.log - Scan from stdin:
cat /var/log/auth.log | scanban -n -f - - Debug config:
scanban -x(dump merged configuration)
For more configuration examples and deployment strategies, see the github wiki.
The main configuration file is located at /etc/scanban.toml. Additional configuration files can be placed in /etc/scanban.d/ (must have .toml extension) for better organization - these will be automatically merged with the main config.
Security Note: Since config files contain shell commands that will be executed as root, keep strict permissions: chmod 600 and chown root:root are recommended.
Important: TOML format requires escaping in regular expressions:
- Double-quoted strings: Use double backslashes:
"\\d+"instead of"\d+" - Single-quoted strings: Use single backslashes:
'\d+'(no escaping needed for backslashes)
Unbanning: Automatic unbanning is essential to prevent memory exhaustion and iptables bloat. Attackers typically don't reuse the same IP for extended periods, so temporary bans (hours to days) are sufficient and more performant than permanent bans.
| Key | Description | Example |
|---|---|---|
files |
List of log files or Docker containers to monitor | files = ["/var/log/auth.log", "docker://nginx"] |
whitelist |
IP addresses or CIDR ranges to never ban | whitelist = ["127.0.0.1", "192.168.1.0/24"] |
bantime |
Duration to ban IPs (in hours) | bantime = 24 |
threshold |
Number of offenses required before banning | threshold = 3 |
ip_regex |
Default regex pattern to extract IP addresses from log lines | ip_regex = "(\\d+\\.\\d+\\.\\d+\\.\\d+)" |
action |
Default action name to execute when banning | action = "ipsetblock" |
unban_action |
Default action name to execute when unbanning | unban_action = "ipsetunblock" |
dry_run |
Enable dry run mode (no actions executed) | dry_run = true |
verbose |
Enable verbose logging | verbose = true |
do_bans |
Enable/disable ban execution | do_bans = true |
do_unbans |
Enable/disable automatic unbanning | do_unbans = true |
unban_list |
Path to store scheduled unbans | unban_list = "/var/lib/scanban/unbanlist.toml" |
include |
Drop-in directory for additional configs | include = "/etc/scanban.d" |
Global settings are defined at the top level of the config file and apply to all rules by default. Individual rules can override these settings.
# Operational settings
dry_run = false
verbose = false
do_bans = true
do_unbans = true
# Default ban parameters
bantime = 24 # Ban for 24 hours
threshold = 3 # Require 3 offenses before banning
ip_regex = "(\\d+\\.\\d+\\.\\d+\\.\\d+)"
action = "ipsetblock"
unban_action = "ipsetunblock"Specify which log sources to monitor. scanban supports both regular files and Docker container logs.
files = [
"/var/log/auth.log",
"/var/log/ufw.log",
"docker://nginx", # Monitor Docker container logs
"docker://mysql"
]Each line from these sources is tested against all configured rules. When a line matches a rule's pattern and an IP is extracted, the threshold counter for that IP is incremented.
Prevent specific IPs or entire networks from ever being banned, even if they match rules. Supports both individual IPs and CIDR notation.
whitelist = [
"127.0.0.1", # Localhost
"192.168.1.0/24", # Local network
"10.0.0.0/8" # Private network
]Actions define named shell commands to execute when banning or unbanning an IP. These can be iptables/ipset commands, custom scripts, or any shell command.
[actions]
# iptables-based blocking (not recommended)
blockit = "iptables -A INPUT -s $ip -j DROP && iptables -A OUTPUT -d $ip -j DROP"
unblockit = "iptables -D INPUT -s $ip -j DROP && iptables -D OUTPUT -d $ip -j DROP"
# ipset-based blocking (more efficient for many IPs)
ipsetblock = "ipset add scanban $ip"
ipsetunblock = "ipset del scanban $ip"
# Custom notification
notify = "/usr/local/bin/alert-slack.sh"Variable Substitution: Use $ip in commands, which will be replaced with the actual IP address. Commands are executed via bash -c.
Environment Variables: All actions have access to these environment variables:
| Variable | Description |
|---|---|
SB_IP |
The offending IP address |
SB_BANTIME |
Ban duration in hours |
SB_FILENAME |
Log file or container where IP was detected |
SB_LINE |
Complete log line that triggered the ban |
SB_NAME |
Name of the action being executed |
SB_UNBANACTION |
Name of the corresponding unban action |
Rules define what patterns to match in log lines and how to handle them. Each rule is defined with [[rules]] and can match one or more patterns. When a pattern matches, scanban extracts the IP address and tracks violations.
Rule Parameters:
| Key | Required | Description | Example |
|---|---|---|---|
pattern |
Yes* | Single regex pattern to match | pattern = "Authentication failed" |
patterns |
Yes* | Multiple regex patterns (any can match) | patterns = ["Invalid user", "Failed password"] |
ip_regex |
No | Custom regex to extract IP (uses global default if omitted) | ip_regex = "SRC=(\\d+\\.\\d+\\.\\d+\\.\\d+)" |
action |
No | Override global ban action | action = "blockit" |
unban_action |
No | Override global unban action | unban_action = "unblockit" |
bantime |
No | Override global ban duration (hours) | bantime = 48 |
threshold |
No | Override global offense threshold | threshold = 1 |
*Either pattern or patterns is required.
Examples:
Simple single-pattern rule:
[[rules]]
pattern = "Authentication failed"Catch multiple SSH brute force patterns:
[[rules]]
patterns = [
"sshd.*Invalid user \\w+ from",
"sshd.*User \\w+ from .* not allowed because not listed in AllowUsers",
"sshd.*Did not receive identification string from"
]
threshold = 1 # Ban after just one offenseCustom IP extraction for firewall logs (when multiple IPs are in the line):
[[rules]]
pattern = "IN=\\w+ .*DPT=138"
ip_regex = " SRC=(\\d+\\.\\d+\\.\\d+\\.\\d+) " # Extract source IP specificallyWhen running in dry run mode, scanban shows what actions would be taken:
$ scanban -n -f ./auth2.log -c ./scanban.toml
2025/06/22 21:45:54 loading config
2025/06/22 21:45:54 opening unban list
2025/06/22 21:45:54 selecting scanner strategy
2025/06/22 21:45:54 building line handlers
2025/06/22 21:45:54 built 1 rules
2025/06/22 21:45:54 built 5 actions
2025/06/22 21:45:54 starting scanner loop
2025/06/22 21:45:54 actioned=true filename=./auth2.log ip=216.144.248.30 action=ipsetblock release=2025-06-23T21:45
2025/06/22 21:45:54 actioned=true filename=./auth2.log ip=80.94.95.15 action=ipsetblock release=2025-06-23T21:45
2025/06/22 21:45:54 actioned=true filename=./auth2.log ip=115.247.46.121 action=ipsetblock release=2025-06-23T21:45
2025/06/22 21:45:54 actioned=true filename=./auth2.log ip=216.144.248.25 action=ipsetblock release=2025-06-23T21:45
2025/06/22 21:45:54 actioned=true filename=./auth2.log ip=69.162.124.227 action=ipsetblock release=2025-06-23T21:45
2025/06/22 21:45:54 10 lines scanned in 0.00 seconds
2025/06/22 21:45:54 9 actioned, 1 rejected
2025/06/22 21:45:54 shutting down
Each ban action shows:
- actioned: Whether the action was taken
- filename: Source log file or container
- ip: Banned IP address
- action: Action executed
- release: When the IP will be unbanned
# /etc/scanban.toml
# Global settings
do_bans = true
do_unbans = true
bantime = 24
threshold = 3
ip_regex = "(\\d+\\.\\d+\\.\\d+\\.\\d+)"
action = "ipsetblock"
unban_action = "ipsetunblock"
# Log sources
files = [
"/var/log/auth.log",
"/var/log/ufw.log",
"docker://nginx"
]
# Never ban these
whitelist = [
"127.0.0.1",
"192.168.1.0/24"
]
# Define actions
[actions]
ipsetblock = "ipset add scanban $ip"
ipsetunblock = "ipset del scanban $ip"
# SSH brute force detection
[[rules]]
patterns = [
"sshd.*Invalid user \\w+ from",
"sshd.*User \\w+ from .* not allowed because not listed in AllowUsers",
"sshd.*Did not receive identification string from"
]
ip_regex = "from (\\d+\\.\\d+\\.\\d+\\.\\d+)"
threshold = 1
# Exploit scanner detection (phpMyAdmin, WordPress)
[[rules]]
patterns = [
"wp-admin",
"phpMyAdmin"
]Scanban includes several built-in security protections that go beyond traditional fail2ban deployments:
All IP addresses and log data are validated and sanitized before being used in shell commands to prevent log injection attacks:
- IP validation: Only valid IPv4/IPv6 addresses are accepted; dangerous characters like
;,|,$(), backticks are rejected - Environment variable sanitization: Control characters (null bytes, newlines, etc.) are stripped from log lines before being passed to actions
- Regex pattern validation: Dangerous regex patterns that could cause ReDoS (Regular Expression Denial of Service) are detected and rejected at startup
This prevents attackers from crafting malicious log entries that could execute arbitrary commands:
# This malicious log entry would be blocked:
Feb 2 12:34:56 host sshd[1234]: Failed password for root from 192.168.1.1; rm -rf /
Scanban protects itself from being weaponized as a DoS vector through hybrid rate limiting:
- Global rate limit: Maximum of 60 ban actions per minute (burst of 10) to prevent resource exhaustion
- Per-IP cooldown: Once an IP is banned, it cannot be re-banned for 1 hour (prevents wasted actions)
- No configuration needed: Sensible hardcoded defaults protect your system immediately
Attack scenarios prevented:
- IP spam: Attacker floods logs with 10,000 random IPs → Only first 10 banned immediately, then throttled to 60/min
- Duplicate spam: Same IP appears 10,000 times in logs → Banned once, remaining 9,999 ignored (no resource cost)
- System stability: Even under attack, scanban won't spawn thousands of iptables processes or crash from memory exhaustion
Dry run exemption: Rate limiting is automatically bypassed in dry run mode (-n flag) so you can see all potential bans when testing configurations.
Scanban automatically tracks comprehensive runtime statistics and writes them to a state file every 5 seconds. This provides real-time visibility into what's happening on your system.
By default, metrics are written to /var/run/scanban.state in YAML format. You can customize the location:
scanban -s /var/run/scanban.state # Default
scanban -s /tmp/scanban-metrics.yaml # Custom location (useful for non-root)Overall Statistics:
- Total lines scanned and processing rate (lines/second)
- Total bans executed
- Unique IPs seen vs. banned
- Uptime
Per-File Breakdown:
- Lines, matches, bans, and errors for each log file/container
- Identify which logs are noisiest
Per-Rule Effectiveness:
- Matches and bans for each rule
- Automatically labeled using rule descriptions or patterns
- Tune your rules based on real data
Action Execution:
- Count of each action type executed
- Track notification delivery, firewall updates, etc.
Error Categorization:
- Whitelisted IPs
- Invalid IPs caught by sanitization
- Rate limiting (cooldown vs. global)
- Threshold not met
- Failed actions
Rate Limiting Stats:
- How many IPs were blocked by per-IP cooldown
- How many actions were throttled by global rate limit
- Indicates if you're under attack
Top Banned IPs:
- Top 10 most frequently banned IPs
- Identify persistent attackers
updated_at: 2026-02-02T15:30:45Z
uptime_seconds: 3600.5
lines_total: 45230
lines_per_second: 12.56
bans_total: 127
unique_ips_seen: 1523
unique_ips_banned: 89
files:
/var/log/auth.log:
lines: 30120
matches: 95
bans: 78
errors: 2
docker://nginx:
lines: 15110
matches: 49
bans: 49
errors: 0
rules:
ssh_bruteforce:
matches: 120
bans: 102
wp_admin:
matches: 24
bans: 24
actions:
ipsetblock: 115
notify: 12
errors:
whitelisted: 12
invalid_ip: 0
rate_limited_cooldown: 38
rate_limited_global: 15
threshold_not_met: 45
no_match: 5432
rate_limiting:
cooldown_blocked: 38
global_blocked: 15
top_banned_ips:
- ip: 192.168.1.100
count: 5
- ip: 10.0.0.50
count: 3Rules are automatically labeled for metrics using the first available:
- desc field (if provided):
desc = "SSH Bruteforce"→ssh_bruteforce - pattern field:
pattern = "Failed password"→failed_password - First pattern from patterns array:
patterns = ["wp-admin"]→wp_admin - Fallback:
unnamed_rule
Labels are sanitized (lowercase, alphanumeric + underscores) for use as metric keys.
The state file can be easily integrated with monitoring systems:
Prometheus/Node Exporter:
# Use a textfile collector to expose metrics
*/5 * * * * /usr/local/bin/scanban-to-prometheus.shGrafana/InfluxDB: Parse the YAML and send metrics via HTTP API
Simple monitoring:
# Check if under attack (high rate limiting)
watch -n 5 'yq .rate_limiting.global_blocked /var/run/scanban.state'
# Monitor specific log file
yq '.files."/var/log/auth.log"' /var/run/scanban.stateScanban was created as a modern alternative to fail2ban with these goals:
- Simpler configuration - fail2ban's configuration can be verbose and complex
- Single binary - Easy deployment with no Python dependencies
- Performance - Go's efficiency handles high-volume logs well
- Docker integration - Native support for monitoring Docker container logs
- Modern codebase - Easier to modify and extend
- Security hardened - Built-in input sanitization and rate limiting (features not present in default fail2ban)
Issues and pull requests welcome at https://github.com/penguinpowernz/scanban
- IPv6 support
- TCP/Unix socket for external ban tools (BYO firewall integration)
- No-op action for monitoring without banning
- More comprehensive test coverage
- add a drop in for protecting a ruby on rails app
- add a drop in for blocking IPs based on tripwires
- add a drop in for blocking IPs based on bad SSH login attempts
- properly handle reverse DNS/PTR records based IPs
- protect against log injection attacks
- rate limit action execution