Application Gateway (WAF) でAzure API ManagementのProxyとPortalを保護する

# はじめに
タイトル通りですが、Azure Application Gateway(以下AppGW)の構成要素がわかりづらかったり、
Azure API Management(以下APIM)の独特な動きでハマって時間がかかったので、
備忘を兼ねてbraindump。

# 実現したいこと
* APIMで、APIエンドポイント(以下Proxy)、開発者ポータル(以下Portal)を公開する
* Proxy,Portalの両公開エンドポイントにはAppGWでWAFを適用する
* イメージ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/85ac50b6-ba2a-b843-8f62-3d71c931323e.png)

# AppGWの機能・構成要素のざっくり説明
Application Gateway はレイヤー 7 のロード バランサー
SSL ターミネーション、Cookie ベースのセッション アフィニティ、ラウンド ロビンによるトラフィックの負荷分散、WEBアプリケーションファイアーウォールなどの機能がある
サイズはSmall、Medium、Largeの3タイプ、SLAはMedium/Largeかつ２インスタンス以上が対象なので、見積もりとかで注意

## サブネット
AppGWはサブネットを占有する
AppGWにプライベートIPを割り当てる場合、/29 以上、割り当てない場合、/28 以上のサイズが必要

## フロントエンドIP(FrontendIPConfiguration)
入口となるプライベートIPまたは、PublicIPリソース(xxxx.region.cloudapp.azure.com)を割り当てる
**[現時点では](https://feedback.azure.com/forums/217313-networking/suggestions/10754025-enable-multiple-ip-addresses-for-azure-application)**1AppGWあたり一つのPublicIPの制限がある
PublicIPは動的のみの為、基本的にはDNSはCNAMEレコードで設定することとなる
(Application Gateway を停止/起動すると、VIPが変更される)

## フロントエンドポート(FrontendPort)
入口のPublicポート
任意指定可能（ふつうは80,443だと思われる)

## リスナー(HttpListener)
フロント側の設定をまとめる要素
フロントエンドIP、フロントエンドポート、プロトコル、SSL証明書を紐づける
マルチサイト(マルチドメイン)構成の場合、ホスト名を指定する
プロトコルがHTTPSなら、秘密鍵証明書(PFX)を指定する
ホスト名指定＝マルチサイト(とPortal上は呼ばれる)(マルチドメイン構成)

## バックエンドプール(BackendAddressPool)
複数作成可能
NIC、仮想マシン スケール セット、パブリック IP、内部 IP、完全修飾ドメイン名 (FQDN)、Azure Web Apps が設定できる
**Azureの外部に出る場合、アウトバウンドIPはAppGWに割り当てたパブリックIPとなる**

## HTTP設定(BackendHttpSetting)
バックエンドへアクセスする際のHTTP設定(日本語の名前、もう１単語欲しいよね わかりづらい）
以下の項目を設定する

* プロトコル(HTTPorHTTPS)
  * HTTPSなら、バックエンドの証明書公開鍵(CER)を指定する（再暗号化）
* ポート
* Cookieベースアフィニティ
    * Cookie ベースのセッション アフィニティ
    * バックエンドが単一IPならば無意味(おそらく)
* 接続のドレイン
    * 正常性プローブによって、バックエンドノードが切り離される際、接続が終わるまで維持する(新規コネクションは張らない)
* バックエンドパスのオーバーライド
    * Rewriteではない(パスの上書きロジック、変な挙動する(気がする))
* カスタムプローブの使用
  * 正常性プローブのカスタム設定を選択
* ホスト名
  * バックエンドプールへリクエスト転送時、ホストヘッダの変更を行う
  * PowerShell/ARMでの設定は可能。
  * ポータルで設定不可、閲覧不可
  * PickHostNameFromBackendAddressオプションがあり、バックエンドプールが(複数の?)ホスト名の場合、その値を使用することが可能

## ルール(RequestRoutingRules)
BASICとパスベースの２種類ある
リスナー、バックエンドプール、HTTP設定を紐づける
BASICは単一のバックエンド/HTTP設定を紐づける
パスベースはパス毎にバックエンド/HTTP設定を紐づける

## 正常性プローブ(Probe)
バックエンドの正常性を確認するプローブのうち、既定以外の正常性プローブを設定したい場合に作成する
ホスト名、プロトコル、パス、チェック間隔、タイムアウト等を設定する
PickHostNameFromBackendHttpSettingsオプションがあり、HTTP設定のホスト名を引き継ぐことも可能


## WAF
Web アプリケーション ファイアウォール
AppGWのサイズ( Small、Medium、Large)のうちMedium、Largeのみ使用可能。
BASIC(WAFなし)と比べるとおおよそ倍の価格となる
OWASP コア ルール セット 3.0 または 2.2.9の規則に対応
検出モード/防止モードがある

# APIMの構成要素のざっくり説明
## 価格レベル
DEVELOPER/BASIC/STANDARD/PREMIUMの４つ
特筆すべきは、仮想ネットワークサポートはDEVELOPER/PREMIUMのみである点
今回は、PortalにもWAFを適用したいので、VNET参加は必須となる
（Proxyのみであれば、ポリシーで制御可能(なはず)）
## VNET
外部/内部の２つのモード
外部＝PublicIPあり＋VNETエンドポイント
内部＝VNETのみ（既定のドメインは、パブリック DNS サーバーには登録されない）
## 既定のホスト名
* 管理:xxx.management.azure-api.net
* ポータル:xxx.portal.azure-api.net
* プロキシ:xxx.azure-api.net
* SCM:xxx.scm.azure-api.net

# できあがった構成の詳細
## 構成図
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/e809d48d-429c-5b23-e477-57e512534c96.png)

