Makefile | ●●●●● patch | view | raw | blame | history | |
README.md | ●●●●● patch | view | raw | blame | history | |
vilain | ●●●●● patch | view | raw | blame | history | |
vilain.1 | ●●●●● patch | view | raw | blame | history | |
vilain.conf | ●●●●● patch | view | raw | blame | history | |
vilain.py | ●●●●● patch | view | raw | blame | history | |
vilain.rc | ●●●●● patch | view | raw | blame | history |
Makefile
New file @@ -0,0 +1,36 @@ # vilain - anti-bruteforce for OpenBSD # See LICENSE file for copyright and license details. # # vilain version VERSION = 0.3 # Customize below to fit your system # paths PREFIX = /usr/local MANPREFIX = ${PREFIX}/man/man1/ install: @echo installing executable file to ${DESTDIR}${PREFIX}/bin @mkdir -p ${DESTDIR}${PREFIX}/bin @cp -f vilain ${DESTDIR}${PREFIX}/bin @echo installing script file to ${DESTDIR}${PREFIX}/sbin @cp -f vilain.py ${DESTDIR}${PREFIX}/sbin @chmod 755 ${DESTDIR}${PREFIX}/bin/vilain @chmod 644 ${DESTDIR}${PREFIX}/sbin/vilain.py @echo installing init script in /etc/rc.d @cp -f vilain.rc /etc/rc.d/vilain @chmod 755 /etc/rc.d/vilain @echo installing manual page to ${DESTDIR}${MANPREFIX}/man1 @mkdir -p ${DESTDIR}${MANPREFIX}/ @cp -f vilain.1 ${DESTDIR}${MANPREFIX}/vilain.1 @chmod 644 ${DESTDIR}${MANPREFIX}/vilain.1 uninstall: @echo removing executable file from ${DESTDIR}${PREFIX}/bin @rm -f ${DESTDIR}${PREFIX}/bin/vilain @rm -f ${DESTDIR}${PREFIX}/sbin/vilain.py @echo removing manual page to ${DESTDIR}${MANPREFIX}/ @rm -f ${DESTDIR}${MANPREFIX}/vilain.1 .PHONY: install uninstall README.md
@@ -1,15 +1,9 @@ # vilain Mimic fail2ban with pf for OpenBSD. Inspired from http://www.vincentdelft.be/post/post_20161106 This repository is just for work. See here for last vilain "stable" version : http://git.yeuxdelibad.net/vilain/ In pf.conf, add : In pf.conf, add according to your configuration : table <vilain_bruteforce> persist block quick from <vilain_bruteforce> @@ -23,4 +17,14 @@ pfctl -t vilain_bruteforce -T show To start vilain at boot, add this in ``/etc/rc.local`` ``` /usr/bin/tmux new -s vilain -d /usr/local/bin/vilain ``` Then, to attach to the tmux session, run : ``` tmux a -t vilain ``` vilain
@@ -1,179 +1,9 @@ #!/usr/bin/env python3.4 # -*- coding:Utf-8 -*- """ Author : thuban <thuban@yeuxdelibad.net> Licence : MIT Description : Mimic fail2ban with pf for OpenBSD. Inspired from http://www.vincentdelft.be/post/post_20161106 In pf.conf, add : table <vilain_bruteforce> persist block quick from <vilain_bruteforce> You might want to add a cron task to remove old banned IP. As example, to ban for one day max : pfctl -t vilain_bruteforce -T expire 86400 To see banned IP : pfctl -t vilain_bruteforce -T show """ import sys import os import configparser import re import time import logging import subprocess from multiprocessing import Process, Queue, TimeoutError configfile = "/etc/vilain.conf" version = "0.1" vilain_table = "vilain_bruteforce" logfile = "/var/log/daemon" if os.geteuid() != 0: print("Only root can use this tool") sys.exit() # Configure logging logger = logging.getLogger(__name__) logging.basicConfig(filename=logfile, format='%(asctime)s %(module)s:%(funcName)s:%(message)s', datefmt='%H:%M:%S') logger.setLevel(logging.DEBUG) ch = logging.StreamHandler(sys.stdout) logger.addHandler(ch) # functions def readconfig(): if not os.path.isfile(configfile): logging.error("Can't read config file, exiting...") sys.exit(1) config = configparser.ConfigParser() config.read(configfile) return(config) def load_config(): c = readconfig() d = c.defaults() watch_while = int(d['watch_while']) maxtries = int(d['maxtries']) vilain_table = d['vilain_table'] return(watch_while, maxtries, vilain_table) def load_sections(): c = readconfig() for s in c.sections(): logfile = c.get(s,'logfile') regex = c.get(s,'regex') d = {'name' : s, 'logfile':logfile, 'regex':regex} yield d def check_logs(logfile, regex, bad_ip_queue): """ worker who put in bad_ip_queue bruteforce IP """ if not os.path.isfile(logfile) : logger.warning("{} doesn't exist".format(logfile)) return # Watch the file for changes stat = os.stat(logfile) size = stat.st_size mtime = stat.st_mtime RE = re.compile(regex) while True: time.sleep(0.5) stat = os.stat(logfile) if mtime < stat.st_mtime: mtime = stat.st_mtime with open(logfile, "rb") as f: f.seek(size) lines = f.readlines() ul = [ u.decode() for u in lines ] line = "".join(ul).strip() ret = RE.match(line) if ret: bad_ip = ret.groups()[0] bad_ip_queue.put(bad_ip) size = stat.st_size def ban_ips(bad_ip_queue, watch_while, maxtries, vilain_table): """ worker who ban IP on bad_ip_queue add IP in bad_ips_list record time when this IP has been seen in ip_seen_at = { ip:time } check number of occurence of the same ip in bad_ips_list if more than 3 : ban and clean of list check old ip in ip_seen_at : remove older than watch_while """ bad_ips_list = [] ip_seen_at = {} while True: try: ip = bad_ip_queue.get(0.5) logger.info("{} detected".format(ip)) bad_ips_list.append(ip) ip_seen_at[ip] = time.time() n_ip = bad_ips_list.count(ip) if n_ip >= maxtries: logger.info("Blacklisting {}".format(ip)) subprocess.call(["pfctl", "-t", vilain_table, "-T", "add", ip]) ip_seen_at.pop(ip) while ip in bad_ips_list: bad_ips_list.remove(ip) to_remove = [] for recorded_ip, last_seen in ip_seen_at.items(): if time.time() - last_seen >= watch_while: logger.info("{} not seen since a long time, forgetting...".format(recorded_ip)) to_remove.append(recorded_ip) for i in to_remove: ip_seen_at.pop(i) except TimeoutError: pass except KeyboardInterrupt: sys.exit(0) def main(): os.chdir(os.path.dirname(os.path.abspath(__file__))) watch_while, maxtries, vilain_table = load_config() bad_ip_queue = Queue() workers = [] for entry in load_sections(): logger.info("Start vilain for {}".format(entry['name'])) worker = Process(target=check_logs, args=(entry['logfile'], entry['regex'], bad_ip_queue)) workers.append(worker) worker.daemon = True worker.start() ban_worker = Process(target=ban_ips, args=(bad_ip_queue, watch_while, maxtries)) workers.append(ban_worker) ban_worker.daemon = True ban_worker.start() for w in workers: w.join() return 0 if __name__ == '__main__': main() # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 #!/bin/sh PYTHONVERSION=$(ls -l /usr/local/bin/python3.* |grep -Eo "3\.[0-9]" |tail -n1) PYTHON="/usr/local/bin/python$PYTHONVERSION" if [ -x $PYTHON ]; then $PYTHON /usr/local/sbin/vilain.py >> /var/log/daemon 2>&1 & else echo "Error : no python3 executable found" fi exit vilain.1
New file @@ -0,0 +1,41 @@ . .TH vilain 28 "March 2017" "" "Fail2ban-like for OpenBSD" .SH NAME vilain \- fail2ban-like for OpenBSD . .SH SYNOPSIS .RS To use vilain : 1. Copy configuration file in /etc/vilain.conf and configure according to your needs. 2. Configure pf according to the example below 3. start vilain via rcctl You only need python >= 3.5 to use it. .RE .SH DESCRIPTION .RS In pf.conf, add according to your configuration : table <vilain_bruteforce> persist block quick from <vilain_bruteforce> You might want to add a cron task to remove old banned IP. As example, to ban for one day max : pfctl -t vilain_bruteforce -T expire 86400 To see banned IP : pfctl -t vilain_bruteforce -T show .RE .SH LOGS .RS Watch vilain logs in /var/log/daemon. .RE .SH BUGS .RS Please let me know : <thuban@yeuxdelibad.net> .RE vilain.conf
@@ -2,12 +2,17 @@ # 24h + 5min # Time to keep banned a bad ip watch_while = 86700 # Max tries before being bannes # Max tries before being banned maxtries = 3 # pf table to keep bad IP. # remember to clean the table with this command in a cron job : # pfctl -t vilain_bruteforce -T expire 86400 # /sbin/pfctl -t vilain_bruteforce -T expire 86400 vilain_table = vilain_bruteforce ### Ip ignored ### [ignoreip] ip1 = 92.150.160.157 ip2 = 92.150.160.156 ### Guardians #[name of the guardian] @@ -22,3 +27,28 @@ logfile = /var/log/authlog regex = .* Connection closed by ([\S]+) .* #[http404] #logfile = /var/www/logs/access.log #regex = (?:\S+\s){1}(\S+).*\s404\s.* [http401] logfile = /var/www/logs/access.log regex = (?:\S+\s){1}(\S+).*\s401\s.* [http403] logfile = /var/www/logs/access.log regex = (?:\S+\s){1}(\S+).*\s403\s.* [smtp] logfile = /var/log/maillog regex = .* event=failed-command address=([\S]+) .* [dovecot] logfile = /var/log/maillog regex = .*auth failed.*rip=([\S]+),.* [wordpress] # don't use if you have wordpress logfile = /var/www/logs/access.log regex = (?:\S+\s){1}(\S+).*wp-login.php.* vilain.py
New file @@ -0,0 +1,194 @@ #!/usr/bin/env python3 # -*- coding:Utf-8 -*- """ Author : thuban <thuban@yeuxdelibad.net> Licence : MIT Require : python >= 3.5 Description : Mimic fail2ban with pf for OpenBSD. Inspired from http://www.vincentdelft.be/post/post_20161106 In pf.conf, add : table <vilain_bruteforce> persist block quick from <vilain_bruteforce> You might want to add a cron task to remove old banned IP. As example, to ban for one day max : pfctl -t vilain_bruteforce -T expire 86400 To see banned IP : pfctl -t vilain_bruteforce -T show """ import sys import os import configparser import re import logging import subprocess import asyncio import time from multiprocessing import Process configfile = "/etc/vilain.conf" version = "0.3" vilain_table = "vilain_bruteforce" logfile = "/var/log/daemon" sleeptime = 0.5 if os.geteuid() != 0: print("Only root can use this tool") sys.exit() # Configure logging logger = logging.getLogger(__name__) logging.basicConfig(filename=logfile, format='%(asctime)s %(module)s:%(funcName)s:%(message)s', datefmt='%H:%M:%S') logger.setLevel(logging.DEBUG) ch = logging.StreamHandler(sys.stdout) logger.addHandler(ch) # functions def readconfig(): if not os.path.isfile(configfile): logging.error("Can't read config file, exiting...") sys.exit(1) config = configparser.ConfigParser() config.read(configfile) return(config) def load_config(): c = readconfig() d = c.defaults() watch_while = int(d['watch_while']) maxtries = int(d['maxtries']) vilain_table = d['vilain_table'] if c.has_section('ignoreip'): ignoreip = [ i[1] for i in c.items('ignoreip') if i[0] not in c.defaults()] else: ignoreip = [] return(watch_while, maxtries, vilain_table, ignoreip) def load_sections(): c = readconfig() for s in c.sections(): if c.has_option(s,'logfile'): logfile = c.get(s,'logfile') regex = c.get(s,'regex') d = {'name' : s, 'logfile':logfile, 'regex':regex} yield d class Vilain(): def __init__(self): self.loop = asyncio.get_event_loop() self.watch_while, self.maxtries, self.vilain_table, self.ignore_ip = load_config() #self.bad_ip_queue = [] self.bad_ip_queue = asyncio.Queue(loop=self.loop) for entry in load_sections(): logger.info("Start vilain for {}".format(entry['name'])) asyncio.ensure_future(self.check_logs(entry['logfile'], entry['regex'], entry['name'])) asyncio.ensure_future(self.ban_ips()) def start(self): try: self.loop.run_forever() except KeyboardInterrupt: self.loop.close() finally: self.loop.close() async def check_logs(self, logfile, regex, reason): """ worker who put in bad_ip_queue bruteforce IP """ if not os.path.isfile(logfile) : logger.warning("{} doesn't exist".format(logfile)) else : # Watch the file for changes stat = os.stat(logfile) size = stat.st_size mtime = stat.st_mtime RE = re.compile(regex) while True: await asyncio.sleep(sleeptime) stat = os.stat(logfile) if mtime < stat.st_mtime: mtime = stat.st_mtime with open(logfile, "rb") as f: f.seek(size) lines = f.readlines() ul = [ u.decode() for u in lines ] line = "".join(ul).strip() ret = RE.match(line) if ret: bad_ip = ret.groups()[0] if bad_ip not in self.ignore_ip : #self.bad_ip_queue.append({'ip' : bad_ip, 'reason' : reason}) await self.bad_ip_queue.put({'ip' : bad_ip, 'reason' : reason}) size = stat.st_size async def ban_ips(self): """ worker who ban IP on bad_ip_queue add IP in bad_ips_list record time when this IP has been seen in ip_seen_at = { ip:time } check number of occurence of the same ip in bad_ips_list if more than 3 : ban and clean of list check old ip in ip_seen_at : remove older than watch_while """ bad_ips_list = [] ip_seen_at = {} while True: await asyncio.sleep(sleeptime) #if not len(s#elf.bad_ip_queue) > 0: # continue ip_item = await self.bad_ip_queue.get() #ip_item = self.bad_ip_queue.pop() ip = ip_item['ip'] reason = ip_item['reason'] logger.info("{} detected, reason {}".format(ip, reason)) bad_ips_list.append(ip) ip_seen_at[ip] = time.time() n_ip = bad_ips_list.count(ip) if n_ip >= self.maxtries: logger.info("Blacklisting {}".format(ip)) subprocess.call(["pfctl", "-t", self.vilain_table, "-T", "add", ip]) ip_seen_at.pop(ip) while ip in bad_ips_list: bad_ips_list.remove(ip) to_remove = [] for recorded_ip, last_seen in ip_seen_at.items(): if time.time() - last_seen >= self.watch_while: logger.info("{} not seen since a long time, forgetting...".format(recorded_ip)) to_remove.append(recorded_ip) for i in to_remove: ip_seen_at.pop(i) def main(): os.chdir(os.path.dirname(os.path.abspath(__file__))) v = Vilain() p = Process(target=v.start()) p.start() return 0 if __name__ == '__main__': main() # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 vilain.rc
New file @@ -0,0 +1,13 @@ #!/bin/sh # # $OpenBSD: vilain.rc,v 1.0 2017/03/28 17:58:46 Thuban$ daemon="/usr/local/bin/vilain" . /etc/rc.d/rc.subr pexp="python3.*vilain\.py" rc_reload=NO rc_cmd $1