1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

平文パスワードはログに残り、侵入は痕跡すら残らない ── ログは"消す/必ず残す"を設計する

1
Posted at

はじめに ── 「念のため全部出す」ログが、パスワードを残して侵入を消す

以前の私は、ログを「念のため全部出しておけば、いざという時に役立つ」と本気で思っていました。

ログインや決済のコントローラでは、たとえばこんな1行を書いていました(当時は Laravel を使い始めたばかりでした)。

// ログイン・決済コントローラ内(当時のイメージ)
Log::info($request->all());   // ← リクエストを丸ごとログへ。何を残し、何を消すかは未設計

$request->all() はリクエストの中身を丸ごと配列で返します。それをそのままログに流していました。「多く出しておくほど、あとで困らない」くらいの認識だったのです。

そんな私が、ある勉強会でこのログの中身を見せられました。CloudWatch Logs に残っていたのは、ログインと決済を1回ずつ通しただけなのに、平文のパスワードと、カード番号が、そのまま残っているレコードでした。ログの閲覧権限を持つ全員に、それが見えていたのです。

さらに悪いことに、逆側は空っぽでした。同じアプリに総当たり(ブルートフォース)を仕掛けられても、肝心の攻撃の痕跡は、どこにも残っていなかったのです。認証失敗を記録していなかったので、あとから「誰が・いつ・何回失敗したか」を追えませんでした。

残ってほしくないものが残り、残ってほしいものが残っていない。この非対称に、当時の私は気づいていませんでした。ログは「書けば安全」なものではなく、「何を消し、何を必ず残すか」を目的から設計する対象だったのです。

この記事は、そんなちょっと前の私に向けて書きます。

なお本記事は、社内で開催しているセキュリティ勉強会(全6回)の第6回「セキュアログ設計+インシデント追跡」であり、シリーズの最終回です。全体のロードマップは次のとおりで、本記事は右端(現在地)にあたります。

  • 第1回: 認証・認可(IDOR)
  • 第2回: インジェクション・SSRF
  • 第3回: XSS・CSRF・セッション
  • 第4回: 暗号・シークレット
  • 第5回: ネットワーク・WAF
  • 第6回: ログ・追跡(本記事=現在地・最終回)

実は第1回から第5回まで、「その脅威を"追う"話は第6回で」と、追跡の話をすべてこの最終回に送ってきました。この記事では、それを「何を残せば追えるか」という視点で回収します。当日の発表スライドも公開しているので、あわせてご覧ください。

想定読者:

  • Log::info($request->all()) のように、リクエストを丸ごとログに出している(出していた)Web アプリ開発者
  • 機微データ(パスワード・クレジットカード・PII)がログに残るリスクを意識していない人
  • 認証失敗を「必ず」残さないと侵入に気づけない、という感覚が薄い人
  • CloudWatch Logs は使っているが、保持・改ざん防止・Logs Insights での追跡までは設計していない人

この記事のゴールは、ログを目的から設計し、侵入を追える状態にすることです。機微データがログに漏れる様子を実演で体感し、「消す・残す・守る・追う」の設計をフレームワーク(Laravel / FuelPHP)と AWS の具体で見ていきます。

先に結論(この記事で伝えたい3点)

細かい説明に入る前に、いちばん伝えたいことを3つ先に出します。

  1. ログは「目的」から設計する。「先に全部出す」ではなく、「ログで何をしたいか」から要件を決めます。
  2. 機微データ(パスワード・クレカ・PII)はログに残さない。ログは本番データより保護が薄いことが多く、書けばそこが新たな漏洩源になります。
  3. 攻撃・認証失敗の痕跡は「必ず」残す ── 追跡できないと侵入に気づけない。残していなければ、侵入は起きても「起きたことすら分からない」状態になります。

この3点を、以降の本文でほどいていきます。そして記事の最後(Take-away)でも、この3つを一字一句そのまま再掲します。

この記事で扱うこと・扱わないこと

