セキュリティ

監査ログを設計する — 誰が・いつ・何をしたかを、後から追えるようにする

業務システムで問い合わせやトラブルが起きたとき、
「誰がいつ何をしたか」を答えられるかが対応の質を決めます。

何を記録するか

すべてを記録すると量が膨大になり、肝心なものが埋もれます。
優先度は次の順です。

必須
- 認証(ログイン成功・失敗・ログアウト)
- 権限の変更
- データの削除
- 金額に関わる変更
- 個人情報の閲覧・出力

あると良い
- レコードの作成・更新
- 設定変更
- 一括処理の実行

通常は不要
- 一覧画面の表示
- 検索

実装

class AuditLog(models.Model):
    actor = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    actor_label = models.CharField(max_length=120)   # 退会後も残す表示名
    action = models.CharField(max_length=40)          # create / update / delete / export
    target_model = models.CharField(max_length=60)
    target_id = models.CharField(max_length=40)
    changes = models.JSONField(null=True)             # 変更前後
    ip = models.GenericIPAddressField(null=True)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)

actor_label を別に持つのが要点です。
ユーザーが削除されても「誰が」が残ります
外部キーだけだと NULL になり、追跡できなくなります。

変更前後を記録する

def diff(before: dict, after: dict) -> dict:
    return {
        k: {"before": before.get(k), "after": after.get(k)}
        for k in set(before) | set(after)
        if before.get(k) != after.get(k)
    }

変わった項目だけを記録します。全項目を残すと量が跳ね上がります。

パスワードや個人情報は、値そのものではなく変更事実だけを残します。

MASKED = {"password", "card_number", "my_number"}
changes = {k: ("(変更)" if k in MASKED else v) for k, v in changes.items()}

記録漏れを防ぐ

各ビューで書くと必ず漏れます。シグナルで自動化します。

@receiver(post_delete)
def log_delete(sender, instance, **kwargs):
    if sender in AUDITED_MODELS:
        AuditLog.objects.create(
            actor=current_user(), action="delete",
            target_model=sender.__name__, target_id=str(instance.pk),
        )

シグナルからリクエスト情報を取るには、
ミドルウェアで現在のユーザーを保持する仕組みが必要です。

論理削除と組み合わせる

監査ログに「削除した」と残っていても、
中身が消えていれば何を削除したのか分かりません

class SoftDeleteModel(models.Model):
    deleted_at = models.DateTimeField(null=True, db_index=True)

    class Meta:
        abstract = True

論理削除にしてゴミ箱から復元できるようにすると、
誤操作の救済もできます。業務システムでは物理削除を避けるのが基本です。

見られる形にする

記録しても、検索できなければ使われません
管理画面から絞り込めるようにします。

・ユーザーで絞る
・期間で絞る
・対象レコードで絞る(この注文に何が起きたか)

「この1件の履歴」が時系列で見られると、問い合わせ対応が非常に速くなります。

保存期間と改ざん防止

  • 保存期間を決める(1年、3年など)
  • アプリから更新・削除できないようにする(追記のみ)
  • 古いものは別ストレージへ退避

監査ログを管理画面から編集できる状態だと、証跡としての価値がありません。

まとめ

  • 認証・権限変更・削除・金額・個人情報の閲覧は必須で記録
  • actor_label を別に持ち、ユーザー削除後も追える形に
  • 変更は差分のみ。秘密情報は値を残さない
  • シグナルで自動化して記録漏れを防ぐ
  • 論理削除と組み合わせる
  • 検索できる形にし、追記のみに制限する