GKE で Google Cloud Armor を利用してみる

7 min read読了の目安(約6600字

GKE + Cloud Armor

Google Cloud Armor とは、クライアントからの攻撃からwebアプリケーションを保護してくれるというGCPが提供するサービスです。
IP制限を行ったり、事前定義されたWAFを利用して各種攻撃を防いだりできます。

そんなCloud ArmorをGKEで利用するための手順等を簡単にまとめていきます。

前提

  • GCP projectの作成
  • 各種binのインストールおよび設定
    • gcloud
    • terraform
    • kubectl

やること

以下のような、form送信ができるwebアプリケーションをGKE上で動かしているとします。

ソースコードはこんな感じ

表示したい内容

main.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	server := http.Server{
		Addr: ":80",
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, "static/index.html")
	})
	http.HandleFunc("/form", func(w http.ResponseWriter, r *http.Request) {
		r.ParseForm()
		fmt.Fprintln(w, r.Form)
	})
	server.ListenAndServe()
}
static/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Simple Form</title>
</head>
<body>
  <form action="/form" method="post">
    <input type="text" name="post" id="">
    <button type="submit">Submit!</button>
  </form>
</body>
</html>
  • 送信前
    form送信前

  • 送信後
    form送信後

formのレスポンスはContent-Type: text/plainなのでhtmlタグやscriptをpostしても実行はされないものの、postされたbodyのチェックは行っていないので、

<script>alert('foobar')</script>(XSS)
xss

1 ' or ' 1 ' = ' 1;(SQLi)
slqi
といった内容がpostできてしまいます。

そこで、Google Cloud Armorにより事前構成されたルールのうち以下2つを適用し、攻撃を防げるのか確認していきます。

  • xss-stable
  • sqli-stable

Google Cloud Armor security policy の作成

ここでは、test-security-policyという名前で作成していきます。なので、その部分は読み替えていただければと。
ルールの優先度は1000としていますが、0~2,147,483,646であれば何でも構いません。
今回適用する2つのルールに優先度は特にないので、まとめて1つのルールとします。

やり方としては以下の3つがありますが、下2つだけ記述します。

  • GCPコンソール
  • gcloud
  • terraform

gcloudで作成

gcloudから行う場合、

  1. security policy 作成
  2. security policy に適用するルールの作成

の2ステップとなります。詳しくは公式ドキュメント参照。

gcloud compute security-policies create test-security-policy \
  --description "security policy for test"

gcloud compute security-policies rules create 1000 \
  --security-policy test-security-policy
  --expression "evaluatePreconfiguredExpr('xss-stable') || evaluatePreconfiguredExpr('sqli-stable')"
  --action "deny-403"

terraformで作成

terraformが利用するサービスアカウントには(Update,Deleteも考慮して)以下の権限が必要です。

  • compute.securityPolicies.get
  • compute.securityPolicies.create
  • compute.securityPolicies.update
  • compute.securityPolicies.delete

そしたら、以下のような定義ファイルを用意してapplyします。

main.tf
provider "google" {
  project     = "PROJECT_ID"                       # 自身のGCP project id
  credentials = "path/to/serviceaccount-key.json"  # file指定じゃなくて環境変数利用する場合は不要
}

resource "google_compute_security_policy" "security-policy" {
  name        = "test-security-policy"
  description = "security policy for test"

  rule {
    action   = "deny(403)"
    priority = "1000"
    match {
      expr {
        expression = "evaluatePreconfiguredExpr('xss-stable') || evaluatePreconfiguredExpr('sqli-stable')"
      }
    }
  }

  rule {
    action   = "allow"
    priority = "2147483647"
    match {
      versioned_expr = "SRC_IPS_V1"
      config {
        src_ip_ranges = ["*"]
      }
    }
    description = "default rule"
  }
}

1つ注意点として、
コンソールあるいはgcloudで作成した場合は自動的に優先度2147483647のデフォルトルールが作成される一方で、
terraformから作成する場合は自身で優先度2147483647のデフォルトルールを定義する必要があります。
(コンソールやgcloudでの作成時に自動作成されるすべてのIPアドレスを許可ルールを定義しています)

GKE 設定

前提として、GKEクラスタは作成済とします。

マニフェスト(Before)

Security Policy適用前のマニフェストは以下の通りです。

apiVersion: v1
kind: Namespace
metadata:
  name: cloud-armor-test
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: cloud-armor-test
  name: test-deployment
spec:
  selector:
    matchLabels:
      app: go-simple-form
  replicas: 2
  template:
    metadata:
      labels:
        app: go-simple-form
    spec:
      containers:
      - name: go-simple-form
        image: gcr.io/PROJECT_ID/go-simple-form:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
          protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  namespace: cloud-armor-test
  name: test-service
spec:
  type: NodePort
  selector:
    app: go-simple-form
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  namespace: cloud-armor-test
  name: test-ingress
spec:
  backend:
    serviceName: test-service
    servicePort: 80

BackendConfig 作成

GKEでGoogle Cloud Armor security policyを利用するには、CRDのBackendConfigを使用します。

https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-features?hl=ja#cloud_armor
backendconfig.yaml
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  namespace: cloud-armor-test
  name: test-backendconfig
spec:
  securityPolicy:
    name: test-security-policy  # さっき作成したsecurity policyの名前を指定

Service にアノテーションを追加

上で作成したBackendConfigをServiceリソースのアノテーションで参照することで、securiy policyを適用することができます。

https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-features?hl=ja#associating_backendconfig_with_your_ingress
service.yaml
apiVersion: v1
kind: Service
metadata:
  namespace: cloud-armor-test
  name: test-service
+   annotations:
+   cloud.google.com/backend-config: '{"default": "test-backendconfig"}'  # backendconfig name を指定
spec:
  type: NodePort
  selector:
    app: go-simple-form
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80

テスト

以上で適用は完了したので、実際のところどうなるのかテストします。
Ingress のマニフェストで静的IPの指定はしていないので、自動でGCLBに振られたIPを確認し、ブラウザから接続します。

$ kubectl get ingress
NAME           CLASS    HOSTS   ADDRESS          PORTS   AGE
test-ingress   <none>   *       34.117.217.233   80      11h

XSS

XSS送信前

XSS送信後

SQLi

SQLi送信前

SQLi送信後

無事どちらもWAFでブロックすることができました!
(post内容が403画面から分からないのでエビデンスとしてちょっと不十分ですが動画撮るほどでもないので...)