fail2ban:1.1, use Redis, add logging, ban time and max attempts to be configured via UI soon

master
andryyy 2017-06-24 00:07:18 +02:00
parent fdc98f8418
commit b8e9b3d879
3 changed files with 28 additions and 4 deletions

View File

@ -2,7 +2,7 @@ FROM python:2-alpine
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
RUN apk add -U --no-cache iptables ip6tables RUN apk add -U --no-cache iptables ip6tables
RUN pip install docker RUN pip install docker redis
COPY logwatch.py / COPY logwatch.py /
CMD ["python2", "-u", "/logwatch.py"] CMD ["python2", "-u", "/logwatch.py"]

View File

@ -8,20 +8,28 @@ import ipaddress
import subprocess import subprocess
from threading import Thread from threading import Thread
import docker import docker
import redis
import time
import json
r = redis.StrictRedis(host='172.22.1.249', port=6379, db=0)
RULES = { RULES = {
'mailcowdockerized_postfix-mailcow_1': 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed', 'mailcowdockerized_postfix-mailcow_1': 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed',
'mailcowdockerized_dovecot-mailcow_1': '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),', 'mailcowdockerized_dovecot-mailcow_1': '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),',
'mailcowdockerized_sogo-mailcow_1': 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked', 'mailcowdockerized_sogo-mailcow_1': 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked',
'mailcowdockerized_php-fpm-mailcow_1': 'Mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)', 'mailcowdockerized_php-fpm-mailcow_1': 'Mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)',
} }
BAN_TIME = 1800
MAX_ATTEMPTS = 10 r.setnx("F2B_BAN_TIME", "1800")
r.setnx("F2B_MAX_ATTEMPTS", "10")
bans = {} bans = {}
log = {}
quit_now = False quit_now = False
def ban(address): def ban(address):
BAN_TIME = int(r.get("F2B_BAN_TIME"))
MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
ip = ipaddress.ip_address(address.decode('ascii')) ip = ipaddress.ip_address(address.decode('ascii'))
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped: if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
ip = ip.ipv4_mapped ip = ip.ipv4_mapped
@ -39,6 +47,9 @@ def ban(address):
bans[net]['last_attempt'] = time.time() bans[net]['last_attempt'] = time.time()
if bans[net]['attempts'] >= MAX_ATTEMPTS: if bans[net]['attempts'] >= MAX_ATTEMPTS:
log['time'] = int(round(time.time()))
log['message'] = "Banning %s" % net
r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
print "Banning %s" % net print "Banning %s" % net
if type(ip) is ipaddress.IPv4Address: if type(ip) is ipaddress.IPv4Address:
subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"]) subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
@ -47,9 +58,15 @@ def ban(address):
subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"]) subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"]) subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
else: else:
log['time'] = int(round(time.time()))
log['message'] = "%d more attempts until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], net)
r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
print "%d more attempts until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], net) print "%d more attempts until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], net)
def unban(net): def unban(net):
log['time'] = int(round(time.time()))
log['message'] = "Unbanning %s" % net
r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
print "Unbanning %s" % net print "Unbanning %s" % net
if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network: if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"]) subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
@ -64,11 +81,17 @@ def quit(signum, frame):
quit_now = True quit_now = True
def clear(): def clear():
log['time'] = int(round(time.time()))
log['message'] = "Clearing all bans"
r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
print "Clearing all bans" print "Clearing all bans"
for net in bans.copy(): for net in bans.copy():
unban(net) unban(net)
def watch(container): def watch(container):
log['time'] = int(round(time.time()))
log['message'] = "Watching %s" % container
r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
print "Watching", container print "Watching", container
client = docker.from_env() client = docker.from_env()
for msg in client.containers.get(container).attach(stream=True, logs=False): for msg in client.containers.get(container).attach(stream=True, logs=False):
@ -79,6 +102,7 @@ def watch(container):
def autopurge(): def autopurge():
while not quit_now: while not quit_now:
BAN_TIME = int(r.get("F2B_BAN_TIME"))
for net in bans.copy(): for net in bans.copy():
if time.time() - bans[net]['last_attempt'] > BAN_TIME: if time.time() - bans[net]['last_attempt'] > BAN_TIME:
unban(net) unban(net)

View File

@ -311,7 +311,7 @@ services:
- acme - acme
fail2ban-mailcow: fail2ban-mailcow:
image: mailcow/fail2ban:1.0 image: mailcow/fail2ban:1.1
build: ./data/Dockerfiles/fail2ban build: ./data/Dockerfiles/fail2ban
depends_on: depends_on:
- dovecot-mailcow - dovecot-mailcow