Google Cloud の WAF といえば Cloud Armor ですが、WAF 導入時に気になるのは正規の通信の誤検知がないかどうかです。
誤検知が全く無いセキュリティツールというのは残念ながら存在しないので、最初に検知だけして遮断はしない設定 (Cloud Armor でいうと Preview モード) に
してしばらく様子を見る という運用をする場合、ログを見て誤検知を判別する必要があります。

また、Preview モードでの運用を終了し、遮断する設定になった後にも、誤検知やセキュリティインシデント時など、ログは重要な情報源です。
調査にあたっては、ログ項目やログの出方を把握しておく必要があります。

自分の備忘録も兼ねて、ログ項目とログの出方についてまとめてみました。

前提

Global external Application Load Balancer 利用時に、backend service に Cloud Armor の backend security policy を適用した場合の例です。

今回書かないこと

Cloud Armor やルールの選定・設定方法、ログ出力の設定方法については記載しません。
また、Cloud Armor Enterprise で利用できるルールについても今回は述べません。Cloud Armor Standard で利用可能なルールを主に対象とします。

Cloud Armor ログとは

Cloud Armor のログは、Load Balancer のログに含まれています。
https://cloud.google.com/armor/docs/request-logging

詳細ログを出力するには設定が必要です。有効化すると、ログに項目が追加されます。
https://cloud.google.com/armor/docs/verbose-logging

Cloud Armor 関連のログ項目まとめ

Load Balancer ログ項目

Cloud Armor を利用しなくても出力される項目ですが、HTTP 通信に関する基本的な情報を含み、調査における必須項目のため、合わせてまとめます。

項目 内容
insertId ログエントリの識別子
severity ログエントリの重要度*1 (例) INFO, WARNING
timestamp Google Front End (GFE) がリクエストを受信した時間
resource.labels.backend_service_name リクエストを受信した backend service 名
httpRequest.remoteIp クライアントIP
httpRequest.requestMethod HTTP リクエストメソッド (GET/HEAD/PUT/POST)
httpRequest.requestUrl リクエスト URL 例 “http://example.com/some/info?color=red”
httpRequest.userAgent User Agent
httpRequest.requestSize リクエストメッセージサイズ (byte)
httpRequest.responseSize レスポンスメッセージサイズ (byte)
httpRequest.status レスポンスステータスコード

Global external Application Load Balancer ログについて
https://cloud.google.com/load-balancing/docs/https/https-logging-monitoring

HttpRequest 詳細
https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest

*1 Security Policy の最終判定が Allow の場合は INFO、Deny の場合は WARNING になるようです。プレビューモード有効のルールで検知したとしても、最終的に Allow になっていれば INFO となっていました。

Cloud Armor 基本ログ項目

Load Balancer (Backend Service) に Cloud Armor のセキュリティポリシーを紐づけた場合に出力されるようになる項目です。

jsonPayload.enforcedSecurityPolicy にはプレビューモードが無効のルールによる検知内容や評価結果、jsonPayload.previewSecurityPolicy にはプレビューモードが有効のルールによる検知内容や評価結果が入ります。

項目 内容
jsonPayload.securityPolicyRequestData.remoteIpInfo.regionCode クライアントIPに対応する国*2 (例) JP, US
jsonPayload.statusDetails Security policy によるリクエスト検証結果の簡単な説明
jsonPayload.enforcedSecurityPolicy.name jsonPayload.previewSecurityPolicy.name 検知した Security Policy 名
jsonPayload.enforcedSecurityPolicy.outcome jsonPayload.previewSecurityPolicy.outcome 検知ルールによるリクエスト処理結果 ACCEPT, DENY など
jsonPayload.enforcedSecurityPolicy.priority jsonPayload.previewSecurityPolicy.priority 検知ルールの Priority
jsonPayload.enforcedSecurityPolicy.configuredAction jsonPayload.previewSecurityPolicy.configuredAction 検知ルールに設定されているアクション ALLOW, DENY, RATE_BASED_BAN など
jsonPayload.enforcedSecurityPolicy.preconfiguredExprIds jsonPayload.previewSecurityPolicy.preconfiguredExprIds 検知ルールにおいてマッチした事前構成ルールのID

