Azure Application Gateway で送信元 IP に応じて振り分けを行う


Azure の負荷分散を行う機能で、送信元 IP アドレスやヘッダー等の属性に応じて振り分けを行う機能は Azure Front Door (グローバル) と Application Gateway (リージョン内) の二つがあります。Azure Front Door については以下のルールエンジンを行うことで簡単に設定することができます。

https://docs.microsoft.com/ja-jp/azure/frontdoor/front-door-rules-engine

Application Gateway についても以下に情報はありますが、パスベースのルーティング規則と URL 書き換えを組み合わせてルーティングを行うため、設定が少し複雑で、いくつか留意点があります。実際に設定を行い動作を確認した際のポイントを記載していきます。

https://docs.microsoft.com/ja-jp/azure/application-gateway/rewrite-http-headers-url#common-scenarios-for-url-rewrite

完成イメージ図 (例として IP アドレスで振り分けてますが、ヘッダーやクエリストリングも指定できます)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/eb390a7e-bd03-c40f-f299-509430c4e2f2.png)
※本記事で記載されている IP アドレスは例として使用しているものですので、実際にはアクセスしないようご留意ください。

<font color="Red">書き換えルールセットで指定できる値は上限がありますので、要件を満たせるかは事前に確認が必要です。</front>



|リソース|制限|	
|---|---|
|書き換えルール セットの数 |400 |
|書き換えルール セットごとのヘッダーまたは URL 構成の数 |40 |
|書き換えルール セットごとの条件の数 |40 |

https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/management/azure-subscription-service-limits#application-gateway-limits

## 1. バックエンド VM の準備
Azure VM を 2 台作成し、Web サーバーのサービスを稼働させておきます。
さらに以下の様にそれぞれの VM にディレクトリを一つ作成し、/testX/ というパスでアクセスできるようにしておきます。

|バックエンドプール名 | VM 名 | パス |
|---|---|---|
|backend | VM1,VM2 ※ | なし |
|backend1 | VM1 | /test1/index.html |
|backend2 | VM2 | /test2/index.html |

上記のような構成にする理由は、書き換え規則の "パスマップの再評価" と HTTP 設定の "パスのオーバーライド" は排他となっており、どちらかしか設定できないため、パスベースのルーティングが必須となるからです。
今回の要件ではパスマップの再評価は必須であるため、/testX/ というパスでリクエストを受け付けられるようにバックエンドを構成しておきます。
<font color="Red">※ backend は既定のパスマップのルール用のプールですが、 VM を入れている理由は、後程設定する書き換え規則を関連付けするバックエンドプールが空っぽのままだと Status 502 となり、正しく書き換え規則の評価が行われないためです。</front>

## 2. Application Gateway のパスベースのルール作成
前提： 
- Application Gateway v2 を作成済み
- バックエンド用の Web サーバーを 2 つ作成済み

Application Gateway のルールで以下のようなパスベースのルールを作成します。構成するポイントとしてはバックエンドごとに特定のパスで振り分けるように設定します。
既定のバックエンドターゲットは基本的に利用されませんのでどんなバックエンドでも問題ありません (空のバックエンドでも可)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/1c0aa9f2-7b35-8f95-e5c0-138c6a913457.png)

## 3. 書き換え規則の作成

書き換えセットを作成します。<font color="Red">関連付けするルーティング規則は (既定の書き換え設定) のみを選びます。他のパス設定も選んでしまとエラーで保存することができません。書き換え規則がループすることを防ぐためだと考えられます。</front>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/7623e221-0d7d-fbd8-00c3-c66a09b4f791.png)


### 3-1. VM1 用の書き換え規則の作成
以下のような書き換え規則を作成していきます。

- 条件

|設定項目 |値 |
|---|---|
|チェックする変数の方 |サーバー変数 |
|サーバー変数 | client_ip |
|大文字と小文字を区別する |いいえ |
|演算子 | 等しい (=)|
|~一致させるパターン |送信元 IP アドレス1 |

- 結果