対象環境とバージョンは次のとおりです。

  • Laravel 13.x(2026年時点の現行安定版。内部は Monolog 3)
  • FuelPHP 1.x(レガシー。Monolog ベース)
  • AWS: CloudWatch Logs / CloudWatch Logs Insights / メトリクスフィルタ・サブスクリプションフィルタ / S3 Object Lock / CloudTrail / ECS FireLens・awslogs・CloudWatch Agent
  • OWASP / CWE / 標準: OWASP Logging Cheat Sheet・Logging Vocabulary Cheat Sheet・Top 10 A09、CWE-532 / CWE-778、RFC 5424 / RFC 3339 / W3C Trace Context、PCI DSS の PAN 表示要件
  • 言語は PHP

範囲を絞るため、次の項目は本記事では扱いません。いずれも重要ですが、1記事に詰め込むと焦点がぼやけます。

  • パスワードのハッシュ化・暗号化やシークレット管理(Secrets Manager・.env)そのものは扱いません(第4回で扱いました。本記事では「その原則をログに拡張する」だけを扱います)。
  • WAF・VPC フローログなど境界側のログの取り方は扱いません(第5回で扱いました。本記事では「それを同じ集約先で相関する」側を扱います)。
  • 組織的なインシデント対応(IR)・CSIRT の深掘りは扱いません(記事末で別シリーズの復習ログへ誘導します)。

ここでは「アプリが何を・どう書くか(設計)」と「残したログでどう追うか(追跡)」の2点に絞ります。

なぜログか ── 何を記録し、何を書いてはいけないか

まず土台を揃えます。ここが揃っていないと、あとの設計もデモも宙に浮きます。

ログの目的は3つ ── 目的から要件を決める

そもそもログは何のために取るのでしょうか。目的は大きく3つに整理できます。

  • 抑止: ログを取っていると示すことで、攻撃者の意欲を下げます。
  • 検証/解析: 侵害後に「何が・いつ・どのように起きたか」を特定します。
  • 原因究明: それをもとに再発を防ぎます。

CCT の教材(Module 18)でも、ログの必要性としてインシデントの特定・ポリシー違反の監視・不正行為の特定・長期的な問題の特定・ベースライン設定・法令遵守が挙げられています。ログは通常の操作では変更できない、時系列の永久的な記録源です(出典: CCT eBook 書籍2183-2184/PDF2194-2195)。

ここで大事なのは、ロギング要件を**「先に全部出す」ではなく「ログで何をしたいか」という目的から決める**ことです。効果的なロギングには、イベントの正規化・集計や、相関のための全ソースのタイムスタンプ同期が要ります(出典: CCT eBook 書籍2185/PDF2196)。「多く出せば役立つ」という発想が、そもそもの出発点から間違っていたわけです。

アプリは「書く」だけ、集約はインフラ層(12-factor)

次に、ログをどこに・どう出すかの責務分担です。ここは分けて考えるのが肝心です。