*2 2025/01現在、ドキュメントにはログ項目として記載がありませんが、実際のログには出力されています。なお、Cloud Armor は独自の geolocation database を使用しています。
https://cloud.google.com/armor/docs/security-policy-overview#source_geography_rules

その他の Cloud Armor ログ項目はこちら
https://cloud.google.com/armor/docs/request-logging

Cloud Armor 詳細ログ項目

項目 内容
jsonPayload.enforcedSecurityPolicy.matchedFieldType jsonPayload.previewSecurityPolicy.matchedFieldType 検知ルールにマッチしたフィールドの種類 (例) ARG_VALUES, COOKIE_VALUES, REFERER, USER_AGENT
jsonPayload.enforcedSecurityPolicy.matchedFieldName jsonPayload.previewSecurityPolicy.matchedFieldName Key-Value ペアの値の部分とマッチした場合、ここに Key が入る
jsonPayload.enforcedSecurityPolicy.matchedFieldValue jsonPayload.previewSecurityPolicy.matchedFieldValue 検知した値の先頭から16バイトまで

その他の詳細ログ項目はこちら
https://cloud.google.com/armor/docs/verbose-logging

ログ例

test-policy という名前で作成した Security Policy を使い、詳細ログを有効化し、Cloud Logging でログ出力を調べました。
Cloud Armor 無しでも出力される項目や、Cloud Armor のログ項目であっても検知状況による出力差異のないものについては、記載を省略しています。
また、Cloud Logging の Logs Explorer 画面表示に近い形で記載しており、 JSON 形式にはなっていません。

デフォルトルール

Security policy に必ず存在する、priority 2147483647 のルールで処理された結果をまず見てみます。

Deny (403)

デフォルトルールのアクションを Deny とし、レスポンスステータスコードを 403 とした場合

{
    httpRequest: {
        status: 403
    }
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "DENY"
            name: "test-policy"
            outcome: "DENY"
            priority: 2147483647
        }
        statusDetails: "denied_by_security_policy"
    }
}

「デフォルトルール」のような記載はありませんが、priorityで判別がつきます。

Deny (502)

Action を Deny、レスポンスステータスコードをデフォルトの 403 以外にした場合、Cloud Armor ログの出方は変わりませんが、httpRequest.status を見ると、設定したステータスコードが返っていることがわかります。

{
    httpRequest: {
        status: 502
    }
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "DENY"
            name: "test-policy"
            outcome: "DENY"
            priority: 2147483647
        }
        statusDetails: "denied_by_security_policy"
    }
}

Allow

デフォルトルールの Action を Allow とし、他のどのルールでも Deny されずにデフォルトルールで Allow となった場合

{
    httpRequest: {
        status: 200
    }
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "ALLOW"
            name: "test-policy"
        outcome: "ACCEPT"
        priority: 2147483647
        }
        statusDetails: "response_sent_by_backend"
    }
}

Priority や outcome、statusDetails から、デフォルトルールでリクエストが許可されたことが分かります。

以降は httpRequest、jsonPayload.enforcedSecurityPolicy.name、jsonPayload.previewSecurityPolicy.name は省略します。
また、以降の例ではデフォルトルールのアクションは ALLOW としています。

基本モードのルール

Security policy のルールには基本モード (Basic mode) と詳細モード (Advanced mode) の2タイプありますが、まず基本モードで検知された場合の出力を見てみます。

Action が Allow

IP や IP 範囲指定でリクエストを許可するルールです。

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "ALLOW"
            outcome: "ACCEPT"
            priority: 5000
        }
        statusDetails: "response_sent_by_backend"
    }
}

ルールの内容や説明はログに記載されないため、どんなルールで検知したのかは Priority を見て特定する必要があります。

Action が Allow (Preview モード)

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "ALLOW"
            outcome: "ACCEPT"
            priority: 2147483647
        }
        previewSecurityPolicy: {
            configuredAction: "ALLOW"
            outcome: "ACCEPT"
            priority: 5000
        }
        statusDetails: "response_sent_by_backend"
    }
}

