🦾

Cloud Armor で怪しいアクセスログからルールをプレビューして適用する流れ

waddy_u2022/07/15に公開

Cloud Armor は Google Cloud のマネージドWAFです。

https://cloud.google.com/armor?hl=ja

この記事では怪しいアクセスをブロックする流れをメインに説明します。先日GAとなった目玉機能の Adaptive Protection(適応型保護) や追加料金を払えば利用できる Managed Protection はここでは扱いません。

Cloud Armor の構成

おおよそ AWS WAF と似たような構成になっており、

このあたりが特徴です。今回はスタンダードティアの料金体系での基本的な流れを紹介します。

WAF の課題 - ルールを適用するまで対象がわからない

ある不審なリクエストをブロックしたいとします。Web Application Firewall の課題として私が感じていたのは「このブロックルールを適用すると、どれくらいのリクエストがブロックされるのかわからない」というものでした。攻撃から身を守るには多少の意図しないブロックは仕方ないとはいえ、ダイレクトにUXを損なうことになります。適用してみて、意図しないブロックが発生していたら、条件を変更するというのは理にかなっていはいるものの一部のユーザーには不便を強いています。

ルールのプレビューモード

Cloud Armor の特徴のひとつに、プレビューモードがあります。これは先の課題を解消してくれるもので、ブロックルールは適用するものの、実際に403を返すことはせず、正常に200を返します。その代わり、Cloud Logging に「プレビューモードだけどいまのリクエストブロック対象ですわ」というログを残してくれます。わたしたちは、このログを確認することで、たしかに設定したルールが意図した範囲に適用されるものである、ということを事前に確認した上で適用できます。ありがたいですね。さっそくためしてみましょう。

前提:バックエンドサービスが作成されていること

Cloud Armor は、HTTP Load Balancer のバックエンドサービスに紐付けるものです。まずはロードバランサー配下のアプリケーションを構築します。


Cloud Run アプリケーションのリージョン外部 HTTP(S) ロード バランシング アーキテクチャ

なお、必須ではありませんが、バックエンドサービスはログ出力を有効にしたほうが良いです。Cloud Armor のセキュリティポリシーが適用されブロックされたかどうかは、このバックエンドサービスのログからわかります。

Google Cloud Armor ログは Cloud Load Balancing のログの一部であるため、Google Cloud Armor のログの生成には、ロードバランサに構成されたログ サンプリング レートが適用されます。HTTP(S) ロード バランシング、TCP プロキシ ロード バランシング、SSL プロキシ ロード バランシングのサンプリング レートを減らすと、Google Cloud Armor のリクエストログは、そのレートでサンプリングされます。
https://cloud.google.com/armor/docs/request-logging?hl=ja

ロギングを有効にする方法はこちらを参照してください。

https://cloud.google.com/load-balancing/docs/https/https-logging-monitoring?hl=ja#logging

以下は、サンプリングレート0.1でのTerraformによる適用例です。

# ロードバランサーのバックエンド: Cloud Run api
resource "google_compute_backend_service" "cloudrun_api_backend" {
  load_balancing_scheme           = "EXTERNAL"
  name                            = "${var.project_id}-cloudrun-api"
  description                     = "Cloud Run User API"
  port_name                       = "http"
  protocol                        = "HTTP2"
  session_affinity                = "NONE"
  timeout_sec                     = 30
  connection_draining_timeout_sec = 0
  security_policy = google_compute_security_policy.user_app_armor_policy.self_link

  # Cloud Armor の検出ログを見るために設定する
+  log_config {
+    enable = true
+    sample_rate =  0.1
+  }

  backend {
    group = google_compute_region_network_endpoint_group.cloudrun_api_neg.self_link
  }
}

バックエンドサービスとログ出力の準備ができたら、Cloud Armor のセキュリティポリシーを作成します。

Cloud Armor の作成

マネジメントコンソール > Cloud Armor > セキュリティポリシーの作成 とします。

  • ターゲットへのポリシーの適用 で、適用したい先のバックエンドサービスを選びます
  • Adaptive Protection は、今回の例では利用しませんが、スタンダードティアでも潜在的な攻撃のアラートを出してくれるため有効にしておいても良いです

設定が確定したら作成します。

ブロックの流れ

準備ができたのでブロックまでの流れを追っていきます。

怪しいリクエストログを見つける

じじじ人力!?というツッコミをもらいそうですが、ここでは「さまざまな判断材料は自動で揃えるが、ブロック対象のリクエストについて最後は人間が判断する」くらいに思ってください。だいたいの場合は、IPアドレスユーザーエージェントの組み合わせでブロックするのではないでしょうか。たとえば Cloud Logging で以下のあやしいリクエストを見つけたとします。

{
  "insertId": "62d0cf9200019f2b843c1093",
  "httpRequest": {
    "requestMethod": "GET",
    "requestUrl": "https://example.com/articles/super-power?wesdf12fsef",
    "requestSize": "406",
    "status": 200,
    "responseSize": "16321",
    "userAgent": "curl/7.79.1",
    "remoteIp": "3.5.140.1",
    "serverIp": "142.250.207.14",
    "latency": "0.067959112s",
    "protocol": "HTTP/1.1"
  },
  "timestamp": "2022-07-15T02:23:14.106283Z",
  "severity": "INFO",
  "receiveTimestamp": "2022-07-15T02:23:14.155967694Z",
  "spanId": "474634380232661585"
}
  • IPアドレス: "remoteIp": "3.5.140.1" [1]
  • ユーザーエージェント: userAgent: "curl/7.79.1"

なのでこの情報を使ってブロックします。ちなみに "serverIp": "142.250.207.14"[2]はロードバランサーのIPアドレスのはずです。