## フロントエンドIP
パブリックIPリソースを指定する
DNSのCNAMEレコードで、ドメインが解決できるようにする

* portal.example.com->example.japaneast.cloudapp.azure.com
* api.example.com->example.japaneast.cloudapp.azure.com

## フロントエンドポート
HTTPリダイレクトの構成は不要の為、443のみ作成
## HTTPリスナー①
|項目|値|
|:--|:--|
|FrontendIPConfiguration|フロントエンドIP|
|FrontendPort|フロントエンドポート(443)|
|Protocol|HTTPS|
|SSLSertificate|portal.example.com(pfx)|
|HostName|portal.example.com|
|RequireServerNameIndication|true|
## HTTPリスナー②
|項目|値|
|:--|:--|
|FrontendIPConfiguration|フロントエンドIP|
|FrontendPort|フロントエンドポート(443)|
|Protocol|HTTPS|
|SSLSertificate|api.example.com(pfx)|
|HostName|api.example.com|
|RequireServerNameIndication|true|

## バックエンドプール
APIMのプライベートIPアドレス
(10.0.1.0/24にAPIMデプロイすると10.0.1.5になる(1-3は予約されるので、常に2個目?ﾖｸﾜｶﾗﾝ))
(apim.staticIPs[0]でもとれるので、依存関係アリのARMTemplateにしても良いかも)

## バックエンドHTTP設定①
|項目|値|
|:--|:--|
|Port|443|
|Protocol|Https|
|CookieBasedAffinity|Disabled|
|HostName|**portal.example.com**|
|PickHostNameFromBackendAddress|**false**|
|RequestTimeout|180|
|Probe|APIM正常性プローブ|
|AuthenticationCertificate|portal.example.com(cer)**<br>api.example.com(cer)**|
## バックエンドHTTP設定②
|項目|値|
|:--|:--|
|Port|443|
|Protocol|Https|
|CookieBasedAffinity|Disabled|
|HostName|**api.example.com**|
|PickHostNameFromBackendAddress|**false**|
|RequestTimeout|180|
|Probe|APIM正常性プローブ|
|AuthenticationCertificate|api.example.com(cer)|

## APIM正常性プローブ
|項目|値|
|:--|:--|
|Protocol|Https|
|Host|**api.example.com**|
|Path|**/status-0123456789abcdef**|
|Interval|30|
|Timeout|120|
|UnhealthyThreshold|8|
|PickHostNameFromBackendHttpSettings|**false**|
|MinServers|0|
|Match-body|(empty)|
|Match-statusCodes|200-399|

## リクエストルーティングルール①
|項目|値|
|:--|:--|
|RuleType|Basic|
|HttpListener|HTTPリスナー①|
|BackendAddressPool|APIMバックエンドプール|
|BackendHttpSettings|バックエンドHTTP設定①|

## リクエストルーティングルール②
|項目|値|
|:--|:--|
|RuleType|Basic|
|HttpListener|HTTPリスナー②|
|BackendAddressPool|APIMバックエンドプール|
|BackendHttpSettings|バックエンドHTTP設定②|

# ハマったポイント
1. **APIMは適切なホストヘッダー(SNI)を持つ要求だけに応答する**点
    * バックエンドHTTP設定
        * 今回のようにバックエンドアドレスプールがIPの場合、ホスト名を指定する必要がある
        * (ホスト名はPortalで設定/閲覧 出来ない)
    * 正常性プローブ
        * →既定の正常性プローブはhttp(s)://backendip/にアクセスする(ホスト名を指定しない)ので、使用不可
        * (+proxyは/で200を返さない)
