Azure Storage セキュリティ関連まとめ②　～Key Vault～

[SC（非公式）Advent Calendar 2017](https://qiita.com/advent-calendar/2017/syscon) の21日目です。

前回の[Azure Storage セキュリティ関連まとめ①](https://qiita.com/gunji10/items/4c76c57732bc72a8d134)の続きになります。

#前回から
Storageのクライアント側暗号化を実践しても、暗号化キーをどこに保存するの？
ということでAzure Key Vaultを使ってみましょうという試み。
①暗号化キー相当の何かしらの文字列をKey Vaultに格納して取り出す。
②Key Vaultで管理されたStorage Accountを作成して、アクセスキーの管理(キーのローテーションとか)から解放されたい。
以上の2つを試していきます。

#Azure Key Vault
[AzureKeyVaultとは](https://docs.microsoft.com/ja-jp/azure/key-vault/key-vault-whatis)

- Azureの提供しているServiceで使用するKeyやSecretの情報を暗号化して、HSM(Hardware Security Module)で保護・保存してくれるサービス
- Key Vaultで管理しているキーやシークレットを利用することで、StorageAccountにアクセスキー無しでアクセスすることができる。
- AzureADが必要。

#①暗号化キーの保護
暗号化キーをKey Vaultのシークレットに登録して、C#のプログラムから呼び出します。
その際、KeyVaultにアクセスするために、AzureADに登録したアプリケーションのプリンシパルを使用します。

##アプリケーション登録
Azure Active Directory --> アプリの登録 --> ＋新しいアプリケーション
storage-securityというアプリを登録します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/9a7ef54f-192d-60a2-7db7-ca4d3a1a42e2.png)

注意:warning:
アプリケーションの登録は、ディレクトリに対する**全体管理者**の権限がないとできません。ポータルにログインしているユーザーが全体管理者のロールを持っていない場合は、管理者ロールのアカウントから、権限をもらってください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/2e1d920c-471d-0c0d-9f38-aaaa184358c8.png)

登録したアプリの**アプリケーションID**と**アプリケーションキー**をメモします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/4eb8e472-9783-8609-d4c6-5c854d18e692.png)

キーを登録します。保存ボタンを押したときに表示される値をメモしておきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/240fc6c1-6ef4-95ef-bec0-45dea5d8b944.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/b52335b0-a2e8-142b-c0d3-b3855b13b50e.png)


##Key Vault作成
ほかのAzureのサービス同様、リージョンとかリソースグループとかを選びます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/852540c5-6721-b25f-6927-09d595735265.png)
先ほど追加したアプリケーションのプリンシパルをアクセスポリシーを追加します。
シークレットアクセス許可の「取得・一覧・セット」にチェックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/8b170c67-e24d-f026-5936-56d7d94f26a4.png)

##Key Vaultのシークレットの登録
任意の文字列を追加します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/7e22010b-5573-c6c0-b66e-93544b68ea50.png)
名前：`StorageSecret`
値　：`SomthigKeyToEncript`
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/62920095-5499-ea23-3c01-30290abb92e9.png)

##Secretの取得
今しがた登録したシークレットを取得します。

~~~C#:EncriptKV.cs

using AzureStorageSecurityLabo.Common;
using Microsoft.Azure.KeyVault;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Threading.Tasks;

public class EncriptKV
{
    static async Task Main(string[] args)
    {
        StorageSettings settings = StorageSettings.Default;

        var cliantId = settings.AppId;
        var secret = settings.AppSecret;
        var secretIdentifier = settings.SecretIdentifier;

        try
        {
            var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(
                    (authority, resource, scope) =>
                        GetAccessToken(authority, resource, scope, cliantId, secret)));

            var sec = await kv.GetSecretAsync(secretIdentifier);

            Console.WriteLine(sec.Value); // SomthigKeyToEncript
        }
        catch (Exception ex)
        {
            Console.WriteLine("One or more exceptions occurred.");
            Console.WriteLine(ex);
        }
        finally
        {
            Console.WriteLine("Press enter key to exit");
            Console.ReadLine();
        }
    }

    private static async Task<string> GetAccessToken(
        string authority, string resource, string scope, string cliantId, string secret)
    {
        var authContext = new AuthenticationContext(authority);

        ClientCredential clientCred = new ClientCredential(cliantId, secret);

        AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);

        if (result == null)
            throw new InvalidOperationException("Failed to obtain the JWT token");

        return result.AccessToken;
    }
}
~~~