ブロックルールを作成する

IPアドレスとユーザーエージェントを特定したので、セキュリティポリシーへルールを追加します。

このとき、「条件」には記法を参考に、以下のように入力します。

inIpRange(origin.ip, '3.5.140.1/32') && has(request.headers['user-agent']) && request.headers['user-agent'].contains('curl/7.79.1')

さらに、プレビューのみを有効するにチェックを入れます。

プレビューにチェックを入れることで、実際にブロックまではせず、そのかわり条件に一致していることをログ出力します。

プレビューログを確認する

条件に合致するルールを作成しました。これでアクセスしてみましょう。ブロックはされませんが、以下のログがロードバランサーのログ(resource.type="http_load_balancer")として出力されます。

{
  "insertId": "1s3h2rcg12f1i1q",
  "jsonPayload": {
    "enforcedSecurityPolicy": {
      "name": "test-policy",
      "outcome": "ACCEPT",
      "priority": 2147483647,
      "configuredAction": "ALLOW"
    },
    "statusDetails": "client_disconnected_after_partial_response",
+    "previewSecurityPolicy": {
+      "priority": 0,
+      "name": "test-policy",
+      "configuredAction": "DENY",
+      "outcome": "DENY"
+    },
    "@type": "type.googleapis.com/google.cloud.loadbalancing.type.LoadBalancerLogEntry",
    "cacheId": "NRT-cf0517a3",
    "remoteIp": "3.5.140.1"
  },
  "httpRequest": {
    "requestMethod": "GET",
    "requestSize": "61",
    "status": 200,
    "responseSize": "63560",
    "userAgent": "curl/7.79.1",
    "remoteIp": "3.5.140.1",
    "cacheLookup": true,
    "latency": "0.388584s"
  },
  "resource": {
    "type": "http_load_balancer",
  },
  "timestamp": "2022-07-15T05:50:35.809583Z",
  "severity": "INFO"
}

previewSecurityPolicyが、まさに冒頭で記した「プレビューモードだけどいまのリクエストブロック対象ですわ」という記録になります。

意図しないリクエストがブロックされないか確認する

プレビューログを一覧して、意図しない、善良なリクエストがブロックされないか確認しましょう。Cloud Logging のクエリで次のように入力すれば一覧できます。

resource.type="http_load_balancer"
jsonPayload.previewSecurityPolicy.configuredAction="DENY"

とくにIPアドレスのみの指定や、一般的なブラウザのユーザーエージェントは、他のユーザーも巻き込む可能性があるためチェックをオススメします。たとえばリファラやリクエスト先でさらに絞り込めそうであれば、ブロック範囲をより狭くできます。

プレビューを解除して本適用する

プレビューでブロック対象を確認したら、ルールを編集して、プレビューモードを解除します。これで条件に合致するリクエストはブロック(403)されます。

<!doctype html><meta charset="utf-8"><title>403</title>403 Forbidden

このようなレスポンスになり、ブロックされていることがわかります。

ブロックログを確認する

Cloud Logging でつぎのクエリを入れます。ブロックまたは許可されたーが適用されたログを一覧します。

resource.type="http_load_balancer"
jsonPayload.enforcedSecurityPolicy.configuredAction="DENY"

次のようなリクエストログが見つかります。意図どおりブロックできていることが確認できました。

{
  "insertId": "1rtzymifc41of2",
  "jsonPayload": {
    "cacheId": "NRT-33e460bb",
    "@type": "type.googleapis.com/google.cloud.loadbalancing.type.LoadBalancerLogEntry",
+    "enforcedSecurityPolicy": {
+      "outcome": "DENY",
+      "name": "test-policy",
+      "priority": 0,
+      "configuredAction": "DENY"
+    },
    "statusDetails": "denied_by_security_policy",
    "remoteIp": "3.5.140.1"
  },
  "httpRequest": {
    "requestMethod": "GET",
    "requestSize": "61",
    "status": 403,
    "responseSize": "353",
    "userAgent": "curl/7.79.1",
    "remoteIp": "3.5.140.1",
    "cacheLookup": true,
    "latency": "0.248457s"
  },
  "resource": {
    "type": "http_load_balancer"
  },
  "timestamp": "2022-07-15T06:46:54.337457Z",
  "severity": "WARNING",
  "receiveTimestamp": "2022-07-15T06:46:55.367075272Z",
  "spanId": "64f80aac2648396f"
}

これで Cloud Armor を使ってブロックする一連の流れは完了です。

おわりに

Cloud Armor のルールを作って、プレビュー・適用する流れを試しました。料金も従量課金がベースでお財布に優しい点、助かりますね。

  • WAF リクエスト: $0.75/100 万件のリクエスト
  • WAF セキュリティ ポリシー: ポリシー 1 件あたり月額 $5
  • WAF ルール: ルールあたり月額 $1

https://cloud.google.com/armor/pricing?hl=ja

今回の例だとセキュリティポリシーがひとつ、ルールがひとつ(基本ルールも入るのかは不明です、すいません)なのでだいたい1,000円くらいで使える試算になります。どなたかの参考になれば幸いです。

参考

https://www.wafcharm.com/blog/gcp-check-waf-detection-status-ja/

https://qiita.com/pict3/items/7b21f034006f6ee18b77
脚注
  1. facebook のIPアドレスです ↩︎

  2. google のIPアドレスです ↩︎

Zenn Tech Blog

Zenn開発チームのテックブログです。Zennの開発・運用にまつわる技術的な知見を投稿します。主な技術スタックは React / Next.js / TypeScript / Ruby on Rails / Google Cloud などです。

Discussion

ログインするとコメントできます