1. **APIMは/status-0123456789abcdefというプローブ用のURLを提供している**点 ※2
    * ドキュメントが見つからない。言及しているのは[リリースノート](https://blogs.msdn.microsoft.com/apimanagement/2017/02/23/release-notes-february-23-2017/)と[Docs内のVNETAPIM-AppGWの記事](https://docs.microsoft.com/ja-jp/azure/api-management/api-management-howto-integrate-internal-vnet-appgateway)くらい
    * Portalのホスト名を指定したカスタム正常性プローブ(/status-0123456789abcdef)は動作しなかった。
    * →Portal側のカスタム正常性プローブもHostをProxy側に向ける
1. **正常性プローブは接続先をHTTP設定のAuthenticationCertificateを使用して認証する**点
    * Portal側のHTTP設定①にもProxyの証明書の指定が必要となる ※1
 
# 出来上がったARMTemplate

[template](https://github.com/HiroyukiSakoh/QiitaAttachments/blob/master/2018/07/AppGW-APIM/template.json)
[parameter](https://github.com/HiroyukiSakoh/QiitaAttachments/blob/master/2018/07/AppGW-APIM/parameters.json)

```powershell:PFXファイルからPFXとCERのBASE64エンコード.ps1
$HostName = "portal.example.com"
Get-PfxCertificate -FilePath ($HostName+".pfx") | Export-Certificate -FilePath ($HostName+".cer") -Type CERT
$PfxBytes = Get-Content ($HostName+".pfx") -Encoding Byte
[System.Convert]::ToBase64String($pfxBytes) | Out-File ($HostName+".pfx_base64.txt")
$CerBytes = Get-Content ($HostName+".cer") -Encoding Byte
[System.Convert]::ToBase64String($CerBytes) | Out-File ($HostName+".cer_base64.txt")
```

# おわりに
trial and errorで試そうにも、AppGW/APIMの構成変更に時間がかかりすぎて全然進まなかった
もっと構成変更素早くしてほしい
時間かかるだけじゃなく、途中でリソースが死んでサポートに連絡したり
MSDNの無料枠使い切りそうになったり

ドキュメント類もなんか足りない
シナリオに対する手順ベースに記述されてばかりで、各構成要素の意味を解説する記事があまりない
`pickHostNameFromBackendHttpSettingsとかPickHostNameFromBackendHttpSettingsとか`
`パスベースルーティングのパス書き換えの仕様も謎`
`あと、既定の正常性プローブのアクセス先ホスト名が127.0.0.1ってほんと意味わからんかった`

あと、AppGWははやくRewriteサポートすべき
WAFも嬉しいが、真に必要なのはマネージドなリバプロだったりする
AppGWはARR使ってる?のになんで対応が遅いのか（↓2016/2に開かれてる）
[Support URL rewriting with Application Gateway – Customer Feedback for Microsoft Azure](https://feedback.azure.com/forums/217313-networking/suggestions/12239826-support-url-rewriting-with-application-gateway)

# 参考文献
## 公式ドキュメント
### APIM
* [内部 VNET 内の API Management と Application Gateway の統合](https://docs.microsoft.com/ja-jp/azure/api-management/api-management-howto-integrate-internal-vnet-appgateway) ([英語版はこちら](https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-integrate-internal-vnet-appgateway))
  * →Proxyのみで、Portalを公開する方法までは示されていない。
* [SNI ヘッダーを使用せずに呼び出すクライアント](https://docs.microsoft.com/ja-jp/azure/api-management/configure-custom-domain#clients-calling-without-sni-header)

### AppGW
* [Application Gateway での無効なゲートウェイによるエラーのトラブルシューティング](https://docs.microsoft.com/ja-jp/azure/application-gateway/application-gateway-troubleshooting-502) 
* [Application Gateway によるマルチテナント バックエンドのサポート](https://docs.microsoft.com/ja-jp/azure/application-gateway/application-gateway-web-app-overview)
* [Application Gateway による正常性監視の概要](https://docs.microsoft.com/ja-jp/azure/application-gateway/application-gateway-probe-overview) 

## その他
* [Azure API Management in VNET with Gateway (502 - Web server received an invalid response while acting as a gateway or proxy server)](https://stackoverflow.com/questions/42752819/azure-api-management-in-vnet-with-gateway-502-web-server-received-an-invalid/42754855#42754855)
* [qiita Azure Application Gateway は PowerShell で作るもの](https://qiita.com/tetsuya-ooooo/items/cb86e44fdf12c6be4413)