`var cliantId = settings.AppId;`
AzureADに登録したアプリのアプリケーションID
`var secret = settings.AppSecret;`
AzureADに登録したアプリの設定したキー
`var secretIdentifier = settings.SecretIdentifier;`
Key Vaultで設定したシークレットのIdentifier
※KeyVaultのブレードから閲覧可能

![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/a56a3c6b-8d3e-0b75-61fa-90960fb714f3.png)

Key Vaultに格納したSecret(今回は暗号化キー)を取得することができました。
暗号化キーでもいいし、DBの接続文字列でも、SotrageAccountのアクセスキーでもいいと思いますが、
Azure Key Vaultは、Key Vaultで管理されたStorage Serviceを作成することができます。

#②Key Vault で管理されたStorageの利用

##Azure Key Vault ストレージアカウントキー
- Azure Key Vaultで管理されたAzure Storage アカウントを使用してキーの一覧表示ができる。
- Azure Key Vault は定期的にキーを再生成することができる。
- キーの値は、呼び出し元に応答で返されることはない。
- 2017/12/21時点プレビュー

つまり、Storageアクセスキーの管理をAzure Key Vaultが勝手にやってくれて、
アプリケーションプリンシパル経由でKey Vaultにアクセスして、
Key Vault管理下にあるStorageへのアクセス権限を上手いことやってくれるやつです。

##Azure Storage の作成
まずは普通にストレージアカウントを作成します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/713c176e-d6e2-2fa9-4793-4e261e877b92.png)

注意:warning:
Key Vaultとの紐づけを行うため、対象のストレージはKey Vaultと同じリージョンにある必要があります。

##Key VaultとStorage Accountの紐づけ
ここからはPowerShellの出番です。
任意のユーザーでAzureRmにログインします。

###ロール ベースのアクセス制御 (RBAC) の権限設定
~~~PowerShell:PowerShell
# Azure Key Vaultのプリンシパルを取得
$yourKeyVaultServicePrincipalId = (Get-AzureRmADServicePrincipal -ServicePrincipalName cfa8b339-82a2-471a-a3c9-0fc0be7a4093).Id
# Storage AccountのリソースID
$storageResourceId = '/subscriptions/{SubscriptionId}/resourceGroups/{ResousrceGroupName}/providers/Microsoft.Storage/storageAccounts/managedvault'
# Key Vaultにストレージアカウントのキーオペレーターロールを追加
New-AzureRmRoleAssignment -ObjectId $yourKeyVaultServicePrincipalId -RoleDefinitionName 'Storage Account Key Operator Service Role' -Scope $storageResourceId
~~~
ロール ベースのアクセス制御 (RBAC) の権限設定をします。
Azure Key Vaultにストレージアカウントのキーを操作する権限を追加します。
`cfa8b339-82a2-471a-a3c9-0fc0be7a4093`はAzure Key VaultのアプリケーションIDです。
Azure AD-->エンタープライズアプリケーション-->すべてのアプリケーション
-->Azure Key Vault-->プロパティからアプリケーションIDがわかります。 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/c7ae82fd-1a3b-6afc-2b13-88f93f5852dd.png)

###Key Vaultのアクセスポリシー追加
~~~PowerShell:PowerShell
# 操作するユーザーのプリンシパルを取得
$youruserPrincipalId = (Get-AzureRmADUser -SearchString "your user principal name").Id
# Key Vaultのアクセスポリシーを追加
Set-AzureRmKeyVaultAccessPolicy -VaultName managedstorage -ObjectId $youruserPrincipalId -PermissionsToStorage all -PermissionsToSecrets all
~~~
管理されたストレージアカウントと SAS定義を作成する前に、
ストレージ アカウントに、Key Vaultサービスへのアクセスを追加します。
`-PermissionsToStorage`はPowerShellやAzureCLIからじゃないと登録できないみたいです。プレビューだから？？？

###Key Vaultで管理されたStorageアカウント作成
~~~PowerShell:PowerShell
# Key Vaultで管理されたStorageアカウントを作成します
Add-AzureKeyVaultManagedStorageAccount -VaultName managedstorage -Name managedvault -AccountResourceId $storageResourceId -ActiveKeyName key1 -RegenerationPeriod ([System.Timespan]::FromDays(3))
~~~
`-RegenerationPeriod`はKeyの再生成期間。
プレビュー期間は無料。
一般提供価格 - 更新あたり ¥102