The Twelve-Factor App は、ログについてこう述べています(出典: https://12factor.net/logs)。

twelve-factor アプリは、自身の出力ストリームのルーティングや保管に一切関与しない。
各プロセスはイベントストリームをバッファせず stdout に書く。
本番では、各プロセスのストリームは実行環境が捕捉し、まとめて最終的な宛先へルーティングする。

つまり、アプリ(Laravel / FuelPHP)がやるのは、構造化したログを標準出力(stdout)に書くことだけです。どこへ送り、どう保管するかには関与しません。集約はインフラ層の仕事で、ECS の FireLens、awslogs ドライバ、CloudWatch Agent といった手段が担い、CloudWatch Logs にまとめます(出典: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_firelens.html)。

このように中央のログ収集装置へ集めること(集中ロギング)は、それ自体が改ざん対策になり、証拠のバックアップ源としても機能します(出典: CCT eBook 書籍2184-2189/PDF2195-2200)。

補足として、CloudWatch Logs の基本単位も押さえておきます。ログイベントは活動の1レコード(タイムスタンプ+生メッセージ)、ログストリームは同一ソースのログイベントの並び、ロググループは保持・監視・アクセス制御を共有するログストリームの集合です(出典: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatchLogsConcepts.html)。「アプリは何を書くかに集中し、どこへ送り・どう保管するかはインフラの仕事」──この記事の本編は、前者に重心を置きます。

「必ず残す」ものを、標準イベント名と重大度で

では、何を必ず残すのでしょうか。OWASP の Logging Cheat Sheet は、常にログすべきものとして、入力検証失敗・認証の成功/失敗・認可(アクセス制御)失敗・セッション管理失敗・高リスク機能(管理者権限の使用)・機微データへのアクセス・暗号鍵の使用などを挙げています(出典: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)。

ここで重要なのは、イベント名を自己流にせず、標準の語彙で揃えることです。OWASP の Logging Vocabulary Cheat Sheet は、標準イベント名を定めています(出典: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html)。

必ず残すイベント 標準イベント名 重大度
認証成功 / 失敗 authn_login_success / authn_login_fail INFO / WARN
認可(アクセス制御)失敗 authz_fail CRITICAL
入力検証失敗 OWASP: 常時ログ対象 WARN
重要操作(管理者 / 機微アクセス / 鍵) authz_admin 重要度に応じる

重大度は RFC 5424 の8段階(0 Emergency から 7 Debug まで)で、数字が小さいほど深刻です(出典: https://www.rfc-editor.org/rfc/rfc5424)。CCT の教材でも Linux ログの重大度はレベル0〜7の8段階で、最も高いのがレベル0とされ、これと一致します(出典: CCT eBook 書籍2214-2215/PDF2225-2226)。Laravel はこの8レベルを Monolog 経由で提供します(後述)。CCT も、すべての管理者アクセス・ルート権限者の全行為・ログへの全アクセスは残すべきとしています(出典: CCT eBook 書籍2187-2188/PDF2198-2199)。

ログに「書いてはいけない」もの(CWE-532)

逆に、絶対に残してはいけないものがあります。CCT は明確です。「パスワード、暗号化キー、銀行情報、クレジットカード情報、個人を特定できる情報(PII)、個人の健康情報、ソースコードなどの情報はログに残してはいけない」(出典: CCT eBook 書籍2187-2188/PDF2198-2199)。OWASP も、アプリケーションソースコード・セッション識別値・アクセストークン・認証パスワード・DB 接続文字列・暗号鍵・カード会員データなどを「ログに残すべきでない」データとして列挙しています(出典: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)。

なぜここまで厳しいのでしょうか。理由は、ログは本番データより保護が薄いことが多いからです。CWE-532「Insertion of Sensitive Information into Log File」は、機微情報をログに書くと、攻撃者に「機微情報を得るための、追加の・より保護の薄い経路」を与える、と述べています(出典: https://cwe.mitre.org/data/definitions/532.html)。

つまり「念のため全部出す」は、機微データを、より無防備な場所にわざわざ撒き散らす最悪の設計です。では実際に何が残るのか。次の章のデモで見てみましょう。

【実演】機微データがログに漏れる

ここが山場です。以下は説明用のフィクスチャ(架空のデータ)で、実在の CloudWatch 管理画面を再現したものではありません。カード番号は業界で周知のテスト用 Visa 番号であり、実在するカードではありません

デモ: ログイン+決済を1回通すだけ

やることはシンプルです。冒頭の Log::info($request->all()) が入ったアプリに、ログインをメールとパスワードで1回、決済をカード番号で1回だけ通します。そして CloudWatch Logs を見ます。注目してほしいのは、ログに「何が」残るか、です。

平文パスワードとカード番号がログに残った(before)

デモ用のロググループ /ecs/app/production に残ったレコードは、次のとおりでした。

// アプリが「念のため」リクエストを丸ごとログ出力していた(ログイン+決済コントローラ内)
Log::info('checkout request', $request->all());   // ← 危険: 中身を無検査で出力

// → CloudWatch Logs(ロググループ /ecs/app/production)に、こう残る:
{
  "level": "INFO",
  "message": "checkout request",
  "context": {
    "email": "taro@example.com",
    "password": "P@ssw0rd!",                 ← 平文パスワードが残る(CWE-532)
    "card_number": "4111111111111111",       ← カード番号(PAN)が残る ※テスト用番号(実在カードではない)
    "cvv": "123"                             ← 本来保存してはいけない値まで
  }
}
// ログ閲覧権限を持つ全員に見える。ログは"消えない置き場所"=新たな漏洩源

password に平文のパスワード、card_number にカード番号、そして本来どこにも保存してはいけない cvv まで、そのまま残っています。これは CWE-532 そのものです(出典: https://cwe.mitre.org/data/definitions/532.html)。値はダミーですが、構図は本物です。ログ自体が新しい漏洩源になった瞬間です。

対比: 消せば、ログに残らない(after)

同じリクエストでも、出力前に機微データを機械的に落とす処理(リダクション)を通すと、こう変わります。あわせて、後述の「残す設計」に合わせてメッセージも構造化し、message の自由文から event(標準イベント名)へ寄せています。注目してほしいのは値のマスクの方です。

{
  "level": "INFO",
  "event": "checkout",
  "context": {
    "email": "taro@example.com",
    "password": "****",                      ← 全マスク(値は一切残さない=OWASP 除外)
    "card_number": "411111******1111",       ← PCI DSS: 先頭6桁+末尾4桁のみ
    "request_id": "018f2a...-uuid"           ← 相関ID(Log::withContext)
  }
}
// cvv は保存要素ではない=そもそも出力しない

password は全マスクで、値を一切残しません(OWASP の除外リストどおり)。card_number は先頭6桁と末尾4桁だけを残し、あいだをマスクします。これは PCI DSS の PAN 表示要件(先頭6桁+末尾4桁までを表示可能な上限とし、それ以外はマスクする)に沿った形です(出典: PCI Security Standards Council https://www.pcisecuritystandards.org/)。cvv はそもそも保存する要素ではないので出力しません。かわりに相関ID(request_id)を付けています。「消せば、ログに残らない」──実装の全文は後の「消す設計」で示します。

第4回の「平文で機微データを置かない」が、ログにも及ぶ

いま起きたことを、少し引いて捉えます。機微データを「置く場所」は、DB だけではありません。

第4回では、パスワードはハッシュ、秘密はマネージドなシークレット管理へ、という DB 側の原則を扱いました。今日の第6回は、同じ機微データがログや標準出力にも残りうる、という話です。第4回の「平文で機微データを置かない」を、ログにまで拡張します(ハッシュ・暗号・Secrets Manager の詳細は第4回に譲り、ここでは再解説しません)。

なお、クレジットカード(PAN)のマスキングは第4回では登場しなかった、第6回の新しい領域です。そして、冒頭で触れた「侵入の痕跡が残っていない」問題(総当たりの痕跡を残す話)は、後半の追跡の章で回収します。

何を消し、何を必ず残すか ── セキュアログ設計

デモで体感した怖さを、「消す設計・残す設計・守る設計」という言葉に翻訳していきます。

消す設計 ── リダクション+除外リスト

まず消す設計です。やることは、ログを出力する前に、機微なキーを機械的にマスクする(リダクション)ことです。除外リストにパスワード・トークン・カード番号などを並べておき、そこに当たるキーを落とします。

Laravel 13.x の内部は Monolog 3 です。Monolog 3 の processor は Monolog\LogRecord(値オブジェクト)を受け取り LogRecord を返します(旧 Monolog 2 の array ではありません)。LogRecordcontext は readonly なので、加工するときは複製して $record->with(context: ...) で不変更新します(出典: https://github.com/Seldaek/monolog/blob/main/src/Monolog/LogRecord.phphttps://github.com/Seldaek/monolog/blob/main/doc/01-usage.md)。

// app/Logging/RedactSensitive.php — Monolog 3 processor(機微データを出力前にマスク)
// Monolog 3: processor は LogRecord を受け取り LogRecord を返す。context は readonly なので with() で不変更新する
use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;

class RedactSensitive implements ProcessorInterface
{
    // ログに残してはいけないキー(OWASP 除外リスト)
    private array $mask = ['password', 'pwd', 'secret', 'token', 'authorization', 'cvv'];

    public function __invoke(LogRecord $record): LogRecord
    {
        $context = $record->context;                         // readonly の context を複製して加工
        foreach ($context as $k => $v) {
            if (in_array(strtolower((string) $k), $this->mask, true)) {
                $context[$k] = '****';                       // 全マスク
            }
            if (strtolower((string) $k) === 'card_number' && is_string($v)) {
                // PCI DSS: 先頭6桁 + 末尾4桁のみ、中間はマスク
                $context[$k] = substr($v, 0, 6) . str_repeat('*', max(0, strlen($v) - 10)) . substr($v, -4);
            }
        }
        return $record->with(context: $context);             // 不変更新: 加工済み context で新しい LogRecord を返す
    }
}
// config/logging.php の該当チャンネルに 'processors' => [App\Logging\RedactSensitive::class]
// ※そもそも $request->all() を無検査で出さない。出すなら必ずリダクションを通す

この processor を通せば、たとえ $request->all() を出しても、password 等は **** に、card_number411111******1111 に落ちます(=先ほどの after)。ポイントは、$request->all() を無検査でそのまま出さないことです。

ひとつ注意点があります。消しすぎると、今度は追えなくなります。だから消すのは機微データだけにして、イベント自体はきちんと残します。

残す設計① ── 構造化+相関ID+UTC

残すなら、後で「追える」形で書きます。ポイントは3つです。

  1. 構造化ログ(JSON)で書く。機械で検索・集計できるからです。
  2. 相関ID(request_id)を全ログに付与する。1つのリクエストを縦断して追えます。Laravel なら Log::withContextLog::shareContext で付けられます(出典: https://laravel.com/docs/13.x/logging)。
  3. タイムスタンプは UTC に統一する。RFC 3339(=ISO 8601 のプロファイル)の Z 表記で書きます。時刻がずれていると、複数ソースのログを1本の線につなげられません(出典: https://www.rfc-editor.org/rfc/rfc3339)。

構造化ログの1レコードは、たとえばこうなります。

{
  "@timestamp": "2026-07-04T09:15:32Z",
  "level": "INFO",
  "event": "checkout",
  "userid": "taro@example.com",
  "request_id": "018f2a...-uuid"
}

相関の標準としては W3C Trace Context があり、traceparent ヘッダでリクエストをサービス横断に相関します(形式は version-trace-id-parent-id-trace-flags。例: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01)(出典: https://www.w3.org/TR/trace-context/)。AWS では ALB や X-Ray が同目的の X-Amzn-Trace-Id を付与しますが、その詳細形式はここでは断定しません(相関の実例としては W3C の traceparent を挙げるにとどめます)。CCT も、全システムを信頼できる時間ソースで同期し、正規化して相関せよと述べています(出典: CCT eBook 書籍2184-2189/PDF2195-2200)。

残す設計② ── 認証失敗を必ず記録する(CWE-778)

残す設計の2つ目、これが最も大切です。認証失敗を「必ず」記録することです。標準イベント名 authn_login_fail、重大度 WARN で記録します(連続失敗の上限には authn_login_fail_max という語彙もあります)。

// 相関ID を全ログに付与(ミドルウェア。Laravel 13.x)
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
// $requestId = (string) Str::uuid();
Log::shareContext(['request_id' => $requestId]);   // 以降の全チャンネルのログに付与

// 認証失敗は「必ず」標準イベント名+重大度で記録(OWASP 標準語彙)
Log::warning('authn_login_fail', [
    'event'   => 'authn_login_fail',       // 標準イベント名
    'userid'  => $email,                   // ※メールは PII。方針次第でハッシュ/一部マスク
    'ip'      => $request->ip(),
    // @timestamp は UTC・ISO 8601(RFC 3339)で出力する設定に揃える
]);
// ※ Laravel は Monolog で RFC 5424 の8レベル(emergency..debug)を提供

もしこれを記録していなければ、CWE-778「Insufficient Logging」に該当します。CWE-778 は、失敗ログインのようなセキュリティ上重要なイベントが記録されないと、悪意ある挙動の検知が困難になり、攻撃成功後のフォレンジック分析を妨げ、攻撃元の特定が困難または不可能になる、と述べています(出典: https://cwe.mitre.org/data/definitions/778.html)。CCT も、認証ログが総当たりを記録し、悪意ある試みの特定を支援するとしています(出典: CCT eBook 書籍2355-2356/PDF2366-2367)。

第1回の総当たり、第4回の認証失敗や鍵アクセスは、この1行の記録があって初めて追えます。その回収は、次の追跡の章でやります。

残したログを「守る」設計 ── WORM・整合性検証・保持

正しく残しても、あとで消されたり書き換えられたりしては証拠になりません。「必ず残す」には「消せない場所に守って残す」まで含まれます。

守る観点 手段 要点
改ざん防止 S3 Object Lock(WORM) 書いたら削除・上書き不可。Compliance(root でも削除不可)/ Governance / Legal hold
完全性検証 CloudTrail ログファイル整合性検証 SHA-256 でハッシュし、SHA-256 with RSA で digest に署名。改変/削除/偽造を検出
保持 CloudWatch Logs retentionInDays 1〜3653日(最大約10年)。未設定は無期限。目的から保持期間を決める
時刻・正規化 UTC・RFC 3339 / RFC 5424 severity Z 表記の UTC で統一(相関に必須)。severity は 0〜7(数字が小さいほど深刻)

S3 Object Lock は WORM(write-once-read-many)モデルで、オブジェクトを一定期間または無期限に削除・上書きできないようにします。Compliance モードなら root ユーザーを含む誰も削除・上書きできません(出典: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html)。CloudTrail のログファイル整合性検証は、「検出なしにログファイルを改変・削除・偽造することを計算的に不可能にする」機能です(出典: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-log-file-validation-intro.html)。

保持は CloudWatch Logs の retentionInDays で指定でき、指定可能値は 1〜3653 日(最大約10年)、設定しなければ無期限です(出典: https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutRetentionPolicy.html)。料金は取り込み・保管・分析の3構造ですが、具体的な単価は変動するため本記事では断定せず、公式の料金ページを参照してください(出典: https://aws.amazon.com/cloudwatch/pricing/)。

ここまでで「正しく書いて、安全に保管する」は完成しました。しかし、それだけでよいのでしょうか。残したログを誰かが見て追う仕組みがなければ、侵入は起きても誰も気づきません。正しく残した。では、それを「追える」のか。この問いを、次の章で受けます。

追跡できないと侵入に気づけない ── インシデント追跡

ここが記事の重心です。バラバラの機能紹介ではなく、「残したから追える」に収束する一本の追跡論として読んでください。前提として、自社は新規の Laravel とレガシーの FuelPHP を両方使っています。

Logs Insights で認証失敗を集計する(総当たりが1クエリで見える)

残した authn_login_fail を、CloudWatch Logs Insights で userid ごとに集計してみます。Logs Insights は CloudWatch Logs のデータを対話的に検索・分析するクエリ機能で、stats/count などで集計できます(出典: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AnalyzingLogData.html)。

# CloudWatch Logs Insights: 認証失敗イベントを userid ごとに集計
fields @timestamp, event, userid
| filter event = "authn_login_fail"
| stats count(*) as fails by userid
| sort fails desc

# 結果(直近1時間):
#   userid                fails
#   hanako@example.com      532    ← 1アカウントに532回の失敗 = 総当たり(ブルートフォース)
#   taro@example.com          3
#   sato@example.com          2
# → authn_login_fail を"必ず残していた"から、総当たりが1クエリで見えた(残さなければ痕跡なし)

直近1時間で、hanako のアカウントに532回の失敗。これは明らかに総当たりです。他のユーザーは数回しかありません。authn_login_fail を「必ず残していた」から、総当たりが1クエリで見えたわけです。残していなければ、この行はそもそも存在しません。

残していたから追えた ── 残さなければ痕跡すら残らない

いまの集計が成立したのは、認証失敗を残していたからです。ここが核心の危機感です。

  • 記録があれば: 総当たりを追え、悪意ある試みを特定できます。
  • 記録がなければ: 総当たりも侵入も「起きたことすら分からない」ままです(CWE-778)。

CWE-778 のいうとおり、記録がなければフォレンジックの証跡が残らず、攻撃元の特定が困難または不可能になります(出典: https://cwe.mitre.org/data/definitions/778.html)。CCT も、認証ログはブルートフォースで侵害された場合その活動を記録し、悪意ある試みの特定を支援するとしています(出典: CCT eBook 書籍2355-2356/PDF2366-2367)。認証ログは、フォレンジックの生命線です。追跡できないと、侵入に気づけません。

人が数えるのではなく、仕組みが「叩く」(A09)

とはいえ、532回に気づけたのは、私がたまたまクエリを叩いたからです。偶然の後追いにすぎません。毎回それを人が数えるのは無理があります。

そこで、authn_login_fail の件数をメトリクスフィルタで数値メトリクスに変換し、CloudWatch アラームで閾値超過時に自動通知します(メトリクスフィルタは、取り込んだイベントからメトリクスを抽出して CloudWatch メトリクスに変換する機能です)(出典: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatchLogsConcepts.html)。

「人が見に行かないと気づけない(後追い)」から「仕組みが叩いてくれる(先回り・人手なし)」への転換です。これはまさに OWASP Top 10 の A09:2021 Security Logging and Monitoring Failures(ログと監視がなければ侵害は検知できない)の要点で、監査対象としてログイン・失敗ログイン・高額取引が挙げられています(出典: https://owasp.org/Top10/2021/A09_2021-Security_Logging_and_Monitoring_Failures/)。なお2025年版では A09 が "Security Logging & Alerting Failures" に改称され、対応を促す「アラート」がいっそう強調されました(出典: https://owasp.org/Top10/2025/A09_2025-Security_Logging_and_Alerting_Failures/)。

点を線に ── 相関で攻撃を1本に束ねる

単一のシグナルで気づいた次は、点を線にします。相関ID やトレースID で、1つの攻撃を複数のログにまたがって縦断するのです。

CloudWatch のサブスクリプションフィルタを使うと、複数のロググループのイベントをリアルタイムに同じ集約先へ配信できます。そこに相関ID(request_id)やトレースID(traceparent)を組み合わせて束ねます。

ここで第5回が効きます。WAF ログや VPC フローログも同じ集約先に集めれば、境界を素通りした攻撃を1本の線で追えます。第5回で「素通りした攻撃を追うのは第6回」と送っていた予告の回収です。

なお、メトリクスフィルタ・サブスクリプションフィルタの具体的な CLI 引数やフィルタパターン構文はここでは載せません(逐語のコマンドは AWS 公式ドキュメントで確認してください)。本記事では、どの部品をどうつなぐかという概念を押さえます。

検知はゴールではなく「受け渡し」/ 追ったログは「証拠」になる

集計・アラート・相関で「気づく・追える」までは来ました。しかし検知はゴールではありません。対応(インシデントレスポンス)の入口です。ログの役割は、「誰が・何を・いつ」を対応チームに正確に渡すこと、つまり追跡の成果を人の対応にバトンすることです。

そして追ったログには、もう一つの顔があります。デジタル証拠です。CCT は、デジタル証拠の情報源としてセキュリティログ・アクセスログ・DNS ログ・認証ログを挙げています(出典: CCT eBook 書籍2355-2356/PDF2366-2367)。証拠に必要なのは完全性、すなわち改ざんされていないと示せることです。ここで、先ほどの「守る設計」(S3 Object Lock の WORM と CloudTrail の整合性検証)が効きます。守る設計があるから、「追えるだけでなく、証拠として使える」を技術で支えられるわけです。

インシデント対応の全体像(準備〜封じ込め〜証拠収集〜根絶〜回復の流れ、一次対応者の役割、Chain of Custody)や、より深い IR / CSIRT の話は、本記事では深追いしません。記事末で別シリーズの復習ログへ誘導します。

レガシーな FuelPHP でも、今日から実装できる

「うちはレガシーの FuelPHP だから無理」で終わらせないでほしいのです。FuelPHP 1.x の Log も、実は Monolog ベースです。Log::warningLog::error で重大度を付けられますし、除外リストで機微データを落とす、request-id で相関を取る、これらはレガシーでも書けます(出典: https://fuelphp.com/docs/classes/log.html)。

// FuelPHP 1.x(レガシー)も Monolog ベース。重大度を付けて構造化して残せる
// config: 'log_threshold' => Fuel::L_WARNING, 'log_date_format' => 'Y-m-d H:i:s'(→ UTC/ISO8601 に揃える余地)
Log::warning('authn_login_fail user='.$email.' ip='.Input::ip());   // 認証失敗を必ず残す
// 機微データ($request 相当)を丸ごと出さない・除外してから書く、が第一歩

既定の log_thresholdFuel::L_WARNINGlog_date_format'Y-m-d H:i:s' で、これは UTC の ISO 8601 ではないので、そこは揃える余地があります。まずやるべきは、冒頭の $request->all() の丸出しをやめることです。

追跡できないと気づけない、だが設計すれば気づける

ここで着地します。設計しなければ、侵入は痕跡すら残らず、追跡できないと気づけません。これが今日の危機感です。

でも、煽って終わりにはしません。消すもの・残すもの・守るもの・追う仕組みを、目的から設計すれば、ちゃんと気づけます。明日から、設計は変えられます。

シリーズ回収 ── 脅威 → 残すログ → 追跡

最終回の収束点です。第1回から第5回までの脅威を「残すログイベント」に割り付け直し、この章で作った追跡手段(集計・アラート・相関)につなぎます。

脅威 残すログイベント 追跡手段
第1回 総当たり(認証・IDOR) authn_login_fail(+重大度) Logs Insights で userid 集計 / 閾値アラート
第2回 攻撃試行(SQLi / SSRF) 入力検証失敗ログ Logs Insights 検索 / 相関
第3回 XSS / セッション異常 / CSRF 失敗 セッション管理失敗ログ Logs Insights 検索
第4回 認証失敗 / 鍵アクセス 認証・鍵操作ログ(機微はリダクション) 集計・相関
第5回 素通りした攻撃(WAF / 境界) WAF ログ・VPC フローログ 同じ集約先で相関

全編を貫く「脅威から対策へのマッピング」の、追跡版です。各回の詳細はそれぞれの回に譲り、ここでは回収ラベルにとどめます。

脅威 → 対策マッピング

直前の回収表が「どの回の脅威を、どのログイベントで追うか」という回の軸だったのに対し、この表は「そのログを消す/残す/守る、どう設計するか」という設計の軸で整理し直したものです。

脅威 残す / 消す 追跡・対策
機微データのログ混入 消す(リダクション+除外リスト) PAN=先頭6/末尾4、パスワードは残さない
攻撃・認証失敗が記録されず気づけない 残す(標準イベント名+重大度+相関) 集計 / アラート / 相関
ログ改ざん・証拠にならない 守る(WORM+整合性検証) S3 Object Lock / CloudTrail 検証
共通 目的から設計する 「書けば安全」ではない=設計する

呼び名や角度は違っても、貫いているのは1つです。ログは「書けば安全」ではなく、設計する対象です。

まとめ ── ログは「消すもの」と「必ず残すもの」を設計する

最後に、この記事の要点を振り返ります。今日の Take-away は、この3点です(冒頭の結論3点と一字一句そろえています)。

  1. ログは「目的」から設計する
  2. 機微データ(パスワード・クレカ・PII)はログに残さない
  3. 攻撃・認証失敗の痕跡は「必ず」残す ── 追跡できないと侵入に気づけない

ちょっと前の私がいちばん助かったであろう一言は、「ログは書けば安全なものではなく、"消すもの"と"必ず残すもの"を目的から設計する対象だ」でした。ここが腑に落ちると、$request->all() の1行も、authn_login_fail の1行も、S3 Object Lock の1設定も、すべて「侵入を追える状態を作るための設計」として一本の線でつながります。設計を誤れば、ログ自体が漏洩源になり、しかも侵入に気づけません。

Next action: 明日からできる3つ

理想論で終わらせないために、明日からできる具体を3つ挙げます。1・2・3が、そのまま上の結論①②③に対応します。

  1. 消す: $request->all()passwordcardtoken をコードから grep で棚卸しし、あればリダクションを通します。
  2. 残す・追う: 認証失敗ログ(authn_login_fail)と、閾値アラートを有効にします。
  3. 守る: ログの保持期間と、改ざん防止(WORM / 整合性検証)を確認します。

このうち1つでも赤信号があれば、そこが設計の穴です。まずは丸ログの grep から始めてみてください。

シリーズ総括と、次の一歩

これで全6回が終わりました。第1回の認証・認可から、インジェクションと SSRF、XSS・CSRF・セッション、暗号とシークレット、ネットワークと WAF、そして今日のセキュアログと追跡まで。振り返ると、全6回はいずれも「作る側の設計」でアプリを守る話でした。攻撃を外から止めるのではなく、コードと構成の設計で穴をふさぐ、という一貫した視点です。

次の一歩は、組織的なインシデント対応です。本記事で「検知は対応への受け渡し」「ログはデジタル証拠になる」と触れた、その先です。より深い IR / CSIRT(NIST SP 800-61・RFC2350・X.1060・JPCERT/CC など)は、別シリーズの CySec 復習ログ #9#10〜#12 に用意しています。作る側の設計から、組織で守る側へ、そこでバトンを渡します。

あわせて読みたい

本シリーズ第1〜5回はすべて公開済みです。別シリーズ(CySec 復習ログ)の続編は順次公開予定のため、公開され id が確定した回から、下記の「近日公開予定」を実リンクへ差し替えます。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?