Carpe Diem

備忘録

Cloud ArmorのAdaptive Protectionを使ってみる

概要

Cloud ArmorのようなWAFは通常ルールベースで不正なアクセスを制御するため、悪意あるユーザがIPアドレスやパラメータをコロコロ変えたりすると都度ルールを設定する必要が出てきてイタチごっこになりがちです。

そんなケースに対応できるよう、機械学習を用いて攻撃であるかどうかを判断してくれるマネージドサービスがAdaptive Protectionです。

イメージとしては以下の図のように悪意あるユーザの攻撃と判断されたらアラートで通知してくれて、適用すべきルールまで自動的に教えてくれます。

ref: https://cloud.google.com/armor/docs/adaptive-protection-use-cases?hl=ja

なのでサービス側としてはアラートを見て「これは攻撃だろう」と判断したら提案されたルールを適用するだけで防ぐことが可能になります。

つまりAdaptive Protectionを用いることで

  • 迅速な攻撃の検知ができる
  • 不正リクエストを弾くためのルール(パラメータ)を人が考えなくて良い

といったメリットを享受できます。

環境

  • terraform v1.2.3
  • hashicorp/google-beta v4.26.0

Adaptive Protection

Managed Protection

まず前提としてAdaptive Protectionをフルで利用するためににはManaged Protection Plusを利用する必要があります。
利用していない場合

  • 攻撃は検知するが攻撃シグネチャを教えてくれない
  • ルールを提案してもらえない

といったことになり、検知はできても攻撃を防ぐためのルールを人が考えないといけません。

設定方法

SecurityPolicy

Adaptive Protectionはterraformで以下のように設定できます。
providerはgoogle-betaを使う必要があります。

resource "google_compute_security_policy" "adaptive" {
  provider    = google-beta
  name        = "adaptive-protection"
  description = "Adaptive Protection"

  adaptive_protection_config {
    layer_7_ddos_defense_config {
      enable          = true
      rule_visibility = "STANDARD"
    }
  }

  rule {
    action   = "allow"
    priority = "2147483647"

    match {
      versioned_expr = "SRC_IPS_V1"

      config {
        src_ip_ranges = ["*"]
      }
    }

    description = "default rule"
  }
}

Kubernetes Manifest

BackendConfig

先程作成したCloudArmorを使うようBackendConfigを用意します。

apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: prd-api-bc
spec:
  timeoutSec: 10
  securityPolicy:
    name: "adaptive-protection"
  healthCheck:
    checkIntervalSec: 15
    timeoutSec: 10
    port: 8000
    type: HTTP
    requestPath: /

Service

BackendConfigをServiceに紐付けるようにします。

apiVersion: v1
kind: Service
metadata:
  name: prd-api
  annotations:
    cloud.google.com/backend-config: '{"ports": {"http":"prd-api-bc"}}'
    cloud.google.com/neg: '{"ingress": true}'
  labels:
    name: prd-api
spec:
  type: ClusterIP
  selector:
    name: prd-api
  ports:
    - name: http
      protocol: TCP
      port: 8000

Ingress

前述のServiceに流すようなIngressを用意します。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: prd-api
  annotations:
    kubernetes.io/ingress.allow-http: "true"
    kubernetes.io/ingress.class: gce
    kubernetes.io/ingress.global-static-ip-name: "prd-api"
