logwatch.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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. client = docker.from_env()
  20. for container in client.containers.list():
  21. if "postfix-mailcow" in container.name:
  22. postfix_container = container.name
  23. elif "dovecot-mailcow" in container.name:
  24. dovecot_container = container.name
  25. elif "sogo-mailcow" in container.name:
  26. sogo_container = container.name
  27. elif "php-fpm-mailcow" in container.name:
  28. php_fpm_container = container.name
  29. RULES = {}
  30. RULES[postfix_container] = {}
  31. RULES[dovecot_container] = {}
  32. RULES[sogo_container] = {}
  33. RULES[php_fpm_container] = {}
  34. RULES[postfix_container][1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed'
  35. RULES[dovecot_container][1] = '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),'
  36. RULES[sogo_container][1] = 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked'
  37. RULES[php_fpm_container][1] = 'Mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)'
  38. r.setnx("F2B_BAN_TIME", "1800")
  39. r.setnx("F2B_MAX_ATTEMPTS", "10")
  40. r.setnx("F2B_RETRY_WINDOW", "600")
  41. bans = {}
  42. log = {}
  43. quit_now = False
  44. def ban(address):
  45. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  46. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  47. RETRY_WINDOW = int(r.get("F2B_RETRY_WINDOW"))
  48. WHITELIST = r.hgetall("F2B_WHITELIST")
  49. ip = ipaddress.ip_address(address.decode('ascii'))
  50. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  51. ip = ip.ipv4_mapped
  52. address = str(ip)
  53. if ip.is_private or ip.is_loopback:
  54. return
  55. self_network = ipaddress.ip_network(address.decode('ascii'))
  56. if WHITELIST:
  57. for wl_key in WHITELIST:
  58. wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
  59. if wl_net.overlaps(self_network):
  60. log['time'] = int(round(time.time()))
  61. log['priority'] = "info"
  62. log['message'] = "Address %s is whitelisted by rule %s" % (self_network, wl_net)
  63. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  64. print "Address %s is whitelisted by rule %s" % (self_network, wl_net)
  65. return
  66. net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
  67. net = str(net)
  68. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  69. bans[net] = { 'attempts': 0 }
  70. active_window = RETRY_WINDOW
  71. else:
  72. active_window = time.time() - bans[net]['last_attempt']
  73. bans[net]['attempts'] += 1
  74. bans[net]['last_attempt'] = time.time()
  75. active_window = time.time() - bans[net]['last_attempt']
  76. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  77. log['time'] = int(round(time.time()))
  78. log['priority'] = "crit"
  79. log['message'] = "Banning %s" % net
  80. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  81. print "Banning %s for %d minutes" % (net, BAN_TIME / 60)
  82. if type(ip) is ipaddress.IPv4Address:
  83. subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  84. subprocess.call(["iptables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  85. else:
  86. subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  87. subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  88. r.hset("F2B_ACTIVE_BANS", "%s" % net, log['time'] + BAN_TIME)
  89. else:
  90. log['time'] = int(round(time.time()))
  91. log['priority'] = "warn"
  92. log['message'] = "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  93. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  94. print "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  95. def unban(net):
  96. log['time'] = int(round(time.time()))
  97. log['priority'] = "info"
  98. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  99. if not net in bans:
  100. log['message'] = "%s is not banned, skipping unban and deleting from queue (if any)" % net
  101. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  102. print "%s is not banned, skipping unban and deleting from queue (if any)" % net
  103. r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
  104. return
  105. log['message'] = "Unbanning %s" % net
  106. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  107. print "Unbanning %s" % net
  108. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  109. subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  110. subprocess.call(["iptables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  111. else:
  112. subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  113. subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  114. r.hdel("F2B_ACTIVE_BANS", "%s" % net)
  115. r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
  116. del bans[net]
  117. def quit(signum, frame):
  118. global quit_now
  119. quit_now = True
  120. def clear():
  121. log['time'] = int(round(time.time()))
  122. log['priority'] = "info"
  123. log['message'] = "Clearing all bans"
  124. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  125. print "Clearing all bans"
  126. for net in bans.copy():
  127. unban(net)
  128. def watch(container):
  129. log['time'] = int(round(time.time()))
  130. log['priority'] = "info"
  131. log['message'] = "Watching %s" % container
  132. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  133. print "Watching", container
  134. for msg in client.containers.get(container).attach(stream=True, logs=False):
  135. for rule_id, rule_regex in RULES[container].iteritems():
  136. result = re.search(rule_regex, msg)
  137. if result:
  138. addr = result.group(1)
  139. print "%s matched rule id %d in %s" % (addr, rule_id, container)
  140. log['time'] = int(round(time.time()))
  141. log['priority'] = "warn"
  142. log['message'] = "%s matched rule id %d in %s" % (addr, rule_id, container)
  143. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  144. ban(addr)
  145. def autopurge():
  146. while not quit_now:
  147. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  148. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  149. QUEUE_UNBAN = r.hgetall("F2B_QUEUE_UNBAN")
  150. if QUEUE_UNBAN:
  151. for net in QUEUE_UNBAN:
  152. unban(str(net))
  153. for net in bans.copy():
  154. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  155. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  156. unban(net)
  157. time.sleep(30)
  158. if __name__ == '__main__':
  159. threads = []
  160. for container in RULES:
  161. threads.append(Thread(target=watch, args=(container,)))
  162. threads[-1].daemon = True
  163. threads[-1].start()
  164. autopurge_thread = Thread(target=autopurge)
  165. autopurge_thread.daemon = True
  166. autopurge_thread.start()
  167. signal.signal(signal.SIGTERM, quit)
  168. atexit.register(clear)
  169. while not quit_now:
  170. for thread in threads:
  171. if not thread.isAlive():
  172. break
  173. time.sleep(0.1)
  174. clear()