システムログにおけるPIIのマスキング:Fluent Bitによるメール、IP、パスワードの秘匿化ガイド

Security tutorial - IT technology blog
Security tutorial - IT technology blog

なぜ転送元でのログマスキングが不可欠なのか

ログはデバッグに欠かせませんが、ユーザーのメールアドレス、IPアドレス、管理者の資格情報がELKやLokiスタックに漏洩した瞬間、大きなリスクへと変わります。私自身、あるマイクロサービスのデプロイによって生の認証トークンがダンプされ始め、午前3時に500GB以上のElasticsearchインデックスをクリーンアップする羽目になったことがあります。GDPRやSOC2などの枠組みでは、機密データが中央ログサーバーに到達した時点で、技術的にはデータ漏洩が発生したとみなされます。

これを処理する最も賢明な方法は、エッジ(転送元)で行うことです. ホストを離れる前にFluent Bit内で個人情報(PII)を秘匿化することで、平文の機密データがネットワークに流れるのを防ぐことができます。この戦略は攻撃対象領域を大幅に削減し、コンプライアンス監査担当者の不安を解消します。

クイックスタート(5分で実装)

Fluent Bitを稼働させている場合、modifyフィルタを使用して即座にマスキングを実装できます。アプリケーションログに、今すぐ削除する必要があるuser_passwordというフィールドが含まれていると仮定しましょう。

以下のフィルタブロックをfluent-bit.confに追加します:

[FILTER]
    Name    modify
    Match   *
    Set     password_status [MASKED]
    Rename  user_password plain_password
    Hard_set plain_password ********

この設定では、元のフィールド名を変更し、値をアスタリスクで上書きします。しかし、本番環境のログがこれほど整理されていることは稀です。機密性の高い文字列は、長くて乱雑なメッセージブロックの奥深くに埋もれているのが一般的です。そのような場合には、正規表現が非常に有効です。

ログ行のどこにでもあるIPv4アドレスをキャッチするには、次のように記述します:

[FILTER]
    Name    modify
    Match   *
    Regexp  log /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/ [IP_MASKED]

サービスを再起動すると、そのフィールド内のすべてのIPv4アドレスが[IP_MASKED]に置き換わります。ダッシュボードに内部インフラの詳細が漏洩する心配はもうありません。

ディープダイブ:Modify vs. Lua

Fluent Bitはデータをレコードのストリームとして処理します。PIIを効果的にクリーンアップするには、規模に応じてmodifyフィルタかluaフィルタのどちらかを選択する必要があります。modifyは単純なキーと値の入れ替えにおいて非常に高速ですが、入れ子状のデータや複数のパターンを扱う複雑なロジックにはluaフィルタが適しています。

Modifyフィルタ

C言語のコアに直接組み込まれているmodifyフィルタは、極めて高速です。最小限のCPUオーバーヘッドでレコードを操作します。AddSetRegexpなどのルールを使用して特定のフィールドを対象にできます。デバッグ用にドメインを残しつつメールアドレスをマスキングしたい場合(例:m***@company.com)も、キャプチャグループを使用したmodifyで対応可能です。

Luaフィルタ

一度に5種類のPIIをクリーンアップしたり、ネストされたJSONを解析したりする必要がある場合は、Luaに切り替えます。これにより、データを分析してクリーンにするための完全なスクリプト環境が手に入ります。パフォーマンスへの影響は、ほとんどのクラスターで無視できるレベル(通常CPU使用率が3%未満の増加)ですが、セキュリティ上のメリットは絶大です。

高度な活用法:ユニバーサル・スクラバー

最新のスタックの多くは、syslog、JSON、アクセスログが混在しています。多数のmodifyフィルタを積み重ねるよりも、単一のLuaスクリプトで様々なパターンを効率的に処理する方を私は好みます。

masking.luaという名前のファイルを作成します:

function mask_pii(tag, timestamp, record)
    local log = record["log"]
    if log == nil then return 0, timestamp, record end

    -- メールアドレスを秘匿化
    log = string.gsub(log, "[%w%.%-_]+@[%w%.%-_]+%.%a+", "[EMAIL_REDACTED]")
    
    -- IPv4アドレスを秘匿化
    log = string.gsub(log, "%d+%.%d+%.%d+%.%d+", "[IP_REDACTED]")

    -- JSON文字列内のパスワードを秘匿化
    log = string.gsub(log, '"password"%s*:%s*"[^"]+"', '"password":"********"')

    record["log"] = log
    return 2, timestamp, record
end

次に、メイン設定ファイルでこのスクリプトを呼び出します:

[FILTER]
    Name    lua
    Match   *
    script  masking.lua
    call    mask_pii

これにより、設定ファイルの可読性が保たれます。また、500行もある設定ファイル内を探し回るよりも、中央のスクリプト1つで正規表現パターンを更新する方がはるかに簡単です。

これらのルールの設定を始める前に、内部サービスの資格情報が強力であることを確認してください。私はサーバーのパスワードを生成するために、toolcraft.app/ja/tools/security/password-generator のブラウザベースのツールを使用しています。これは完全にクライアントサイドで動作するため、ルート資格情報がネットワークを流れることはなく、セットアップ段階で設定前のプロキシによってログに記録されるのを防ぐことができます。

本番運用のための実践的なヒント

1. 過剰なマスキングを避ける

すべてを秘匿化してはいけません。ログは問題を解決するために存在します。user_idや特定のrequest_idまでマスキングしてしまうと、カスタマーのエラーを追跡することが不可能になります。機密性が高いデータ、または厳格に規制されているデータのみを対象にしてください。

2. Stdoutで検証する

新しい正規表現をいきなり本番環境に投入してはいけません。まずstdout出力プラグインを使用して、ローカルでフィルタを検証してください。これにより、高価なストレージに保存される前に、ルールがどのように動作するかを正確に確認できます。

[OUTPUT]
    Name   stdout
    Match  *

3. 大規模環境でのパフォーマンス

毎秒10万件のイベントを処理する場合、ミリ秒単位の差が重要になります。トラフィックの多い環境では、8割のユースケース(IPマスキングなど)にはC言語ベースのmodifyフィルタを使用し、入れ子構造を含む複雑な2割のケースにLuaを使い分けるようにしましょう。

4. 正しいフィールドをターゲットにする

アプリケーションが構造化されたJSONを送信する場合、汎用的なlogフィールドに対する正規表現は失敗します。サブオブジェクト内に隠れたデータを見逃さないよう、フィルタがrecord["user"]["metadata"]["email"]のような特定のパスをターゲットにしているか確認してください。

セキュリティをパイプラインのエッジにシフトすることで、ユーザーのプライバシーを損なうことなく、開発者に力を与えるロギングシステムを構築できます。セキュリティとは単にハッカーをブロックすることだけではありません。自分たちのデータストリームが最大の負債にならないようにすることでもあるのです。

Share: