不正アクセスログ集計ツールその後

以前の記事で不正アクセスの疑いのあるIPアドレスを集計してアクセスブロックするツールを作成し運用していることと、docker導入に当たってブロックの仕組みを再検討して運用している記事を上げていましたが、もう一つポータルサイトのWEBアプリへの不正アクセス(django管理画面やwordpressへの不正ログイン)を集計する仕組みも紹介しました。実はこの仕組みに関してもdocker化で動作しなくなっていました。

データベースに保管された不正ログイン情報(Login_History)は別コンテナのデータベースサーバーにあるので、localhostからコンテナ名に変更するだけで良いのだが、ブロック済みのIPアドレスリストを取得して新規IPアドレスのみを表示する仕組みについては、ブロックリストがホストで管理されているためコンテナ内からはアクセス出来ない。ブロックリストを保管している/etc/ufw/before.rulesをコンテナに強引にマウントしてしまう方法もあるが、あまりスマートではない。

ということでこれまた興味本位の勉強を兼ねてホスト側からdockerブリッジネットワークへのサービスとしてソケット通信でブロックリストを公開する仕組みを作成した。

  • blocklist-server
#!/usr/bin/env python3
#--*-- coding: utf-8 --*--
# 対象ネットワーク: docker bridge network([shared] 192.168.29.0/24)
# ブロックリストホスト: 192.168.29.1
# 起動(su): /usr/local/sbin/blocklistsrv.py | /usr/bin/logger -i -t blocklistsrv 2>&1 &
# 停止: ps aux | grep blocklistsrv  -> kill -9
# ポート確認: lsof -i :8765  -> kill -9

import os, sys, socket
import psutil
import argparse
import re
import numpy as np

import daemon
from daemon import pidfile

pfile = '/var/run/blocklist-server.pid'

ipset_ufw = '/etc/ufw/before.rules'
ipset_start_line = 'blacklist-ipset-S'
ipset_end_line = 'blacklist-ipset-E'

svport = 8765
svhost = '192.168.29.1'

def getArgs():
    parser = argparse.ArgumentParser('blocklist service cmd')
    parser.add_argument('cmd', nargs='?', help='(start|stop|restart)')
    parser.add_argument('-l', '--syslog', action='store_true', help='enable syslog output')
    return parser.parse_args()

args = getArgs()

ーーー省略ーーー
def blserver():
    socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socket1.bind((svhost, svport))
    socket1.listen(5)

    logger.info('Start Blocklist server. port : %d', (svport))
    while True:
        try:
            clientsocket, address = socket1.accept()
            blist = []
            logger.info(f"Connection from {address} has been established!")
            with open(ipset_ufw, 'r', encoding='utf-8') as fi:
                for line in fi:
                    if re.search(ipset_start_line, line):
                        break
                for line in fi:
                    if res:= re.match('^-A ufw-before-input -s ([0-9./]+) .*', line):
                        #logger.debug(res[1])
                        blist.append(res[1])
                    if re.search(ipset_end_line, line):
                        break
        #except KeyboardInterrupt:
        #    #clientsocket.close()
        #    exit('exit with keyboard interrupt!')
        except FileNotFoundError:
            logger.error('file not found! : %s' % (ipset_ufw))
        except PermissionError:
            logger.error('can\'t read %s, please run as superuser!' % (ipset_ufw))
        finally:
            clientsocket.send('\n'.join(blist).encode('utf-8'))
            clientsocket.close()

        ## blist 送信 

def main():
    #args = getArgs()
    #logger = getLoggerConf(args.syslog)
    if args.cmd == 'start':
        pass
    elif args.cmd == 'stop' or args.cmd == 'restart':
        # デーモンストップ
        if os.path.exists(pfile):
            try:
                logger.info(pfile + " is exits")
                pid =  open(pfile, 'r').read().rstrip()
                proc = psutil.Process(int(pid))
                proc.terminate()
                os.remove(pfile)
                logger.info('pid : %s removed.' % pid)
            except PermissionError:
                logger.error('PID File, permission error, please run as super user.')
                exit(0)
            #except:
            #    logger.error('some error occured.')
        if args.cmd == 'stop':
            exit(0)
    else:
        logger.error('operation command is one of (start|stop|restart)')
        exit(1)

    # デーモン起動
    with daemon.DaemonContext(pidfile=pidfile.TimeoutPIDLockFile(pfile)) as context:
        logger.info(context.is_open)
        blserver()


    #exit(0)



if __name__ == '__main__':
    main()


start, stop, restart機能を持つデーモンスクリプトを作成し、ubuntuホストへサービス登録

  • /usr/lib/systemd/system/blocklist.service
[Unit]
Description=blocklistsrv: this is service of ufw blocking IP address list.
After=docker.service

[Service]
ExecStart=/usr/local/sbin/blocklist-service -l start
ExecStop=/usr/local/sbin/blocklist-service -l stop
ExecReload=/usr/local/sbin/blocklist-service -l restart
Restart=no
Type=simple

[Install]
WantedBy=multi-user.target

ここでsudo systemctl start blocklistでサービスが起動される。ちなみに恒久的に起動するようにするにはsudo systemctl emable blocklistとする。

クライアントであるdocker内のdjangoのコードも下記のようにソケット対応に変更。

blocklist_socket_server = ('192.168.29.1', 8765) 

def get_blocklist():                                                                                                    
    """                                                                                                                 
    root権限でブロックリスト取得コマンドを走らせて標準入力からブロックアドレスのリストを返す                            
    """                                                                                                                 
    socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)                                                         
    socket1.connect(blocklist_socket_server)                                                                            
    bipset = [ ipv4adrset(ips) for ips in socket1.recv(4096).decode('utf-8').split('\n') ]                              
    return bipset                                                                                                       

とりあえず動作するようになったので一安心。

名前付きなUNIXソケットでも良かったけど、またソケットファイルをコンテナにマウント〜となってしまうので、dockerネットワークのゲートウェイから見せるソケットサーバーとしました。こんな感じで良いのだろうか。まぁ特に重要なサービスでもないので結果オーライで運用中です。