spec:
  tls:
    - secretName: wildcard-jun06t-com
      hosts:
        - api.jun06t.com
  defaultBackend:
    service:
      name: prd-api
      port:
        number: 8000
  rules:
    - host: api.jun06t.com
      http:
        paths:
          - path: /*
            pathType: ImplementationSpecific
            backend:
              service:
                name: prd-api
                port:
                  number: 8000

ログからslack通知させる

攻撃を検知するとドキュメントにあるように以下の様のログが生成されます。

...
 jsonPayload: {
   alertId: "11275630857957031521"
   backendService: "test-service"
   confidence: 0.71828485
   headerSignatures: [

    0: {
     name: "RequestUri"
     significantValues: [
      0: {
       attackLikelihood: 0.88
       matchType: "MATCH_TYPE_EQUALS"
       proportionInAttack: 0.85
       proportionInBaseline: 0.01
       value: "/"
      }
     ]
    }
    1: {
     name: "UserAgent"
     significantValues: [
      0: {
       attackLikelihood: 0.92
       matchType: "MATCH_TYPE_EQUALS"
       proportionInAttack: 0.85
       proportionInBaseline: 0
       value: "Unusual browser"
      }
      1: {
       attackLikelihood: 0.87
       proportionInAttack: 0.7
       proportionInBaseline: 0.1
       missing: true
      }
     ]
    }
   ]
   suggestedRule: [
    0: {
     action: "DENY"
     evaluation: {
       impactedAttackProportion: 0.95
       impactedBaselineProportion: 0.001
       impactedBaselinePolicyProportion: 0.001
     }
     expression: "evaluateAdaptiveProtection('11275630857957031521')"
    }
   ]
   ruleStatus: RULE_GENERATED
   attackSize: 5000
 }
 resource: {
    type: "network_security_policy",
    labels: {
      project_id: "your-project",
      policy_name: "your-security-policy-name"
    }
 },
}
}
...

なのでresource.type="network_security_policy"とalertIdありの

resource.type="network_security_policy"
jsonPayload.alertId!=""

とすればイベントを絞ることができます。

以下のボタンからログのクエリベースでアラートを設定できます。

動作確認

自前で擬似的に負荷を与えてアラートが鳴るか検証します。

k6スクリプトを用意し、

import http from 'k6/http';
import { sleep } from 'k6';

export default function () {
  http.get('https://api.jun06t.com');
  sleep(1);
}

実行します。

$ k6 run --vus 1000 --duration 30s script.js

するとAdaptive Protectionのダッシュボードに以下の通知が出ます。

攻撃シグネチャと推奨ルール

日本語だとシグネチャが「署名」と翻訳されたやや意味が分かりづらいですが、実際は攻撃シグネチャのことです。
詳細を開くと以下のように表示されます。

攻撃シグネチャと推薦されたルール

今回攻撃シグネチャ

の2つが出てきました。
これらのパラメータが画像の通りの値の場合、それは悪意あるユーザによる攻撃という判定になります。

ドキュメント上では他にも

  • RegionCode
  • RequestUri

などがあります。

そして最後には推奨ルールが表示されます。

SecurityPolicyに適用

先程の推奨ルールをSecurityPolicyにコピペすれば、このユーザからのリクエストを弾くことができます。簡単ですね。

ブロックされるか確認

では最初と同じ攻撃を実行してみます。

すると画像のように全て失敗するようになりました。ルールが適用されてブロックされていることが確認できます。

一方curlで試してみたところリクエストは通りました。これはUserAgentが異なるため今回のルールが適用されなかったと考えられます。

検証結果

検証結果をまとめると以下でした。

  • ベースとなるリクエスト数に対し、特定のシグネチャ(送信元IP、UA、国コードなど)に偏ったリクエストが異常に多いと攻撃とみなされる
    • devのようにリクエストが少なければ発火しやすいが、prdではかなりのリクエスト数が必要になる
    • 低いしきい値でレート制限したい場合は通常のrate limitの方が良さそう
  • 攻撃とみなされても自動的にブロックするわけではなく、推奨ルールを提示してサービス側に判断を委ねる
    • 推奨ルールはコピペするだけで簡単に設定できる。設定後数分で反映される(5~10分程度かかった)
  • アラートのslack通知はログベースで簡単に設定できる

まとめ

Cloud ArmorのAdaptive Protectionを利用することでイタチごっこになりがちなDDoS対策を自動的に対応できるようになりました。

参考