Azure Functions と KeyVault を連携させてみる

今日は、プロジェクトで関係している、Azure Functions で [KeyVault](https://azure.microsoft.com/en-us/services/key-vault/) を使ってみるということを試してみた。Key Vault は、アプリケーションで使用するキーやシークレットの安全性を守ってくれるサービスだ。HSM というキーやシークレットを守るための専用のハードも使われている。

　今回は、Azure Functions で使われているキーやシークレットの情報を Key Vault に格納して、取得するための方法について書いてみる。


# 1. KeyVault の作成

Key Vault は、Azure CLI, PowerShell 等様々な方法で作成できるが、ここでは簡単に Portal から、作成してしまおう。特に難しいことは何もない。


![KeyVault01.png](https://qiita-image-store.s3.amazonaws.com/0/3470/b4abd04c-faf2-8879-817c-16a34853df76.png)

# 2. KeyVault へのデータのセット

これまた様々な方法が可能ですが、簡単に Portal でいっときましょう。

![KeyVault02.png](https://qiita-image-store.s3.amazonaws.com/0/3470/cd12ce72-6017-112b-d563-49a7c8eed024.png)

ポータルから選びます。今回はキーではなく、シークレットを登録してみましたが、Certificate というファイル形式と、Manual というテキストを入力するのと両方選べます。今回簡単に、`SomeSecret / ushio` みたいなシークレットを登録してみました。

![KeyVault03.png](https://qiita-image-store.s3.amazonaws.com/0/3470/836aadf7-6d4f-926a-e38d-df9c7a148979.png)

# 3. Service Principle (password) で KeyVault に接続する

Azure Functions から Key Vault に接続するためには２つの方法があります。基本的には、Service Principle を作成して、それを、KeyVault に紐づけて、権限を振るという考えです。その Service Principle の作り方で、よく使うパスワードを使う方法、Certificate を使う方法があります。まずは、
簡単な方で、Azure Function (Visual Studio で開発) に対して、Service Principle (password) で接続するための設定とコードを書いてみます。

## 3.1. Service Principle を作成する。

Service Principle は、Active Directory が、あるアプリケーションを、管理し、それに対して様々な権限を設定するための仕組みです。Azure を操作したい場合は、大体これを使います。

最近では、Azure CLI 2.0 を使うと、ワンコマンドで、Service Principle が作れます。[Create an Azure service principal with Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?toc=%2fazure%2fazure-resource-manager%2ftoc.json)ところが、インターネットで見たどの手順もこれを使わず、PowerShell と、Azure でゴリゴリに設定して、作る方法が採用されています。おそらく、CLI でサービスプリンシプルを作って、Azure CLI で、KeyVault に紐づけて権限をつければたぶん一番簡単だと思います。（ただし、Service Principle + certificate の方法は、Azure CLI で出来るかどうかは不明です）いづれにしても、今度 Azure CLI でも試してみたいと思います。

　さて、[Azure Key Vault From Azure Functions](http://www.rahulpnath.com/blog/azure-key-vault-from-azure-functions/)の手順を踏襲してやってみます。大体そのままでいけました。しかし、Azure 上で、Service Principle をゴリゴリに作るのは久々ですが、勉強がてらやっていましょう。

### 3.1.1. 久々に、Azure 旧 Portal から Service Principle を作ってみる。

Service Principle の作りは簡単です。[Create an Azure service principal with Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?toc=%2fazure%2fazure-resource-manager%2ftoc.json)が一番簡単ですが、ポータルで作るとこんな感じ。細かく制御できます。

Azure Active Directory から、アプリケーションを追加します。すると、サインオン URL、アプリケーションID/URL が要求されます。これは、今回のアプリの場合は、ユニークであればいいので、適当に入力してみてください。（おそらく、本当にシングルサインオンする案件とかでは考える必要あり）

![AD01.png](https://qiita-image-store.s3.amazonaws.com/0/3470/5e500af2-4efc-a912-916d-492134779601.png)

アプリケーションの名前も、実際のアプリではあまりつかいませんので、分かりやすい名前を付けてください。

![AD02.png](https://qiita-image-store.s3.amazonaws.com/0/3470/97cb0873-ceef-3146-a4b4-3b72e14c1361.png)

![AD03.png](https://qiita-image-store.s3.amazonaws.com/0/3470/8253e476-1681-12d2-4f99-505c9ef0c1af.png)

すると Azure Function に相当する、アプリケーションが出来ました。ただ、アプリケーションが出来ただけでなんの、関連付けもありませんし、なんのアクセス権限もありません。

![AD04e.png](https://qiita-image-store.s3.amazonaws.com/0/3470/4da68a08-61af-dc6d-39f2-fb219965ac5d.png)

最後の画面で、1. クライアント ID が表示されます。これが、CLIENT_ID と呼ばれる Service Principle の設定に必要となるIDなので保存しておいてください。また、キーの項目は最初何もありませんが、キーをここで生成することで、このサービスプリンシパルがいつから、いつまで有効かを設定できて、キーの値が表示されます。１回しか表示されないので、これも保存しておいてください。

　ちなみに、アプリケーションは、Service Principle によって、Azure などへのアクセスが許可されますが、不正利用とかあると、この「アプリケーション」を削除すれば、そのアプリケーションは、Azure に接続できなくなるので、便利です。

　さて、Application と、その Service Principle は作りました。次は、それをKey Vault と紐づけます。これは元のブログでたぶん間違っていたところですが、下記のコマンドレット(PowerShell) を使います。
ちなみに、この PowerShell に相当することも、Azure CLI で可能っぽいです。[Manage Key Vault using CLI](https://docs.microsoft.com/en-us/azure/key-vault/key-vault-manage-with-cli)

```
Set-AzureRmKeyVaultAccessPolicy -VaultName 'SpikeKey' -ServicePrincipalName '{ここに、CLIENT_IDを}' -PermissionsToSecret all -PermissionsToKey
```
という感じで、KeyVault と Service Principle を紐づけて権限 (Secret と、Key へのアクセスを、all　として与える) 付与しています。

さて、最後は、コードですが、Visual Studio でコードを書いてみました。Azure Functions のタイプは、Web Trigger の C# にしてみました。 ちなみに、Manage Nuget Packages... を選択して、 専用のライブラリを取得しておきます。`Microsoft.Azure.KeyVault`　と  `Microsoft.IndentityModel.Clients.ActiveDirectory`です。あとはコーディングし放題！

```csharp
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Azure.KeyVault;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace KeyVaultIntegration
{
    public static class KeyVaultIntegration
    {

        private const string applicationId = "{CLIENT_IDをここに}";
        private const string applicationSecret = "{取得したキーをここに}";
        [FunctionName("KeyVaultSpike")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
        {
            log.Info("C# HTTP trigger function processed a request.");
            var keyClient = new KeyVaultClient(async (authority, resource, scope) =>
            {
                var adCredential = new ClientCredential(applicationId, applicationSecret);
                var authenticationContext = new AuthenticationContext(authority, null);
                return (await authenticationContext.AcquireTokenAsync(resource, adCredential)).AccessToken;

            });

            var secretIdentifier = "https://spikekey.vault.azure.net/secrets/SomeSecret/";　// KeyVault の Secret Identifier を入れる。
            var secret = await keyClient.GetSecretAsync(secretIdentifier);

            log.Info($"Secret is {secret}");


            return secret == null
                ? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a secret on the query string or in the request body")
                : req.CreateResponse(HttpStatusCode.OK, "Your secret is " + secret.Value);
        }
    }
}
```

ちなみに、Secret Identifier は次のページで取れます。
![KeyVault04.png](https://qiita-image-store.s3.amazonaws.com/0/3470/cffb6f44-abc8-677e-5265-b9f0875a32ae.png)

`https://spikekey.vault.azure.net/secrets/SomeSecret/{英数字の羅列}`なのですが、最後の英数字は、バージョン番号のようで、なくしたら最新を撮ってくるという仕様なので、このように、SomeSecretというシークレット名の後のバージョンは取らないようにしています。

これで、Azure Functions をローカルで起動して、指示されたURLを単純にリクエストすると、ちゃんとシークレットが取得できています。

![AF01.png](https://qiita-image-store.s3.amazonaws.com/0/3470/26c01fd4-01dd-9d38-a1eb-24770d9fe54a.png)

基本的にサービスプリンシパルの、CLIENT_ID と、キー（パスワード)があり、Key Vault と紐づければ、アクセスできるみたいですね。簡単！

# 4. Service Principle (certificate) で KeyVault に接続する

次に、Service Principle を キー（パスワード）ではなくて、Certificate で認証する方法を試してみます。
こっちのほうがセキュリティ的に硬いですが、手順は面倒です。

## 4.1. Winodws SDK のダウンロード

最初に、Private Key, Certificate, pfx ファイルを作る必要があります。Verisign とかにお金を払って試すのもいいですが、最初は、オレオレ証明書でやってみましょう。オレオレ証明書は [WIndows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk) をダウンロードすれば、 makecert/pvk2pfx というキー関連のツールが取得できます。しかし、なんということか、pvk2pfx の方はどこを探してもありませんでした。あまりよくありませんが、[このサイト](http://www.pconlife.com/viewfileinfo/pvk2pfx-exe/)から落としました。自己責任で。ただ、昔を思い出すと、たしか、[PowerShell だけで、自己証明書を作る手順](https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-cluster-creation-via-arm)があったと思います。だから、たぶんこれらのコマンドなしでも大丈夫だと思います。今回はこのコマンドを使って作る手順を。

なんと、これらのコマンドは、コマンドプロンプトでしか動きません。

## 4.2. Private Key と、 Certificate を作成する

下記のコマンドで、Private Key と、Certificate を作成しています。`mykey.pvk`が、Private Key で、 `ADTestVaultApplication.cer` がセルフサインのCertificate (証明書) です。オレオレ証明書ですね。パスワードを聞かれますので、入力しましょう。

```
"C:\Program Files (x86)\Windows Kits\10\bin\10.0.15063.0\x64\makecert.exe"  -sv mykey.pvk -n "cn=AD Test Vault Application" ADTestVaultApplication.cer -b 01/01/2014 -e 01/01/2019 -r
Succeeded
```

そして、これらを、pfx ファイルにまとめます。

```
C:\Users\tsushi\Downloads\pvk2pfx.exe  -pvk mykey.pvk -spc ADTestVaultApplication.cer -pfx ADTestVaultApplication.pfx -po test
```

private key と、certificate から、pfx ファイル（バイナリで、Private Key, Certificate が格納されている形式) に変換して、パスワード `test` をつけています。

## 4.3. pfx ファイルの Azure Functions への登録

次に、Azure Functions に、作った pfx ファイルを登録してみましょう。

Azure Functions のこの画面から

![AF02.png](https://qiita-image-store.s3.amazonaws.com/0/3470/6e4ece2f-bb0a-ae6b-8022-afc334851419.png)

先ほど作った、ADTestValueApplication.pfx を登録してみましょう。パスワードは、先ほど `-po` オプションで渡した`test`です。
![AF04.png](https://qiita-image-store.s3.amazonaws.com/0/3470/7c7ab8ca-d4fb-4505-7fc9-cafc95023b78.png)

## 4.3. Service Principle の作成


先ほど作成した、Certificate を元に、Service Principle を作ります。
```
$certificateFilePath = "C:\Users\tsushi\Codes\certs\ADTestVaultApplication.cer" // ここ
$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$certificate.Import($certificateFilePath)
$rawCertificateData = $certificate.GetRawCertData()
$credential = [System.Convert]::ToBase64String($rawCertificateData)
$startDate= [System.DateTime]::Now
$endDate = $startDate.AddYears(1)
$adApplication = New-AzureRmADApplication -DisplayName "CertAdApplication" -HomePage  "http://www.kvtest.co.jp" -IdentifierUris "http://www.kvtest.co.jp" -CertValue $credential  -StartDate $startDate -EndDate $endDate // ここ

$servicePrincipal = New-AzureRmADServicePrincipal -ApplicationId $adApplication.ApplicationId

Set-AzureRmKeyVaultAccessPolicy -VaultName 'SpikeKey' -ServicePrincipalName $servicePrincipal.ServicePrincipalNames[0] -PermissionsToSecrets all -PermissionsToKeys all
```

「ここ」と書いている２行が変更可能性のある場所です。具体的にはたいしたことをやっていなくて、Certificate を読み込んで、New-AzureRmADApplication というコマンドレットで、Application　を作って、New-AzureADServicePrincipal というコマンドレットで、Service Principle を作っています。先ほどとの違いは、Azure Portal でこの部分を実行したので、CLIENT_ID や、キーは、画面から取得しました。

今回は上記のコマンドを実行後、次のように取得できます。

CLIENT_ID
```
$adApplication.ApplicationId
```
キーは、今回はCertificate を使うので取得不要です。

## 4.4. WEBSITE_LOAD_CERTIFICATES 環境変数の設定

簡単にアップロードできました。次に Application Settingsに移ります。ここで、Certificate のThumbnail を取得します。Thumbprint は親指の指紋という意味です。実体は、Hashのようで、これにより、Certficate を一位に特定できます。

先ほどのコマンドを実行したのち、次のコマンドを実行すれば取得できます。

```
$certificate.Thumbprint
```

![Af05.png](https://qiita-image-store.s3.amazonaws.com/0/3470/906b1715-cf94-00a8-2d13-f0ff132df28d.png)

## 4.5. Azure Functions を Portal で作ってみる。

さて、この構成だと、クライアントで 実行するのは難しそうなので、Portal で実行を試してみました。Nuget で取得するライブラリは、`Microsoft.Azure.KeyVault`と`Microsoft.IndentityModel.Clients.ActiveDirectory`ですが、Portal から実行する場合は、`project.json` に書いておきます。

![AF06.png](https://qiita-image-store.s3.amazonaws.com/0/3470/efd65ef0-1fbb-6d4b-0108-cc6324277aa4.png)

そして、いよいよコードです。Certificate 周りのコードは面倒になりましたが、本質的には、KeyVault クライアントを取得して、実行しているだけです。

```csharp
using System.Net;
using Microsoft.Azure.KeyVault;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

private const string applicationId = "{ここに、CLIENT_ID}";

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{


string certificateThumbprint = System.Environment.GetEnvironmentVariable("WEBSITE_LOAD_CERTIFICATES");
    log.Info("C# HTTP trigger function processed a request.");
    var keyClient = new KeyVaultClient(async (authority, resource, scope)=> {
        var authenticationContext = new AuthenticationContext(authority, null);
        X509Certificate2 certificate;
        X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        try {
            store.Open(OpenFlags.ReadOnly);
            X509Certificate2Collection certificateCollection = store.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbprint, false);
            if (certificateCollection == null || certificateCollection.Count ==0)
            {
                throw new Exception("Certificate not installed in the store");
            }

            certificate = certificateCollection[0];
        }
        finally
        {
            store.Close();
        }
        var clientAssertionCertificate = new ClientAssertionCertificate(applicationId, certificate);
        var result = await authenticationContext.AcquireTokenAsync(resource, clientAssertionCertificate);
        return result.AccessToken;
    });

    var secretIdentifier = "https://spikekey.vault.azure.net/secrets/SomeSecret/";
            var secret = await keyClient.GetSecretAsync(secretIdentifier);

            log.Info($"Secret is {secret}");

    return secret == null
        ? req.CreateResponse(HttpStatusCode.BadRequest, "Please store a secret")
        : req.CreateResponse(HttpStatusCode.OK, "Secret is " + secret);
}
```
テスト実行するとうまく動いています。

![AF07.png](https://qiita-image-store.s3.amazonaws.com/0/3470/64383f63-87dc-df1b-f7ab-a1981d991989.png)

お疲れさまでした。明日はこれをテストしやすいようにして実装して終了です。