add README

This commit is contained in:
rmanach 2025-05-25 15:45:32 +02:00
parent 9145377125
commit d780f2ba6c
2 changed files with 74 additions and 3 deletions

61
README.md Normal file
View File

@ -0,0 +1,61 @@
# ufwban
A little CLI tool (wrapping **ufw**) that read Nginx access logs and block ip based on simple rules.
**THIS IS NOT** a replacement of **fail2ban**, i just do it for fun and to have a simple tool to configure quickly banning undesired IP.
This a **RUDE** IP deny. It will ban the IP on all machine ports with no ban time, so be careful !
## Requirements
* ufw
* Nginx
* Python >= 3.10
## Configuration
Create a `conf.json` next to the script. Use the [conf.json.example](./conf.json.example) for sample.
```json
{
"rules": {
"codes": [],
"contents": [],
"agents": []
},
"whitelist": []
}
```
* **codes**: List of unauthorized HTTP codes
* **contents**: string parts that are not not allowed in the request URL (ex: /x00, .json, .php, .env.local, etc...)
* **agents** string parts that are not not allowed in user-agent (ex: bot)
* **whitelist**: List of IP to whitelist (ex: 192.168.1.1)
## Run
```bash
usage: ufwban [-h] [--dry-run] [--refresh] [--reload] [--live]
Ban ip from Nginx access logs based on simple rules.
options:
-h, --help show this help message and exit
--dry-run
--refresh Drop all the deny ip in the UFW table and return
--reload Reload the UFW firewall
--live Read inputs from stdin
```
* Batch mode:
```bash
# read all access.log*, parse Nginx log and ban ip
python ufwban.py
# you can launch a --dry-run mode to see which ip is going to be denied
python ufwban.py --dry-run
# drop all "DENY IN" ufw rules (be careful)
python ufwban.py --refresh
```
* Live mode:
```bash
# Read and parse Nginx logs on each new entry and ban ip
tail -f /var/log/nginx/access.log | python ufwban.py
```

View File

@ -22,8 +22,11 @@ UFW_CONF = "conf.json"
class UFW:
"""Wraps ufw binary commands"""
@staticmethod
def drop_all():
"""Remove all "DENY IN" rules."""
logging.info("dropping all deny rules...")
while (ufw_deny_ips := UFW.list_deny()) and len(ufw_deny_ips):
for ip, id_ in ufw_deny_ips.items():
@ -33,6 +36,7 @@ class UFW:
@staticmethod
def reload():
"""Force ufw reload."""
logging.info("reloading ufw...")
process = subprocess.run(["ufw", "reload"], capture_output=True)
@ -41,6 +45,7 @@ class UFW:
@staticmethod
def delete_deny_ip(id_: str):
"""Delete a rule based on its id."""
logging.info(f"cmd running: ufw delete {id_}")
process = subprocess.run(
["ufw", "delete", id_], input=b"y\n", capture_output=True
@ -53,6 +58,7 @@ class UFW:
@staticmethod
def ban_ip(ip: str):
"""Deny an ip for any ports on the machine."""
logging.info(f"cmd running: ufw deny from {ip}")
process = subprocess.run(["ufw", "deny", "from", ip], capture_output=True)
@ -61,6 +67,10 @@ class UFW:
@staticmethod
def list_deny() -> dict[str, str]:
"""
List all the denied ip.
Return a dict mapping the ip with its ufw id.
"""
ips = {}
cp = subprocess.run(["ufw", "status", "numbered"], capture_output=True)
if cp.returncode != 0:
@ -258,13 +268,13 @@ def main(refresh: bool = False, reload: bool = False, dry_run: bool = False):
logs = parse_nginx_logs()
logs_to_deny = get_logs_to_deny(logs, rules)
if args.refresh and not args.dry_run:
if refresh and not dry_run:
UFW.drop_all()
return
for ip, log in logs_to_deny.items():
print(f"> banning log: {log}")
if not args.dry_run:
if not dry_run:
UFW.ban_ip(ip)
if reload:
@ -337,7 +347,7 @@ if __name__ == "__main__":
exit_code = 1
logging.fatal(f"unexpected error occurred, err={e}")
except KeyboardInterrupt:
logging.warning("ok, you just kill me..., bye")
logging.warning("ok, you just kill me... Bye !")
logging.info(f"ufwban done in elapsed time: {time.perf_counter() - start:.2f}s")
exit(exit_code)