|設定項目 |値 |
|---|---|
|置き換えの種類 |URL |
|アクション タイプ |設定 |
|コンポーネント |URL パス |
|URL パスの値 |<font color="Red">/test1{var_request_uri}</front> |
|パスマップの再評価 |有効 |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/edb2bd34-d6c6-433c-5f98-8aa71ab319d3.png)


### 3-2. VM2 用の書き換え規則の作成

- 条件

|設定項目 |値 |
|---|---|
|チェックする変数の方 |サーバー変数 |
|サーバー変数 | client_ip |
|大文字と小文字を区別する |いいえ |
|演算子 | 等しい (=)|
|~一致させるパターン |送信元 IP アドレス2 |

- 結果

|設定項目 |値 |
|---|---|
|置き換えの種類 |URL |
|アクション タイプ |設定 |
|コンポーネント |URL パス |
|URL パスの値 |<font color="Green">/test2{var_request_uri}</front> |
|パスマップの再評価 |有効 |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/db85bae7-a2d9-8ad6-6605-913c1529838b.png)




設定が終わったら "作成" を押して書き換え規則を作成します。

## 4. 動作確認

送信元 IP アドレス1 のマシンから Application Gateway へアクセスします。
VM1 の /test1/index.html は linux1 と書いてあるため linux1 という文字列が表示されれば成功です。

```
$ curl -v http://20.xx.xx.218/
*   Trying 20.xx.xx.218...
* TCP_NODELAY set
* Connected to 20.xx.xx.218 (20.xx.xx.218) port 80 (#0)
> GET / HTTP/1.1
> Host: 20.xx.xx.218
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 23 Dec 2021 09:19:26 GMT
< Content-Type: text/html; charset=UTF-8
< Content-Length: 7
< Connection: keep-alive
< Server: Apache/2.4.6 (CentOS) PHP/5.4.16
< Last-Modified: Thu, 23 Dec 2021 07:19:59 GMT
< ETag: "7-5d3cb10f3ed2f"
< Accept-Ranges: bytes
<
linux1
* Connection #0 to host 20.xx.xx.218 left intact
```

バックエンドのパケットキャプチャも見てみます。
リクエストの URL が /test1/ となっており、X-Original-URL のヘッダー (書き換え前の URL) が "/" となっていることが確認できます。

```
09:19:26.054978 IP (tos 0x0, ttl 64, id 62765, offset 0, flags [DF], proto TCP (6), length 366)
    10.10.0.7.33148 > 10.10.1.4.80: Flags [P.], cksum 0xce74 (correct), seq 1:315, ack 1, win 63, options [nop,nop,TS val 3780623255 ecr 744405978], length 314: HTTP, length: 314
        GET /test1/ HTTP/1.1
        X-FORWARDED-PROTO: http
        X-FORWARDED-PORT: 80
        X-Forwarded-For: 20.xx.xx.17:39052, 20.xx.xx.17:39052
        X-Original-URL: /
        Connection: keep-alive
        X-AppGW-Trace-Id: 7a6946dee8c0c02432774e829ad7919b
        Host: 20.xx.xx.218
        X-ORIGINAL-HOST: 20.xx.xx.218
        User-Agent: curl/7.61.1
        Accept: */*

09:19:26.055002 IP (tos 0x0, ttl 64, id 2927, offset 0, flags [DF], proto TCP (6), length 52)
    10.10.1.4.80 > 10.10.0.7.33148: Flags [.], cksum 0x1545 (incorrect -> 0xa524), seq 1, ack 315, win 235, options [nop,nop,TS val 744405979 ecr 3780623255], length 0
09:19:26.055401 IP (tos 0x0, ttl 64, id 2928, offset 0, flags [DF], proto TCP (6), length 365)
    10.10.1.4.80 > 10.10.0.7.33148: Flags [P.], cksum 0x167e (incorrect -> 0x4025), seq 1:314, ack 315, win 235, options [nop,nop,TS val 744405980 ecr 3780623255], length 313: HTTP, length: 313
        HTTP/1.1 200 OK
        Date: Thu, 23 Dec 2021 09:19:26 GMT
        Server: Apache/2.4.6 (CentOS) PHP/5.4.16
        Last-Modified: Thu, 23 Dec 2021 07:19:59 GMT
        ETag: "7-5d3cb10f3ed2f"
        Accept-Ranges: bytes
        Content-Length: 7
        Keep-Alive: timeout=5, max=100
        Connection: Keep-Alive
        Content-Type: text/html; charset=UTF-8

        linux1
```

