npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
When to Use
- Automatically blocking IP addresses that perform port scans against internet-facing servers
- Defending SSH, HTTP, FTP, and other services against brute force attacks with automated IP banning
- Creating custom detection filters for organization-specific attack patterns in log files
- Reducing noise from automated scanning bots before traffic reaches IDS/IPS for deeper analysis
- Implementing defense-in-depth by adding host-based automated response to network monitoring
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.
Prerequisites
- Fail2ban 0.11+ installed (
fail2ban-client --version) - Root/sudo access for iptables/nftables manipulation
- Services logging connection attempts to parseable log files (syslog, auth.log, access.log)
- iptables or nftables installed and operational as the host firewall
- Optional: SMTP server for email notifications on ban events
Workflow
Step 1: Install and Configure Fail2ban
# 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
EOFStep 2: Create Custom Port Scan Detection Filter
# 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}
EOFStep 3: Configure Jail for Port Scanning
# 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
EOFStep 4: Configure Advanced Ban Actions
# 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
EOFStep 5: Test and Validate Detection
# 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>Step 6: Monitor and Maintain
# 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 saveKey Concepts
| 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 |
Tools & Systems
- Fail2ban 0.11+: Log-parsing intrusion prevention framework that bans IP addresses based on pattern matching across any log file
- iptables/nftables: Linux kernel firewall used by Fail2ban ban actions to block offending IP addresses at the network layer
- fail2ban-regex: Testing utility for validating filter regular expressions against actual log files before deploying to production
- fail2ban-client: Command-line management tool for querying jail status, manually banning/unbanning IPs, and reloading configuration
- rsyslog/syslog-ng: System logging daemons that generate the log files Fail2ban monitors for attack detection
Common Scenarios
Scenario: Defending a Public-Facing Web Server Against Automated Scanning
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:
- Install Fail2ban and configure it to monitor Nginx access logs for scanning patterns (404/403 responses to known vulnerability paths)
- Create a custom
http-scanfilter matching common scanner signatures and vulnerability probing URIs - Set maxretry to 10 within a 5-minute findtime, with a 1-hour bantime for first offense
- Enable the recidive jail to escalate ban duration to 7 days for repeat offenders
- Configure webhook notifications to Slack for real-time visibility of banning activity
- Add iptables logging rules for SYN packets to closed ports to detect port scanning
- Create a daily report script showing banned IPs, attack patterns, and geographic distribution
Pitfalls:
- Setting maxretry too low (e.g., 1-2), causing legitimate users who mistype URLs to get banned
- Not whitelisting monitoring systems (Nagios, UptimeRobot) that may trigger filters with their health checks
- Forgetting to persist iptables rules, losing all bans after a reboot
- Not testing filters with fail2ban-regex before deploying, resulting in no matches or excessive false positives
Output Format
## 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 |References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 1
api-reference.md2.0 KB
Fail2ban Port Scan Detection API Reference
fail2ban-client CLI
# Service status
fail2ban-client status
# Jail status
fail2ban-client status sshd
# Ban IP manually
fail2ban-client set sshd banip 192.168.1.100
# Unban IP
fail2ban-client set sshd unbanip 192.168.1.100
# Reload configuration
fail2ban-client reload
# Get ban time for jail
fail2ban-client get sshd bantime
# Set ban time
fail2ban-client set sshd bantime 7200Jail Configuration (/etc/fail2ban/jail.local)
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
banaction = iptables-multiport
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
[portscan]
enabled = true
filter = portscan
logpath = /var/log/syslog
maxretry = 3
findtime = 300
bantime = 86400
action = iptables-allports[name=portscan]Custom Filter (/etc/fail2ban/filter.d/portscan.conf)
[Definition]
failregex = UFW BLOCK .* SRC=<HOST>
iptables .* SRC=<HOST> .* DPT=
ignoreregex =Ban Actions
| Action | Description |
|---|---|
iptables-multiport |
Ban specific ports via iptables |
iptables-allports |
Ban all ports via iptables |
nftables-multiport |
Ban via nftables |
firewallcmd-rich-rules |
Ban via firewalld |
sendmail-whois |
Email notification with WHOIS |
abuseipdb |
Report to AbuseIPDB |
Log Parsing Patterns
# Fail2ban log - count bans per IP
grep "Ban " /var/log/fail2ban.log | grep -oP '\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rn
# Auth.log - failed SSH logins
grep "Failed password" /var/log/auth.log | grep -oP 'from \K\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rn
# Syslog - blocked connections (UFW)
grep "UFW BLOCK" /var/log/syslog | grep -oP 'SRC=\K\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rnEscalating Ban Times (recidive jail)
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 604800 # 1 week for repeat offenders
findtime = 86400
maxretry = 3Scripts 1
agent.py8.2 KB
#!/usr/bin/env python3
"""Fail2ban port scan detection and management agent."""
import json
import os
import re
import subprocess
import sys
from collections import Counter
from datetime import datetime
FAIL2BAN_CLIENT = os.environ.get("FAIL2BAN_CLIENT", "fail2ban-client")
FAIL2BAN_LOG = os.environ.get("FAIL2BAN_LOG", "/var/log/fail2ban.log")
AUTH_LOG = os.environ.get("AUTH_LOG", "/var/log/auth.log")
def check_fail2ban_status():
"""Check Fail2ban service status and active jails."""
try:
result = subprocess.run(
[FAIL2BAN_CLIENT, "status"], capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
return {"running": False, "error": result.stderr.strip()}
jails = []
for line in result.stdout.splitlines():
if "Jail list" in line:
jail_str = line.split(":", 1)[1].strip()
jails = [j.strip() for j in jail_str.split(",") if j.strip()]
return {"running": True, "jails": jails, "jail_count": len(jails)}
except FileNotFoundError:
return {"running": False, "error": "fail2ban-client not found"}
def get_jail_status(jail_name):
"""Get detailed status of a specific jail."""
try:
result = subprocess.run(
[FAIL2BAN_CLIENT, "status", jail_name],
capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
return {"error": result.stderr.strip()}
info = {"jail": jail_name}
for line in result.stdout.splitlines():
line = line.strip()
if "Currently failed" in line:
info["currently_failed"] = int(line.split(":", 1)[1].strip())
elif "Total failed" in line:
info["total_failed"] = int(line.split(":", 1)[1].strip())
elif "Currently banned" in line:
info["currently_banned"] = int(line.split(":", 1)[1].strip())
elif "Total banned" in line:
info["total_banned"] = int(line.split(":", 1)[1].strip())
elif "Banned IP list" in line:
ips = line.split(":", 1)[1].strip()
info["banned_ips"] = [ip.strip() for ip in ips.split() if ip.strip()]
return info
except Exception as e:
return {"error": str(e)}
def ban_ip(ip_address, jail_name="sshd"):
"""Manually ban an IP address in a specific jail."""
try:
result = subprocess.run(
[FAIL2BAN_CLIENT, "set", jail_name, "banip", ip_address],
capture_output=True, text=True, timeout=10
)
return {
"action": "ban",
"ip": ip_address,
"jail": jail_name,
"success": result.returncode == 0,
"timestamp": datetime.utcnow().isoformat() + "Z",
}
except Exception as e:
return {"action": "ban", "success": False, "error": str(e)}
def unban_ip(ip_address, jail_name="sshd"):
"""Unban an IP address from a specific jail."""
try:
result = subprocess.run(
[FAIL2BAN_CLIENT, "set", jail_name, "unbanip", ip_address],
capture_output=True, text=True, timeout=10
)
return {
"action": "unban",
"ip": ip_address,
"jail": jail_name,
"success": result.returncode == 0,
"timestamp": datetime.utcnow().isoformat() + "Z",
}
except Exception as e:
return {"action": "unban", "success": False, "error": str(e)}
def parse_fail2ban_log(log_path=None, limit=5000):
"""Parse Fail2ban log for ban/unban statistics."""
log_path = log_path or FAIL2BAN_LOG
if not os.path.exists(log_path):
return {"error": f"Log not found: {log_path}"}
bans = Counter()
unbans = Counter()
jails = Counter()
recent_bans = []
with open(log_path, "r") as f:
for i, line in enumerate(f):
if i > limit:
break
if "Ban " in line:
ip_match = re.search(r'Ban\s+(\d+\.\d+\.\d+\.\d+)', line)
jail_match = re.search(r'\[([^\]]+)\]', line)
if ip_match:
ip = ip_match.group(1)
jail = jail_match.group(1) if jail_match else "unknown"
bans[ip] += 1
jails[jail] += 1
recent_bans.append({"ip": ip, "jail": jail, "line": line.strip()[:200]})
elif "Unban " in line:
ip_match = re.search(r'Unban\s+(\d+\.\d+\.\d+\.\d+)', line)
if ip_match:
unbans[ip_match.group(1)] += 1
return {
"total_bans": sum(bans.values()),
"total_unbans": sum(unbans.values()),
"unique_banned_ips": len(bans),
"top_banned_ips": bans.most_common(20),
"bans_by_jail": dict(jails),
"recent_bans": recent_bans[-20:],
}
def parse_auth_log_ssh(log_path=None):
"""Parse auth.log for SSH brute force patterns."""
log_path = log_path or AUTH_LOG
if not os.path.exists(log_path):
return {"error": f"Auth log not found: {log_path}"}
failed_ips = Counter()
failed_users = Counter()
success_ips = Counter()
with open(log_path, "r") as f:
for line in f:
if "Failed password" in line or "Failed publickey" in line:
ip_match = re.search(r'from\s+(\d+\.\d+\.\d+\.\d+)', line)
user_match = re.search(r'for\s+(?:invalid\s+user\s+)?(\S+)', line)
if ip_match:
failed_ips[ip_match.group(1)] += 1
if user_match:
failed_users[user_match.group(1)] += 1
elif "Accepted password" in line or "Accepted publickey" in line:
ip_match = re.search(r'from\s+(\d+\.\d+\.\d+\.\d+)', line)
if ip_match:
success_ips[ip_match.group(1)] += 1
return {
"total_failed": sum(failed_ips.values()),
"unique_failed_ips": len(failed_ips),
"top_failed_ips": failed_ips.most_common(20),
"top_targeted_users": failed_users.most_common(20),
"successful_logins": success_ips.most_common(10),
}
def detect_port_scan_from_logs(log_path=None):
"""Detect port scanning patterns from system logs."""
log_path = log_path or os.environ.get("SYSLOG_PATH", "/var/log/syslog")
if not os.path.exists(log_path):
return {"error": f"Syslog not found: {log_path}"}
scanners = Counter()
with open(log_path, "r") as f:
for line in f:
if "UFW BLOCK" in line or "iptables" in line.lower():
ip_match = re.search(r'SRC=(\d+\.\d+\.\d+\.\d+)', line)
if ip_match:
scanners[ip_match.group(1)] += 1
port_scanners = {ip: count for ip, count in scanners.items() if count > 20}
return {
"potential_scanners": len(port_scanners),
"scanner_ips": sorted(port_scanners.items(), key=lambda x: x[1], reverse=True)[:20],
}
def generate_report():
"""Generate comprehensive Fail2ban security report."""
return {
"timestamp": datetime.utcnow().isoformat() + "Z",
"service_status": check_fail2ban_status(),
"ban_statistics": parse_fail2ban_log(),
"ssh_analysis": parse_auth_log_ssh(),
"port_scan_detection": detect_port_scan_from_logs(),
}
if __name__ == "__main__":
action = sys.argv[1] if len(sys.argv) > 1 else "report"
if action == "report":
print(json.dumps(generate_report(), indent=2, default=str))
elif action == "status":
print(json.dumps(check_fail2ban_status(), indent=2))
elif action == "jail" and len(sys.argv) > 2:
print(json.dumps(get_jail_status(sys.argv[2]), indent=2))
elif action == "ban" and len(sys.argv) > 2:
jail = sys.argv[3] if len(sys.argv) > 3 else "sshd"
print(json.dumps(ban_ip(sys.argv[2], jail), indent=2))
elif action == "unban" and len(sys.argv) > 2:
jail = sys.argv[3] if len(sys.argv) > 3 else "sshd"
print(json.dumps(unban_ip(sys.argv[2], jail), indent=2))
elif action == "bans":
print(json.dumps(parse_fail2ban_log(), indent=2))
elif action == "ssh":
print(json.dumps(parse_auth_log_ssh(), indent=2))
else:
print("Usage: agent.py [report|status|jail <name>|ban <ip> [jail]|unban <ip> [jail]|bans|ssh]")