ブログ ELKスタック導入 -完(?)-

프로필

2025年03月05日

462 0

前回述べた通り、JSONパースエラーが発生していたのは単なるパースの失敗ではなく、Kibanaへのログ送信自体が行われていなかったという事実に気がついた。再確認してみると、FastAPI内で生成されたログがLogstashに送られていなかったのが原因だった。ログを送信する方法はいくつかあるが、今回はTCPソケットを使ってLogstashに送信するハンドラーを追加した。

TCPを利用したログ送信

class TCPLogstashHandler(logging.Handler):
    def __init__(self, host, port):
        super().__init__()
        self.host = host
        self.port = port
        self.sock = None
        self._connect()

    def _connect(self):
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.connect((self.host, self.port))
        except (socket.error, socket.timeout) as e:
            self.sock = None
            print(f"Logstash接続失敗: {e}")

    def emit(self, record):
        try:
            if self.sock is None:
                self._connect()

            if self.sock is not None:
                msg = self.format(record) + '\n'
                self.sock.sendall(msg.encode('utf-8'))
        except (BrokenPipeError, socket.error) as e:
            self.sock = None
            print(f"ログ送信失敗: {e}")

それでもJSONパースエラーは残っており、Logstashパイプラインのコーデックを明示的にjson_linesに設定した。

input {
  tcp {
    port => 5044
    codec => json_lines {
      charset => "UTF-8"
    }
  }
}

filter {
  # フィルターロジック...
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "fastapi-logs-%{+YYYY.MM.dd}"
  }
  stdout { codec => rubydebug }
}

この設定後、KibanaでFastAPIのログは正常に収集されていたが、ログの時刻がUTCで表示されるという問題があった。これはELKスタックのデフォルト設定がUTCであるためで、Kibanaとホストの設定をKST基準に変更することで解決した。

Kibanaの設定場所: Stack Management > Advanced Settingsの Timezone for date formatting を Asia/Seoul に設定

タイムゾーン設定を終え、試験のために2日ほど後にKibanaでログを確認すると、完全にカオスだった。
存在しないパスを探索して不要な404ログを大量に出力していた。

最初に行った対処は、ボットに余地を与えないように404ではなく444を返す設定だった。

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;

    ssl_certificate /etc/nginx/ssl/default.crt;
    ssl_certificate_key /etc/nginx/ssl/default.key;

    return 444;
}

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}


server {
    listen 80;
    listen [::]:80;
    server_name cliche.life;

    if ($host = cliche.life) {
        return 301 https://$host$request_uri;
    }

    return 444;
}

正規アクセスのみ許可し、IPやポート直打ちは404ではなく444で完全遮断。
444はTCP接続そのものを切断するため、ブラウザには「接続が切断されました」と表示され、多くのボットは応答なしと判断して再試行を行わない。

しかしそれだけでなく、.env, .git, wp-login, admin, .bak, config/ など、ありとあらゆる機密パスを手当たり次第に探っていた。

もちろん、そんな機密設定が外部公開されるような設定にはしていなかったが、やつらは本当に容赦がなかった。wp-loginやPHP系設定のように、このブログとは全く関係のないパスまで探索していて、ログが汚れまくるのをこれ以上見るに耐えられなかった私は、fail2banを導入することにした。

Fail2ban 導入

Fail2banとは?

Fail2banは侵入防止フレームワークで、サーバーをブルートフォース攻撃などから保護する。Pythonで書かれており、iptablesやTCPラッパーなどと連携してPOSIX準拠のシステム上で動作する。

今のログの酷さを見て、今の状況に最も適したフレームワークだと判断し、Fail2banの導入を決意した。

sudo apt install fail2ban

Ubuntu環境であればこのコマンドでインストールできる。

その後、カスタムフィルターを作成。私の場合は以下のようにKibanaログをCSVで抽出・分析し、来ていたリクエストを全てブロックする形にした

[Definition]
failregex = ^<HOST> .* "(?:GET|POST|HEAD) /(?:\.git/|\?XDEBUG|\?a=|\.env|wp-login|admin/|administrator/|xmlrpc\.php|config/|\.well-known/|console/|_ignition/|\.sql|\.bak|\.php\?|cgi-bin/|\.asp|myadmin|phpmyadmin|adminer|manager/|jenkins/|solr/|%00|%27|%20select%20|%20or%201=1|%20and%201=1|%20from%20|/etc/passwd).*" .*$
ignoreregex =

このようにログを見ることができる。


ログを見ると、この2~3日間でブロックされた数から、どれだけ多くのリクエストが来ていたのかがわかる。

ただしこのログを確認するには、ホスト上で

sudo tail -f /var/log/fail2ban.log

こうやって毎回確認しないといけなかった。
それならばいっそFail2banもELKスタックに統合して一括で管理しようと思った。(正直、「もうこれでいいんじゃないか?」とは思ったが)

というのも、そもそもELKスタックを導入した理由が「ホストでのログ確認が面倒だった」からで、fail2banのために再びターミナルを開いてログを見るのは意味がないと感じた。

ただし問題は、ELKスタックはDockerコンテナ上で動いていて、Fail2banはホスト側で動いているという点。
これを解決するためにFilebeatを使ってFail2banのログを別でLogstashに送ることにした。

この過程でも色々とトラブルがあった。
- Filebeatのポートを5044にすると、FastAPI用のLogstashとポートが衝突しLogstashがシャットダウン
- Filebeatを5045にしても、Docker側でそのポートが開いておらずログが送信されない
- タグ設定をしていなかったため、Fail2banのログがFastAPIのログに混在してしまった

それぞれの解決策は以下の通り

logstash:
    image: docker.elastic.co/logstash/logstash:8.12.0
    volumes:
      - ./elk/logstash/pipeline:/usr/share/logstash/pipeline
    ports:
      - "5044:5044"  # FastAPI
      - "5045:5045"  # Fail2Ban
    depends_on:
      - elasticsearch
    networks:
      - elk
output {
   if "fail2ban" not in [tags] {
    elasticsearch {
      hosts => ["elasticsearch:9200"]
      index => "fastapi-logs-%{+YYYY.MM.dd}"
    }
    stdout { codec => rubydebug }
  }
}

結論

このようにして、Fail2banとFastAPIのログをELKスタックで収集・可視化できるようになっただけでなく、セキュリティ面でも一歩進んだ設定が実現できた。
実際に確認してみると、思った以上に悪意あるリクエストが多く来ていたことがわかったし、開発→デプロイだけで終わりではなく、セキュリティ面にも気を配る必要があるということを改めて実感した。

#ELK #Elasticsearch #Logstash #Kibana #Fail2ban

コメント 0件

コメントを投稿するにはログインが必要です