← 기사 목록
日本語https://zenn.dev/topics/ai/feed

ローカルAI Gatewayに監査ログを実装しました

추출된 키워드

20
監査ログ·5ローカルAI Gateway·5機密情報の検出と制御·4JSON Lines形式·4mutex·3排他制御·3net/http·3Go·3gpt-4o-mini·3openai·3Dockerコンテナ環境·3JSONファイル·3grep·2jq·2SSK·2ISO 8601·2stdout·2findings·2blocked·2forwarded·2

원문

5,653
ローカルAI Gatewayに監査ログを実装しました

ローカルAI Gatewayに監査ログを実装しました

前回の記事では、ローカルAI Gatewayに機密情報の検出と制御を実装しました。

https://zenn.dev/hisa_tech_2973/articles/a8a982bc044f5f

今回は、そのゲートウェイに

監査ログ

を追加実装した内容を紹介します。

なぜ監査ログが必要なのか

機密情報の検出と制御を実装しても、
インシデントに気づき、追跡し、対応するためには、

「何が起きたか」を後から確認できる状態にすること

が必要です。

例えば:

  • どのリクエストがブロックされたか
  • どの機密情報が検出されたか
  • どのクライアントから送られたか

これらを確認できない状態では、インシデントが起きても対応できません。

問題は「後から確認できないこと」

前回実装したログは、標準出力(stdout)へ出力されます。
Dockerコンテナ環境では、標準出力のみに出力している場合、

コンテナを再起動するとログを追跡しにくくなることがあります。

また、ログはリクエストとレスポンスが別々のエントリとして出力されるため、

「このリクエストで何が検出されたか」を一目で把握しにくい

という問題があります。

実装したこと

各リクエストの情報を、1行1エントリのJSONファイルとして記録する監査ログを実装しました。

実装したのは次の3つです。

  • 全リクエストの情報を構造化してファイルに書き出す
  • 検出結果(findings)を同じエントリに含める
  • リクエストごとにaction(blocked / forwarded)を記録する

アーキテクチャ

検出結果に関わらず、

すべてのリクエストを監査ログに記録します。

監査ログの内容

各エントリには次のフィールドが含まれます。

フィールド内容
request_id
リクエストの一意なID
timestamp
リクエスト受信時刻(ISO 8601)
client_ip
送信元IPアドレス
provider
転送先プロバイダ(例:
openai
model
使用モデル
path
リクエストのパス
action
blocked
または
forwarded
forwarded
実際に転送されたかどうか(bool)
findings
検出された機密情報の一覧

findings
は前回実装した検出器の出力をそのまま含めるため、

どのリクエストで何が検出されてブロックされたか

を1エントリで確認できます。

ログの出力例

通過したリクエスト(forwarded)

{
  "request_id": "dd236fa325819cbf",
  "timestamp": "2026-05-22T01:14:47.943348295Z",
  "client_ip": "172.20.0.1",
  "provider": "openai",
  "model": "gpt-4o-mini",
  "path": "/v1/gateway/chat",
  "action": "forwarded",
  "forwarded": true,
  "findings": []
}

ブロックされたリクエスト(blocked)

{
  "request_id": "53e754c87ccdb9c2",
  "timestamp": "2026-05-22T01:16:21.407290077Z",
  "client_ip": "172.20.0.1",
  "provider": "openai",
  "model": "gpt-4o-mini",
  "path": "/v1/gateway/chat",
  "action": "blocked",
  "forwarded": false,
  "findings": [
    {
      "type": "openai_api_key",
      "confidence": "high",
      "matched": "sk-a***",
      "reason": ["regex_match"],
      "action": "block"
    }
  ]
}

findings
が空の場合は検出なし、要素がある場合は検出内容と対応するアクションが記録されます。

実装で意識したこと

スレッドセーフな書き込み

Goのnet/httpはリクエストを並行処理します。
同時に複数のリクエストが来た場合でも書き込みが壊れないよう、

mutexで排他制御しています。

type FileWriter struct {
    mu   sync.Mutex
    file *os.File
}

func (w *FileWriter) Write(entry AuditLog) error {
    line, err := json.Marshal(entry)
    if err != nil {
        return err
    }
    w.mu.Lock()
    defer w.mu.Unlock()
    _, err = w.file.Write(append(line, '\n'))
    return err
}

ファイルのパーミッション

監査ログファイルは

0600
(オーナーのみ読み書き可)で作成します。
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)

ログファイル自体が情報漏洩の経路にならないための、最低限の措置です。

JSON Lines形式

1リクエスト1行のJSON Lines形式で出力します。

// 実際の実装ではエラーを適切に処理しています。
line, _ := json.Marshal(entry)
w.file.Write(append(line, '\n'))

grep
jq
でそのまま検索・抽出できるため、

インシデント時の調査がしやすい形式を選びました。

例えば、ブロックされたリクエストだけを抽出する場合:

grep '"action":"blocked"' logs/audit.log | jq .

SSKの設計パターンとの対応

今回の実装は、SSKにおける次の設計パターンに対応しています。

検出と制御だけでなく、

「何が起きたかを後から確認できる状態にする」

ことが、インシデント対応の前提条件です。

まとめ

  • 何が起きたかを後から追跡可能にすることが、インシデント対応の前提条件
  • 監査ログとして、検出結果を含む構造化エントリをファイルに記録する
  • action(blocked / forwarded)で結果を一目で確認できる
  • スレッドセーフな書き込みとファイルパーミッションを意識した
  • JSON Lines形式にすることで、インシデント時の調査をしやすくした