logwatch.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env python2
  2. import re
  3. import os
  4. import time
  5. import atexit
  6. import signal
  7. import ipaddress
  8. import subprocess
  9. from threading import Thread
  10. import docker
  11. import redis
  12. import time
  13. import json
  14. yes_regex = re.compile(r'([yY][eE][sS]|[yY])+$')
  15. if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)):
  16. print "Skipping Fail2ban container..."
  17. raise SystemExit
  18. r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0)
  19. RULES = {
  20. 'mailcowdockerized_postfix-mailcow_1': 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed',
  21. 'mailcowdockerized_dovecot-mailcow_1': '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),',
  22. 'mailcowdockerized_sogo-mailcow_1': 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked',
  23. 'mailcowdockerized_php-fpm-mailcow_1': 'Mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)',
  24. }
  25. r.setnx("F2B_BAN_TIME", "1800")
  26. r.setnx("F2B_MAX_ATTEMPTS", "10")
  27. r.setnx("F2B_RETRY_WINDOW", "600")
  28. bans = {}
  29. log = {}
  30. quit_now = False
  31. def ban(address):
  32. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  33. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  34. RETRY_WINDOW = int(r.get("F2B_RETRY_WINDOW"))
  35. WHITELIST = r.hgetall("F2B_WHITELIST")
  36. ip = ipaddress.ip_address(address.decode('ascii'))
  37. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  38. ip = ip.ipv4_mapped
  39. address = str(ip)
  40. if ip.is_private or ip.is_loopback:
  41. return
  42. self_network = ipaddress.ip_network(address.decode('ascii'))
  43. if WHITELIST:
  44. for wl_key in WHITELIST:
  45. wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
  46. if wl_net.overlaps(self_network):
  47. log['time'] = int(round(time.time()))
  48. log['priority'] = "info"
  49. log['message'] = "Address %s is whitelisted by rule %s" % (self_network, wl_net)
  50. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  51. print "Address %s is whitelisted by rule %s" % (self_network, wl_net)
  52. return
  53. net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
  54. net = str(net)
  55. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  56. bans[net] = { 'attempts': 0 }
  57. active_window = RETRY_WINDOW
  58. else:
  59. active_window = time.time() - bans[net]['last_attempt']
  60. bans[net]['attempts'] += 1
  61. bans[net]['last_attempt'] = time.time()
  62. active_window = time.time() - bans[net]['last_attempt']
  63. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  64. log['time'] = int(round(time.time()))
  65. log['priority'] = "crit"
  66. log['message'] = "Banning %s" % net
  67. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  68. print "Banning %s for %d minutes" % (net, BAN_TIME / 60)
  69. if type(ip) is ipaddress.IPv4Address:
  70. subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  71. subprocess.call(["iptables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  72. else:
  73. subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  74. subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  75. r.hset("F2B_ACTIVE_BANS", "%s" % net, log['time'] + BAN_TIME)
  76. else:
  77. log['time'] = int(round(time.time()))
  78. log['priority'] = "warn"
  79. log['message'] = "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  80. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  81. print "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  82. def unban(net):
  83. log['time'] = int(round(time.time()))
  84. log['priority'] = "info"
  85. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  86. if not net in bans:
  87. log['message'] = "%s is not banned, skipping unban and deleting from queue (if any)" % net
  88. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  89. print "%s is not banned, skipping unban and deleting from queue (if any)" % net
  90. r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
  91. return
  92. log['message'] = "Unbanning %s" % net
  93. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  94. print "Unbanning %s" % net
  95. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  96. subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  97. subprocess.call(["iptables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  98. else:
  99. subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  100. subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  101. r.hdel("F2B_ACTIVE_BANS", "%s" % net)
  102. r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
  103. del bans[net]
  104. def quit(signum, frame):
  105. global quit_now
  106. quit_now = True
  107. def clear():
  108. log['time'] = int(round(time.time()))
  109. log['priority'] = "info"
  110. log['message'] = "Clearing all bans"
  111. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  112. print "Clearing all bans"
  113. for net in bans.copy():
  114. unban(net)
  115. def watch(container):
  116. log['time'] = int(round(time.time()))
  117. log['priority'] = "info"
  118. log['message'] = "Watching %s" % container
  119. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  120. print "Watching", container
  121. client = docker.from_env()
  122. for msg in client.containers.get(container).attach(stream=True, logs=False):
  123. result = re.search(RULES[container], msg)
  124. if result:
  125. addr = result.group(1)
  126. ban(addr)
  127. def autopurge():
  128. while not quit_now:
  129. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  130. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  131. QUEUE_UNBAN = r.hgetall("F2B_QUEUE_UNBAN")
  132. if QUEUE_UNBAN:
  133. for net in QUEUE_UNBAN:
  134. unban(str(net))
  135. for net in bans.copy():
  136. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  137. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  138. unban(net)
  139. time.sleep(30)
  140. if __name__ == '__main__':
  141. threads = []
  142. for container in RULES:
  143. threads.append(Thread(target=watch, args=(container,)))
  144. threads[-1].daemon = True
  145. threads[-1].start()
  146. autopurge_thread = Thread(target=autopurge)
  147. autopurge_thread.daemon = True
  148. autopurge_thread.start()
  149. signal.signal(signal.SIGTERM, quit)
  150. atexit.register(clear)
  151. while not quit_now:
  152. for thread in threads:
  153. if not thread.isAlive():
  154. break
  155. time.sleep(0.1)
  156. clear()