##Sas定義の設定・利用
ここまでで、`Key Vault`、`Storage Account`、`Key Vaultで管理されたStorage Account`が作成されているはずです。
`Key Vaultで管理されたStorage Account`が内部的にどういう仕組みになっているかわかりませんが、既存のStorage Accountをラップする形でインターフェースを提供していると予想してます。
その証拠に、Storage Exploreなどからは、今まで通り`Storage Account`にアクセスできます。
では、`Key Vaultで管理されたStorage Account`にSasの定義を追加します。
ここからはC#です。

~~~C#:StorageAccessKV.cs
class StorageAccessKV
{
    static async Task Main(string[] args)
    {
        StorageSettings settings = StorageSettings.Default;
        var cliantId = settings.AppId;
        var clientSecret = settings.AppSecret;
        string vaultAddress = "https://managedstorage.vault.azure.net/";
        string storageName = "managedvault";

        try
        {
            var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(
                    (authority, resource, scope) =>
                            GetAccessToken(authority, resource, scope, cliantId, clientSecret)));

            var sas = "sas1";
            var fields = new Dictionary<string, string>() {
                {"sasType", "account"},
                {"signedProtocols", "https"},
                {"signedServices", "b"},
                {"signedResourceTypes", "sco"},
                {"signedPermissions", "rwl"},
                {"signedVersion", "2016-05-31"},
                {"validityPeriod", "P1D"}
            };
            var attr = new SasDefinitionAttributes { Enabled = true };

            var sasDefinition =
                    await kv.SetSasDefinitionAsync(vaultAddress, storageName, sas, fields, attr);

            var secret = await kv.GetSecretAsync(sasDefinition.SecretId);

            var credential = new StorageCredentials(secret.Value);
            var blobUri = new Uri("https://managedvault.blob.core.windows.net/");
            var accountWithSas = new CloudStorageAccount(credential, blobUri, null, null, null);

            var blobClientWithSas = accountWithSas.CreateCloudBlobClient();
            CloudBlobContainer container = blobClientWithSas.GetContainerReference(settings.ContainerName);
            await container.CreateIfNotExistsAsync();
            CloudBlockBlob blob = container.GetBlockBlobReference("keyvault.txt");

            await blob.UploadTextAsync("Hello Key Vault!");

        }
        catch (Exception ex)
        {
            Console.WriteLine("One or more exceptions occurred.");
            Console.WriteLine(ex);
        }
        finally
        {
            Console.WriteLine("Press enter key to exit");
            Console.ReadLine();
        }
    }
    private static async Task<string> GetAccessToken(
        string authority, string resource, string scope, string cliantId, string secret)
    {
        var authContext = new AuthenticationContext(authority);

        ClientCredential clientCred = new ClientCredential(cliantId, secret);

        AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);

        if (result == null)
            throw new InvalidOperationException("Failed to obtain the JWT token");

        return result.AccessToken;
    }
}
~~~

`SasDefinitionAttributes.Enabled`はデフォルト`false`ですので、
明示的にtrueを指定しています。
ややこしそうなSasのパラメータの設定は以下です。

~~~C#
var fields = new Dictionary<string, string>() {
    {"sasType", "account"},
    {"signedProtocols", "https"},
    {"signedServices", "b"},
    {"signedResourceTypes", "sco"},
    {"signedPermissions", "rwl"},
    {"signedVersion", "2016-05-31"},
    {"validityPeriod", "P1D"}
};
~~~
`{"signedServices", "b"}`はBlob、
`{"signedResourceTypes", "sco"}`はService、Container、Object、
`{"signedPermissions", "rwl"}`はRead、Write、List
です。
Storageアカウントのアクセスキーは使用せず、
Sas定義経由でStorageにアクセスしているのがわかると思います。
実際運用するってなった場合は、
1か月に1回キーをローテーションするように設定したストレージアカウントに対して、
何日単位でSasを定義してアプリケーションにキャッシュ。
キャッシュの期限切れ-->Sasの期限切れ-->Sas定義再作成
といった感じになるのかな。(適当)

#おわりに
今回はStorageサービスにスポットを当てましたが、
DBの接続文字列や、他システムのURIなど、用途は多そうです。
Functionsや、Mobile Appsを利用してよりライトにアプリケーションを作成することが可能になっていますが、機微な情報やセキュリティを担保しないといけないデータに関してはおろそかになりがちです。
これを機に重要な設定値をconfigに書くことは卒業しましょう！
（Azure ADアレルギー発症しました・・・:scream:）