Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Configures Fail2ban with custom filters and actions to detect port scanning activity, SSH brute force attempts, and network reconnaissance, automatically banning offending IP addresses and alerting security teams to suspicious network probing.
.claude/skills/detecting-port-scanning-with-fail2ban/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-22 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-19 | ✓→✓ | = Same ✓ | — | — |
Do not use as the sole network security control, for protecting against distributed attacks from many source IPs, or as a replacement for proper firewall rules and network segmentation.
fail2ban-client --version)bash# Install Fail2ban sudo apt install -y fail2ban # Create local configuration (never edit jail.conf directly) sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local # Configure global defaults sudo tee /etc/fail2ban/jail.local << 'EOF' [DEFAULT] # Ban duration (1 hour default, escalates for repeat offenders) bantime = 3600 # Detection window findtime = 600 # Max failures before ban maxretry = 5 # Ban action using iptables banaction = iptables-multiport banaction_allports = iptables-allports # Email notifications destemail = security@example.com sender = fail2ban@example.com mta = sendmail action = %(action_mwl)s # Ignore internal networks ignoreip = 127.0.0.1/8 ::1 10.10.0.0/16 # Use systemd journal backend where available backend = systemd [sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 7200 findtime = 300 [sshd-ddos] enabled = true port = ssh filter = sshd-ddos logpath = /var/log/auth.log maxretry = 6 bantime = 3600 EOF
bash# Create iptables logging rule for dropped connections sudo iptables -N PORTSCAN sudo iptables -A PORTSCAN -j LOG --log-prefix "PORTSCAN_DETECTED: " --log-level 4 sudo iptables -A PORTSCAN -j DROP # Log SYN packets to closed ports (indicates scanning) sudo iptables -A INPUT -p tcp --tcp-flags SYN,ACK,FIN,RST SYN -m state --state NEW \ -m recent --name portscan --set sudo iptables -A INPUT -p tcp --tcp-flags SYN,ACK,FIN,RST SYN -m state --state NEW \ -m recent --name portscan --rcheck --seconds 10 --hitcount 20 -j PORTSCAN # Create Fail2ban filter for port scanning sudo tee /etc/fail2ban/filter.d/portscan.conf << 'EOF' [Definition] # Match iptables port scan log entries failregex = PORTSCAN_DETECTED: .* SRC=<HOST> DST=\S+ .* DPT=\d+ ignoreregex = datepattern = {^LN-BEG} EOF # Create Fail2ban filter for Nmap detection via kernel logs sudo tee /etc/fail2ban/filter.d/nmap-scan.conf << 'EOF' [Definition] # Detect rapid connection attempts to multiple ports from same source failregex = kernel: \[.*\] PORTSCAN_DETECTED: .* SRC=<HOST> iptables: .* PORTSCAN .* SRC=<HOST> ignoreregex = datepattern = {^LN-BEG} EOF # Create filter for HTTP scanning/probing sudo tee /etc/fail2ban/filter.d/http-scan.conf << 'EOF' [Definition] # Detect scanners probing for common vulnerabilities failregex = ^<HOST> .* "(GET|POST|HEAD) /(wp-login|wp-admin|phpmyadmin|admin|.env|xmlrpc|wp-content/uploads).*" (403|404|444) ^<HOST> .* "(GET|POST) /.*\.(php|asp|aspx|jsp|cgi)\?.*" (403|404) ^<HOST> .* "() .*" 400 ^<HOST> .* "(GET|POST) /.*" 400 ignoreregex = datepattern = {^LN-BEG} EOF
bash# Add port scan jails to jail.local sudo tee -a /etc/fail2ban/jail.local << 'EOF' [portscan] enabled = true filter = portscan logpath = /var/log/kern.log maxretry = 10 findtime = 60 bantime = 86400 banaction = iptables-allports action = %(action_mwl)s [nmap-scan] enabled = true filter = nmap-scan logpath = /var/log/kern.log maxretry = 5 findtime = 30 bantime = 86400 banaction = iptables-allports action = %(action_mwl)s [http-scan] enabled = true filter = http-scan logpath = /var/log/nginx/access.log maxretry = 10 findtime = 300 bantime = 3600 banaction = iptables-multiport port = http,https [recidive] enabled = true filter = recidive logpath = /var/log/fail2ban.log bantime = 604800 findtime = 86400 maxretry = 3 banaction = iptables-allports action = %(action_mwl)s EOF
bash# Create custom action that blocks and sends webhook notification sudo tee /etc/fail2ban/action.d/iptables-webhook.conf << 'EOF' [Definition] actionstart = <iptables> -N f2b-<name> <iptables> -A f2b-<name> -j RETURN <iptables> -I <chain> -p <protocol> -j f2b-<name> actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name> <iptables> -F f2b-<name> <iptables> -X f2b-<name> actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]' actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype> curl -s -X POST "<webhook_url>" \ -H "Content-Type: application/json" \ -d '{"text":"[Fail2ban] Banned <ip> from <name> jail (failures: <failures>)"}' actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype> [Init] chain = INPUT blocktype = DROP webhook_url = https://hooks.slack.com/services/XXXX/YYYY/ZZZZ EOF # Create escalating ban action for repeat offenders sudo tee /etc/fail2ban/action.d/escalating-ban.conf << 'EOF' [Definition] actionban = <iptables> -I f2b-<name> 1 -s <ip> -j DROP echo "$(date) BAN <ip> jail=<name> failures=<failures> bantime=<bantime>" >> /var/log/fail2ban-bans.log actionunban = <iptables> -D f2b-<name> -s <ip> -j DROP echo "$(date) UNBAN <ip> jail=<name>" >> /var/log/fail2ban-bans.log EOF
bash# Restart Fail2ban sudo systemctl restart fail2ban # Verify jails are active sudo fail2ban-client status sudo fail2ban-client status sshd sudo fail2ban-client status portscan # Test the port scan filter with a regex check sudo fail2ban-regex /var/log/kern.log /etc/fail2ban/filter.d/portscan.conf # Test the HTTP scan filter sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/http-scan.conf # Simulate a port scan from a test machine (authorized) # From the test machine: nmap -sS -p 1-1000 <target_ip> # Verify the scanner gets banned sudo fail2ban-client status portscan # Should show the test IP in the banned list # Check iptables for the ban rule sudo iptables -L f2b-portscan -n # Unban the test IP sudo fail2ban-client set portscan unbanip <test_ip>
bash# View real-time ban activity sudo tail -f /var/log/fail2ban.log | grep -E "Ban|Unban" # Generate daily summary report sudo tee /usr/local/bin/fail2ban-report.sh << 'SCRIPT' #!/bin/bash echo "=== Fail2ban Daily Report $(date) ===" echo "" echo "Active Jails:" sudo fail2ban-client status | grep "Jail list" echo "" echo "Currently Banned IPs:" for jail in $(sudo fail2ban-client status | grep "Jail list" | sed 's/.*://;s/,//g'); do count=$(sudo fail2ban-client status "$jail" | grep "Currently banned" | awk '{print $NF}') if [ "$count" -gt 0 ]; then echo " $jail: $count banned" sudo fail2ban-client status "$jail" | grep "Banned IP" fi done echo "" echo "Last 24 hours - Ban count by jail:" grep "Ban " /var/log/fail2ban.log | grep "$(date +%Y-%m-%d)" | awk '{print $NF}' | sort | uniq -c | sort -rn SCRIPT chmod +x /usr/local/bin/fail2ban-report.sh # Schedule daily report echo "0 8 * * * root /usr/local/bin/fail2ban-report.sh | mail -s 'Fail2ban Report' security@example.com" | sudo tee /etc/cron.d/fail2ban-report # Persist iptables rules across reboots sudo apt install iptables-persistent sudo netfilter-persistent save
| Term | Definition | |------|------------| | Jail | Fail2ban configuration unit that combines a filter (what to detect), an action (what to do), and parameters (thresholds, timing) for a specific service | | Filter | Regular expression patterns that Fail2ban applies to log files to identify failed authentication attempts, scanning, or other malicious activity | | Recidive Jail | Meta-jail that monitors Fail2ban's own log for repeat offenders, applying escalating ban durations to IPs banned multiple times | | Find Time | Time window in seconds during which Fail2ban counts matching log entries; maxretry failures within findtime triggers a ban | | Ban Action | Command or script executed when an IP is banned, typically adding firewall rules but extensible to webhooks, SIEM alerts, or blocklist updates | | Ignore IP | Whitelist of IP addresses or CIDR ranges that are never banned, preventing lockout of trusted networks and monitoring systems |
Context: A company runs a public web server that receives thousands of automated scan attempts daily from bots probing for vulnerable paths (/wp-admin, /phpmyadmin, /.env). The security team wants to automatically block scanners while allowing legitimate traffic. The server runs Nginx on Ubuntu 22.04.
Approach:
http-scan filter matching common scanner signatures and vulnerability probing URIsPitfalls:
## Fail2ban Port Scan Defense Report
**Server**: web-prod-01 (203.0.113.50)
**Reporting Period**: 2024-03-15 00:00 to 2024-03-16 00:00 UTC
### Active Jails
| Jail | Filter | Max Retry | Ban Time | Currently Banned |
|------|--------|-----------|----------|------------------|
| sshd | sshd | 3 | 2 hours | 12 IPs |
| portscan | portscan | 10 | 24 hours | 47 IPs |
| http-scan | http-scan | 10 | 1 hour | 89 IPs |
| recidive | recidive | 3 | 7 days | 8 IPs |
### 24-Hour Summary
- Total ban events: 347
- Unique IPs banned: 156
- Top attacking country: CN (67 IPs), RU (34 IPs), US (21 IPs)
- Most targeted service: HTTP scanning (214 bans)
- Recidive escalations: 8 IPs banned for 7 days
### Top 5 Banned IPs
| IP Address | Jail | Ban Count | First Seen | Last Seen |
|------------|------|-----------|------------|-----------|
| 45.33.32.156 | portscan | 12 | 00:15 | 23:47 |
| 198.51.100.23 | http-scan | 8 | 02:30 | 18:22 |
| 203.0.113.100 | sshd | 6 | 05:12 | 21:33 || Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.