logwatch.py 7.0 KB

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