同様に送信元 IP アドレス2 のマシンから確認し、VM2 へアクセスしていることも確認できました。

## 5. 備考
この構成の追加の留意点として、以下の様に最初からパスベースのルールに合致するような URL を指定した場合、書き換え規則に合致することなくそのままパスベースでルーティングされることを確認しております。つまり、送信元やヘッダー等による制御はできないことになります。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/1e56c38d-4bb0-34fd-0fb4-14ec44141864.png)

このようなアクセスを防ぎたい場合、以下のような方法が有効であると思います。

- ユーザーが予測しにくいディレクトリ構成とする
- WAF ポリシーを使用して特定の条件で拒否する
- 書き換え規則を追加して対処する

### 対策 1. WAF ポリシーで対応
URL の書き換え規則に独自ヘッダーを追加するアクションを追加してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/e88a7127-d524-1ff0-a85c-3bc6472bf1d0.png)

そして、WAF ポリシーを Application Gateway に紐づけ該当のヘッダーがある場合は、許可するようにします。WAF ポリシーは書き換え規則の後に評価されるため、追加したヘッダーを WAF ポリシーの条件にすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/2b4c1f3c-cab5-8f39-e564-1a1863386b59.png)

もう一つ、優先度を低くした WAF ポリシーですべてのアクセスを拒否するようなルールを作成します。こちらの例ですと、User-Agent があれば拒否としています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/d2eb630f-40a2-8d7f-d136-346805d3db0d.png)

こうすることで既定の書き換え規則でヘッダーが加わってないアクセスは拒否することができました。

```
$ curl -v http://20.xx.xx.218/test1/
*   Trying 20.xx.xx.218:80...
* TCP_NODELAY set
* Connected to 20.xx.xx.218 (20.xx.xx.218) port 80 (#0)
> GET /test1/ HTTP/1.1
> Host: 20.xx.xx.218
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< Server: Microsoft-Azure-Application-Gateway/v2
< Date: Fri, 24 Dec 2021 11:36:26 GMT
< Content-Type: text/html
< Content-Length: 179
< Connection: keep-alive
<
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>Microsoft-Azure-Application-Gateway/v2</center>
</body>
</html>
* Connection #0 to host 20.xx.xx.218 left intact
```

### 対策 2. 書き換え規則を追加して対応

対策 1 同様 URL の書き換え規則に独自ヘッダーを追加するアクションを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/f1e0470f-3058-af62-3964-260761b817a5.png)

新しくパスベースの規則にルールを書き換え規則を追加します。パスマップの再評価を使わなければ書き換え規則を使うことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/271ece44-f36e-cc0a-2af8-43329f4f2417.png)

独自のヘッダーがある場合のみパスを元々の URL に変更するような構成とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/88553081-3031-d9d4-ff97-444ec66a4e42.png)


この構成にすることで /test1/, /test2/ といったパスは使われなくなり、パスベースのルーティング後に / としてバックエンドにアクセスしますので、/test1/, /test2/ のディレクトリを削除しておけば、直接アクセスするような構成の場合 "Status 404 Not Found" とすることができます。

```
$ curl -v http://20.xx.xx.218/test1/
*   Trying 20.xx.xx.218:80...
* TCP_NODELAY set
* Connected to 20.xx.xx.218 (20.xx.xx.218) port 80 (#0)
> GET /test1/ HTTP/1.1
> Host: 20.xx.xx.218
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< Date: Fri, 24 Dec 2021 12:11:49 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 204
< Connection: keep-alive
< Server: Apache/2.4.6 (CentOS) PHP/5.4.16
<
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /test1/ was not found on this server.</p>
</body></html>
* Connection #0 to host 20.xx.xx.218 left intact