Cloud ArmorでGKE IngressへのアクセスをIPで制御する

# やりたかったこと
GKEのIngress (GCLB)でサービスエンドポイントを提供する際に、アクセスを許可するIPをホワイトリストで制御したい

# やったこと（結論）
Cloud ArmorとGKE Ingressをkubernetesカスタムリソースである`BackendConfig`を使って紐付けることで実現します

### 2021-10-31 追記

この記事で紹介している `apiVersion: cloud.google.com/v1beta1` を使用すると、特定バージョンの GKE において Service からアクティブな Google Cloud Armor セキュリティ ポリシーが削除されるという既知の問題があります。該当する GKE バージョンの場合は、 `apiVersion: cloud.google.com/v1` を使うようにしましょう。

影響する GKE バージョンは以下のとおりです。

* 1.18.19-gke.1400～1.18.20-gke.5099
* 1.19.10-gke.700～1.19.14-gke.299
* 1.20.6-gke.700～1.20.9-gke.899


Ref. [基地の問題 - Google Cloud Armor セキュリティ ポリシー フィールドが BackendConfig から削除されました](https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-features#armor_fields_removed)

#### やったことリスト

1. Cloud ArmorでIPホワイトリストルールを作成する
2. GKE Ingress用の静的IPアドレスを確保する
3. k8sクラスタにサンプルアプリケーションをデプロイする
4. k8sクラスタにBackendConfigをデプロイする
5. k8sクラスタにアプリケーションのサービスリソースをデプロイする
6. k8sクラスタにIngressリソースをデプロイする

#### 構築ポイント
1. 作成したCloud Armorのポリシー名を、`BackendConfig`というカスタムリソースの`spec`セクションで指定する
2. Serviceリソースの`annotations`セクションで、`beta.cloud.google.com/backend-config`を使って、サービスポートと`BackendConfig`を紐づけする

# 前置き

## Cloud Armorって??
GoogleCloudPlatformで提供されるGoogle Cloud Load Balancing(GCLB)へのアクセス制御と防御を実現するネットワークセキュリティサービスです。Cloud Armorについては以下の公式ブログがとても参考になります。

![image](https://3.bp.blogspot.com/-CnJMiKTPDSQ/Wrhnnjg6l8I/AAAAAAAAFNQ/MPwvhY_icfkFL6BFZLv_Kp7UMPfVXaRzgCEwYBhgL/s1600/image2.png "image")
[Cloud Armor : インターネットに接続されたサービスを DDoS 攻撃から防御](https://cloudplatform-jp.googleblog.com/2018/05/getting-to-know-Cloud-Armor-defense-at-scale-for-internet-facing-services.html)

GKE IngressもバックエンドはGCLBのため、Cloud ArmorのネットワークポリシーをGKE Ingressリソースに紐付けることでGKE Ingressで構築したサービスエンドポイントへのアクセスを制御できます。

## 参考:Nginx Ingress Controllerの場合のIP制限
Nginx Ingress Controllerでは、以下のようなアノテーションがあるので簡単にIP制限を設定できるかも（やったことない）
https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#whitelist-source-range

>You can specify allowed client IP source ranges through the nginx.ingress.kubernetes.io/whitelist-source-range annotation. The value is a comma separated list of CIDRs, e.g. 10.0.0.0/24,172.10.0.1.

# やってみる

### GKE IngressとCloud Armorの設定方法
以下の公式ドキュメントの通りに設定すればOK
https://cloud.google.com/kubernetes-engine/docs/how-to/cloud-armor-backendconfig

簡単に言うと、`1.10.5-gke.3`以降のkubernetesクラスタで提供される`BackendConfig`と呼ばれるカスタムリソースで設定を行うことで、サービスポートにロードバランサーの構成を追加し、Cloud Armorのネットワークと紐づけます。

なお、この記事の公開時(2019/1/2)では`BackendConfig`はベータ版での提供となっています。
また、ネットワークルールはIPのホワイトリスト/ブラックリストの2つが提供されています。

この記事ではIPホワイトリストでのアクセス許可設定を行っています。

#### Cloud ArmorでIPホワイトリストルールを作成する

```bash

$ gcloud beta compute security-policies create ingress-ip-whitelist --description "sample"
```

`gcloud beta compute security-policies create`コマンドだとデフォルトルールが`allow`になってしまうので、ホワイトリストIPルールにするために`gcloud compute security-policies rules update`コマンドでデフォルトルールを`deny-403`に変更。（`2147483647`はデフォルトルールの`PRIORITY`）
また、`PRIORITY=10000`で許可IPアドレスのルールを作っておきます。

```bash

$ gcloud compute security-policies rules update 2147483647 \
    --security-policy ingress-ip-whitelist \
    --action "deny-403"

$ gcloud compute security-policies rules create 10000 \
    --security-policy ingress-ip-whitelist \
    --description "allow traffic from 192.0.2.0/24,172.16.3.0/24" \
    --src-ip-ranges "192.0.2.0/24,172.16.3.0/24" \
    --action "allow"
```

`--src-ip-ranges`は`,`区切りで複数のネットワークアドレスを記載できますが、1ルールにつき5つまでという制約があります。5つより多くのネットワークアドレスを指定したい場合は別途新しいルールを作りましょう。

なお`--action`には`allow` `deny-403` `deny-404` `deny-502`を指定することができます。

参考:https://cloud.google.com/sdk/gcloud/reference/beta/compute/security-policies/

#### GKE Ingress用の静的IPアドレスを確保する

アプリケーションエンドポイントのIPアドレスを固定するための静的IPアドレスを確保します。

```bash

gcloud compute addresses create sample-app-endpoint --global
```


#### k8sクラスタにサンプルアプリケーションをデプロイする

```yaml:app-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: sample-app
spec:
  selector:
    matchLabels:
      app: hello-app
  replicas: 2
  template:
    metadata:
      labels:
        app: hello-app
    spec:
      containers:
      - name: hello-app-container
        image: gcr.io/google-samples/hello-app:1.0
        ports:
        - containerPort: 8080
```

```bash
$ kubectl apply -f app-deployment.yaml
```

#### k8sクラスタにBackendConfigをデプロイする

ポイント1: 作成したCloud Armorのポリシー`ingress-ip-whitelist`を、BackendConfigのspecセクションで指定します。

```yaml:backend-config.yaml
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  namespace: default
  name: default-backend-config
spec:
  securityPolicy:
    name: "ingress-ip-whitelist"
```

```bash
$ kubectl apply -f backend-config.yaml
```

#### k8sクラスタにサンプルアプリケーションのサービスリソースをデプロイする

ポイント2:Serviceリソースの`annotation`セクションで、`beta.cloud.google.com/backend-config`を使ってサービスポートとBackendConfigを紐づけます。

```yaml:app-service.yaml
apiVersion: v1
kind: Service
metadata:
  namespace: default
  name: sample-app-service
  labels:
    app: hello-app
  annotations:
    beta.cloud.google.com/backend-config: '{"ports": {"80":"default-backend-config"}}'
spec:
  type: NodePort
  selector:
    app: hello-app
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
```

```bash
$ kubectl apply -f app-service.yaml
```

#### k8sクラスタにIngressリソースをデプロイする

ポイント3: Ingressリソースの`annotations`セクションで、`kubernetes.io/ingress.global-static-ip-name`を使ってアプリケーションエンドポイント用に確保した静的IPアドレス名`sample-app-endpoint`を設定します。

```yaml:app-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  namespace: default
  name: sample-app-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "sample-app-endpoint"
spec:
  backend:
    serviceName: sample-app-service
    servicePort: 80
```

```bash
$ kubectl apply -f app-ingress.yaml
```

## やってみた結果
### 設定後

以下のようにIngress/Service/Cloud Armorが設定されました。

Ingress
<img width="769" alt="q_gke_ingress.png" src="https://qiita-image-store.s3.amazonaws.com/0/29276/bed0957b-81e8-e891-afca-ee3bc5796a56.png">

Service
<img width="1164" alt="q_gke_service_annotations.png" src="https://qiita-image-store.s3.amazonaws.com/0/29276/9ac9efc1-aaff-b024-ed3e-a430cb8c7170.png">

Cloud Armor(Rule)
<img width="900" alt="q_policy_rule.png" src="https://qiita-image-store.s3.amazonaws.com/0/29276/c2518c5b-c22a-a04a-c1d8-12930d4976df.png">

Cloud Armor(Target)
<img width="874" alt="q_policy_target.png" src="https://qiita-image-store.s3.amazonaws.com/0/29276/2d6e2c06-1447-d25f-5d86-bcfcd0b66c5b.png">

なんとなくネットワークポリシーがロードバランサに紐付いていることがわかります。

### アクセス制御の挙動
許可されたIPアドレスからは想定通りアプリケーションの画面が表示されました。
<img width="301" alt="q_app_allowed_ip.png" src="https://qiita-image-store.s3.amazonaws.com/0/29276/e115efb9-a3cd-2fb3-56f3-b3cc3e9d0a62.png">

許可していないIPアドレスでアプリケーションにアクセスすると、とってもシンプルな画面でアクセスが拒否されました。
<img width="170" alt="q_app_denied_ip.png" src="https://qiita-image-store.s3.amazonaws.com/0/29276/05e8ad68-a26b-2693-1c81-5405a7034fc3.png">

試してないですが、他の拒否ルールである`404` `502`も、とってもシンプルな画面でアクセスが拒否されるはず。

アクセスが拒否されたアクセスログはStackdriver Loggingで確認することができました。

とっても簡単でした。