Action が Deny

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "DENY"
            outcome: "DENY"
            priority: 5000
        }
        statusDetails: "denied_by_security_policy"
    }
}

なお、レスポンスステータスコード設定を変えても jsonPayload 内容は変わりません。

Action が Deny (Preview)

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "ALLOW"
            outcome: "ACCEPT"
            priority: 2147483647
        }
        previewSecurityPolicy: {
            configuredAction: "DENY"
            outcome: "DENY"
            priority: 5000
        }
        statusDetails: "response_sent_by_backend"
    }
}

ちなみに、ルールのレスポンスコード設定 (403 or 404 or 502) が異なっていてもログ上区別はつきませんでした。

レート制限

Action が Rate based ban のルールに引っかかった場合

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "RATE_BASED_BAN"
            outcome: "DENY"
            priority: 15000
            rateLimitAction: {
                outcome: "BAN_THRESHOLD_EXCEED"
            }
        }
        statusDetails: "denied_by_security_policy"
    }
}

rateLimitAction というログ項目が追加されています。

レート制限 (Preview)

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "ALLOW"
            outcome: "ACCEPT"
            priority: 2147483647
        }
        previewSecurityPolicy: {
            configuredAction: "RATE_BASED_BAN"
            outcome: "ACCEPT"
            priority: 15000
            rateLimitAction: {
                outcome: "RATE_LIMIT_THRESHOLD_CONFORM"
            }
        }
        statusDetails: "response_sent_by_backend"
    }
}

詳細モードのルール

ModSecurity Core Rule Set ベースの事前構成 WAF ルール (以下、事前構成ルール) を使用したルールや、事前構成ルールを使わずにカスタムルール言語のみ使った簡単な自作ルールでの検知ログを見てみます。

事前構成ルール1つ (リクエストボディ以外で検知)

クロスサイト スクリプティングルール (以下、XSS ルール) で Deny となった場合の例です。

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "DENY"
            matchedFieldLength: 8
            matchedFieldName: "src"
            matchedFieldType: "COOKIE_VALUES"
            matchedFieldValue: "<script>"
            matchedLength: 8
            outcome: "DENY"
            preconfiguredExprIds: [
                0: "owasp-crs-v030301-id941320-xss"
            ]
            priority: 10000
        }
        statusDetails: "denied_by_security_policy"
    }
}

事前構成ルールにはそれぞれ複数のシグネチャが含まれますが、preconfiguredExprIds の出力からどのシグネチャにマッチしたかが分かるようになっています。(この例では owasp-crs-v030301-id941320-xss)
ただ、ルール名 (xss-v33-stable や xss-v33-canary) の方は出力されないようです。

また、詳細ログの matchedFieldValue などから検知箇所が分かるようになっており、誤検知の調査などに役立ちます。

事前構成ルール1つ (リクエストボディ以外で検知、Preview モード)

Preview 中でも検知箇所などの詳細は出力されます。

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "ALLOW"
            outcome: "ACCEPT"
            priority: 2147483647
        }
        previewSecurityPolicy: {
            configuredAction: "DENY"
            matchedFieldLength: 8
            matchedFieldName: "src"
            matchedFieldType: "COOKIE_VALUES"
            matchedFieldValue: "<script>"
            matchedLength: 8
            outcome: "DENY"
            preconfiguredExprIds: [
                0: "owasp-crs-v030301-id941320-xss"
            ]
            priority: 10000
        }
        statusDetails: "response_sent_by_backend"
    }
}

事前構成ルール1つ (リクエストボディで検知)

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "DENY"
            inspectedBodySize: 19
            matchedFieldLength: 8
            matchedFieldName: "src"
            matchedFieldType: "ARG_VALUES"
            matchedFieldValue: "<script>"
            matchedLength: 8
            outcome: "DENY"
            preconfiguredExprIds: [
                0: "owasp-crs-v030301-id941320-xss"
            ]
            priority: 10000
        }
        statusDetails: "body_denied_by_security_policy"
    }
}

matchedFieldType や statusDetails から、リクエストボディで検知したことがわかります。
(statusDetails は詳細ログ無効でも出力される項目です)

