Azure Web Apps + Azure Let's Encrypt の環境で送信元IPアドレスを制限する

# はじめに
自身の学習用にAzure Web Appsを使ってWebアプリを立てています。
Webアプリへの通信をHTTPS化するため、Let's Encryptによる無料証明書を発行し、Webアプリに食わせています。
Let's Encryptは３ヶ月に１回の頻度で証明書の更新を行う必要がありますが、
Azure Web Appsには便利な拡張機能として「[Azure Let's encrypt](https://github.com/sjkp/letsencrypt-siteextension)」があり、本機能を利用することで煩わしい更新作業を自動化することができます。

####※参考記事※
Azure Web Apps + Azure Let's Encryptの実装方法については別記事を引用しておきます。
● 　[Let’s Encrypt を使って Azure Web Apps でのhttps通信を設定する](https://blogs.technet.microsoft.com/mskk-cloudos/2016/02/22/lets-encrypt-azure-web-apps-https/)
● 　[Securing an Azure App Service Website under SSL in minutes with Let's Encrypt](https://www.hanselman.com/blog/SecuringAnAzureAppServiceWebsiteUnderSSLInMinutesWithLetsEncrypt.aspx)

# 本題
私のAzure上Webアプリはプライベート用途なので限られた環境からのみアクセスできるよう、送信元IPアドレスの制限をかけようと試みました。
まず最初に[Azure App Service のアクセス制限](https://docs.microsoft.com/ja-jp/azure/app-service/app-service-ip-restrictions)を利用したのですが、３ヶ月後にLet's Encryptの証明書の更新が失敗する問題が発生しました。
その際に別の方法を使って解決することができましたので備忘録として残しておきます。

# 環境
- Azure Web Apps
- Webアプリ（ASP.NET MVC 5）

# 証明書更新ができなくなった原因
[Azure App Service のアクセス制限](https://docs.microsoft.com/ja-jp/azure/app-service/app-service-ip-restrictions)により、Let's Encryptサーバ → Webアプリへの通信が遮断されていたことが原因でした。
[Azure Let's encrypt](https://github.com/sjkp/letsencrypt-siteextension)を使って証明書を発行／更新する場合、
Let's Encryptサーバが対象Webアプリの特定のディレクトリに対してACMEチャレンジ通信(http接続)を行うのですが、
IPアドレス制限によりアクセスできず、更新に失敗していました。(盲点でした、、）

●　　[Let's Encryptのチャレンジの仕様](https://letsencrypt.org/ja/docs/challenge-types/)


# どうするか？
改めてやりたいことを整理します。
　①公開Webアプリの画面には許可された送信元IPのみ接続を許可する
　②Let's Encryptとの証明書更新通信は許可する
　③それ以外の通信は遮断する

上記を実現するため、最初に[Azure App Service のアクセス制限](https://docs.microsoft.com/ja-jp/azure/app-service/app-service-ip-restrictions)にLet's EncryptサーバのIPアドレス／FQDNを登録できれば解決できると考えましたが、
Let's EncryptのIPアドレスは公開されておらず（そもそもIPが動的に変わるためIPによる制限は非推奨とのこと）、
また、[Azure App Service のアクセス制限](https://docs.microsoft.com/ja-jp/azure/app-service/app-service-ip-restrictions)はFQDNの登録が不可のため、
①と②を実現するには別の手段を使って対応する必要がありました。

# 解決策
結論、```web.config```ファイルを使って目的を達成することができました。
本ファイルを利用すれば、Web Appsのディレクトリ単位で柔軟に接続制限を行うことができるため、
今回のようにWebアプリの公開ディレクトリはIP制限をかけるが、
Let's Encryptのチャレンジ通信用のディレクトリへは不特定の送信元からでも接続できるようにする　といったことが可能です。

####※参考記事※
●　[Azure WebサイトでソースIPアドレスによるアクセス許可／拒否（制限）を設定する](https://www.atmarkit.co.jp/ait/articles/1406/20/news089.html)
●　[Azure App ServiceでIP制限](https://qiita.com/mo_22/items/874f8a2f3d126982a812)


# 設定手順
#### 【１】　IPアクセス制限（ホワイトリスト用）のweb.configを作成する
接続を許可するIPアドレスを以下のような構成で記述し、web.configを作成します。

```xml:web.config（Webアプリ公開用）
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <security>
            <ipSecurity allowUnlisted="false" denyAction="NotFound">
                <add allowed="true" ipAddress="x.x.x.x" subnetMask="x.x.x.x" />  <!-- ここに許可するIPを記述-->
                <add allowed="true" ipAddress="x.x.x.x" subnetMask="x.x.x.x" />  <!-- 複数記述することも可能-->
            </ipSecurity>
        </security>
   </system.webServer>
</configuration>
```

上記の```denyAction```にて、許可されていないIPからアクセスされた際の応答ステータスコードを指定することができます。
私の環境では```NotFound```（404）としましたが、他にも以下のように指定できるようです。

|ステータスコード|denyAction属性の設定|
|:--|:--|
|401 Unauthorized |denyAction="Unauthorized"|
|403 Forbidden|denyAction="Forbidden"|
|502 Bad Gatewayなど（応答中止）|denyAction="AbortRequest"|


#### 【２】　Webアプリ公開ディレクトリ（```wwwroot```）に手順１で作成した```web.config```を配置する
Kuduを開き、 **D:\home\site\wwwroot** ディレクトリ内に```web.config```を配置します。


```text:home\site\wwwroot
wwwroot
└── App_Data
└── Views
└── web.config # ←配置
```
この状態にすることで、```wwwroot```およびその配下へのアクセスは指定した送信元IP以外拒否されます。

#### 【３】　手順１のIPアクセス制限を「無効化」させるweb.configを作成する
Let's Encryptのチャレンジ接続をlistenできるよう、手順１で作成した```web.config```を無効化させる用の```web.config```を別途作成します。

```xml:web.config（Let'sEncrypt接続許可用）
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <security>
            <ipSecurity allowUnlisted="true">
       　　　    <clear/>  <!-- 親ディレクトリに設定されているIP制限を無効化させる-->
            </ipSecurity>
        </security>
   </system.webServer>
</configuration>
```

#### 【４】　Let's Encryptのチャレンジ通信が行われるディレクトリを作成する
※１回でも証明書の作成・更新を行っていて、本ディレクトリがすでに作られている場合は読み飛ばして下さい。


Let's Encryptがチャレンジ通信の際に接続を試みるディレクトリを手動で作成します。
Kuduを開き、 **D:\home\site\wwwroot** ディレクトリに移動し、以下2つのディレクトリを新規作成します。

```text:home\site\wwwroot
wwwroot
└── App_Data
└── Views
└── web.config
└── .well-known 　# ←作成
   └── acme-challenge 　# ←作成
```


#### 【５】　手順３で作成したweb.configを手順４のディレクトリ内に配置する
Kuduを開き、 手順３で作成した```web.config```を
手順４で作成した**D:\home\site\wwwroot\\.well-known\acme-challenge** ディレクトリ内に配置します。


```text:home\site\wwwroot
wwwroot
└── App_Data
└── Views
└── web.config
└── .well-known
   └── acme-challenge
      └── .web.config  # ←手順３のweb.configを配置  
```
こうすることで、```.well-known\acme-challenge```へのアクセス時は親ディレクト（wwwroot直下においたweb.config）に設定したIP制限が無効化される状態となるため、送信元IPが特定できないLet's Encryptサーバからの通信を受け付けることが可能です。

※私の環境では無効化用の```web.config```を```acme-challenge```配下に格納しましたが、仕様上、```.well-known```配下に置いてもよいかもしれません。

以上で設定作業は完了です。
#### 【６】　接続確認
今回の要件を満たしているかを以下のパターンで接続を行い確認します。

①Webアプリへのアクセス（接続が許可されたIP）
　結果：　アクセス可能（Web画面が表示される）

②Webアプリへのアクセス（接続が許可されていないIP）
　結果：　アクセス不可（手順１で設定した```denyAction```に応じたエラーコードが返る）

③```http://[url]/.well-known/acme-challenge```へのアクセス（接続が許可されていないIP）
　結果：　アクセス可能

#### 【７】　Let's Encryptの証明書更新
この状態であれば、Azure Web Appsの拡張機能 [Azure Let's encrypt]を利用して証明書の更新ができるはずです。
※更新手順は割愛します。

更新できた場合、Let's Encryptサーバが```.well-Known\acme-challenge```ディレクトリ内にチャレンジ検証用のファイルを作成することが確認できます。

```text:home\site\wwwroot
wwwroot
└── App_Data
└── Views
└── web.config
└── .well-known 　
   └── acme-challenge
      └── web.config
      └── 3bN#87AtTUrQME9F1CCTknf9Oafpm8r43XJGRxdejml  # ←チャレンジ通信時に生成されるファイル
```


以上です。