事前構成ルール複数を同ルール内に OR で列挙

ルール数を節約するために、詳細モードに複数の事前定義ルールを含めることがあると思います。例えばこんな感じです。(sensitivity は適当です)

evaluatePreconfiguredWaf('xss-v33-stable', {'sensitivity': 4}) || evaluatePreconfiguredWaf('lfi-v33-stable', {'sensitivity': 4})

XSS ルールとローカル ファイル インクルードルール (以下、LFI ルール) の両方にマッチしうるようなリクエストがあった場合、どう記録されるのか見てみます。

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "DENY"
            matchedFieldLength: 8
            matchedFieldName: "src"
            matchedFieldType: "COOKIE_VALUES"
            matchedFieldValue: "<script>"
            matchedLength: 8
            outcome: "DENY"
            preconfiguredExprIds: [
                0: "owasp-crs-v030301-id941320-xss"
            ]
            priority: 10000
        }
        statusDetails: "denied_by_security_policy"
    }
}

preconfiguredExprIds の出力から、owasp-crs-v030301-id941320-xss のシグネチャにマッチしたことが確認できます。
XSS ルールと LFI ルールの順番を書き換えてみたところ、LFI ルールのシグネチャ ID が出力されるようになったので、最初にマッチした方だけログに出力されるようです。

事前構成ルール複数を同ルール内に AND で列挙

evaluatePreconfiguredWaf('xss-v33-stable', {'sensitivity': 4}) && evaluatePreconfiguredWaf('lfi-v33-stable', {'sensitivity': 4})

現実で使う可能性は低いですが、上記のように AND で列挙したパターンも見てみることにします。

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "DENY"
            matchedFieldLength: 23
            matchedFieldType: "FILENAME"
            matchedFieldValue: "/.env"
            matchedLength: 5
            matchedOffset: 18
            outcome: "DENY"
            preconfiguredExprIds: [
                0: "owasp-crs-v030301-id941320-xss"
                1: "owasp-crs-v030301-id930130-lfi"
            ]
            priority: 10000
        }
        statusDetails: "denied_by_security_policy"
    }
}

ログに preconfiguredExprIds が複数出力されました。
MatchedField には片方のマッチ情報のみ出力されていました。

自作ルール

適当なパスパターンマッチルールを作成して検知 (Deny) させてみます。

{
    jsonPayload: {
        enforcedSecurityPolicy: {
            configuredAction: "DENY"
            outcome: "DENY"
            priority: 20000
        }
        statusDetails: "denied_by_security_policy"
    }
}

事前構成ルール利用時よりログ項目が大きく減り、基本モードのルールと変わらなくなりました。
リクエストのどの部分にマッチしたかなどは出力されず、Priority でルールを特定する必要があります。

備考

複数ルールにマッチしても最初にマッチしたルール分しか出力されない

Google Cloud Armor のロギングは、セキュリティ ポリシーがプレビュー モードかどうかにかかわらず、受信リクエストに一致する最初の(優先度の高い)ルールに基づいて生成されます。つまり、一致しないルールや、優先度の低い一致ルールに対してログは生成されません。

ドキュメントにある通り、最初にマッチしたルールのみ、ログに出力されます。

Preview モード有効のルールと Preview モード無効のルールがあり、両方にマッチした場合、

リクエストによってプレビューがトリガーされた場合、一致が見つかるまで Google Cloud Armor は他のルールを評価し続けます。一致したルールとプレビュー ルールの両方がログに記録されます。

とあるように、jsonPayload.previewSecurityPolicyjsonPayload.enforcedSecurityPolicy にデータが入ります。

全てのヘッダが記録されるわけではない

User agent などごく一部のヘッダのみログに記録されます。
X-Forwarded-For などはログに含まれないためご注意ください。

おわりに

いざログを調査したいとなっても項目が多すぎて事前調査に時間がかかってしまう、というのはどんなログでもあり得ることだと思います。
ログを標準フォーマットに変換してくれるようなツールを使うのも手ですが、安価に手早く確認したいとなった際には、Cloud Logging で生ログを見ることになります。
そんなログ分析の際に一助となれば幸いです!