Azure Storage セキュリティ関連まとめ①

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

#はじめに
Azure Storageのセキュリティ周りについて調べる機会があったので、
まとめとしてのPOST。

#最低限やるべき
Azure Portalから設定できるものは設定する

##HTTPs Only
- PortalのStorageアカウントから**構成**→安全な転送が必須を**有効**にするだけ。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/648a2a6b-816b-6fd4-f134-23bcc35366bf.png)

##サービスの暗号化
- こちらもPortalのStorageアカウントからBLOB SERVICEの**暗号化**メニューのStorageサービスの暗号化を**有効**にする。基本的にStorageアカウント(V2)を作成する時にデフォルトで有効になっているので、特に意識しなくても有効となっているはず。。。
- Portalにも書いてありますが、ここでいうサービスの暗号化というのはAzure側でディスクに書き込む前に暗号化して、メモリに書き出すときに複合化してくれるってオプションですので、Blobに格納されている時は暗号化されているけど、取り出すときに自動で複合化されます。Azureのデータセンターが襲撃されて、Storageのディスクごと盗まれない限りあまり意味はないんじゃないかと思います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/131619/4fed9ff1-0add-1902-27b2-d5c9a836d8d6.png)

#Azure Storage Accountが抱えているセキュリティホール
セキュリティホールというと大袈裟ですが、
Azure StorageはPrimaryとSecondaryの2つのアクセスKeyで運用しています。
うらを返せば、Primary Keyが流出したら、基本的にはBlobに格納している全データにアクセス可能になってしまいます。
公式には、2つのKeyを運用レベルで切り替えて片方をRegenerateしてくださいってことなんだそうです。

#セキュリティ高めたい場合
お客さんによって、コンプライアンスに引っかかるということがあると思います。
何かしらセキュリティ対策をしないといけない。
##クライアント側暗号化
先のサービスの暗号化とはちがい、Blobに格納する前にアプリケーションで暗号化をかけましょうという話。AzureのSDKもそのユースケースに対応したクラスが用意されています。

~~~C#:Encript.cs
using Microsoft.Azure.KeyVault;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;

    class Encript
    {
        static async Task Main(string[] args)
        {
            try
            {
                CloudBlobClient blobClient = BlobHelper.GetBlobClient();
                CloudBlobContainer container = blobClient.GetContainerReference("encript-test");
                await container.CreateIfNotExistsAsync();
                CloudBlockBlob blob = container.GetBlockBlobReference("encript.txt");

                byte[] data = Encoding.UTF8.GetBytes("クライアント側暗号化です！");

                RsaKey key = new RsaKey("SomthigKeyToEncript");
                BlobEncryptionPolicy policy = new BlobEncryptionPolicy(key, null);

                var accessCondition = new AccessCondition();
                BlobRequestOptions uploadOption = new BlobRequestOptions() { EncryptionPolicy = policy };
                var operationContext = new OperationContext();

                // 暗号化してUpload
                using (var stream = new MemoryStream(data))
                {
                    await blob.UploadFromStreamAsync(stream, accessCondition, uploadOption, operationContext);
                }

                // 複合化なしでDownload
                using (MemoryStream outputStream = new MemoryStream())
                {
                    await blob.DownloadToStreamAsync(outputStream);
                    Console.WriteLine(Encoding.UTF8.GetString(outputStream.ToArray()));
                    // ???6??o??$?e?Yl8?q?3a??d?1~?qO?F?μzM???
                }

                // 複合化してDownload
                KeyResolver resolver = new KeyResolver();
                resolver.Add(key); // key="SomthigKeyToEncript"

                BlobEncryptionPolicy downloadPolicy = new BlobEncryptionPolicy(null, resolver);
                BlobRequestOptions downloadOptions = new BlobRequestOptions() { EncryptionPolicy = downloadPolicy };

                using (MemoryStream outputStream = new MemoryStream())
                {
                    await blob.DownloadToStreamAsync(outputStream, accessCondition, downloadOptions, operationContext);

                    Console.WriteLine(Encoding.UTF8.GetString(outputStream.ToArray()));
                    // クライアント側暗号化です！
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("One or more exceptions occurred.");
                Console.WriteLine(ex);
            }
            finally
            {
                Console.WriteLine("Press enter key to exit");
                Console.ReadLine();
            }
        }
    }
~~~
Uploadするときに、`BlobRequestOptions`に`EncryptionPolicy`を追加することによって、RsaKeyで暗号化されます。Downloadするときも同じポリシーを指定します。

地味にC#7.1の新機能[**非同期Main**](http://ufcpp.net/study/csharp/cheatsheet/ap_ver7_1/)を使ってみました。
やはりC#の書き心地はいいです。

###Shared Access Signature（SAS）
もう一つの有効な手段として、SASを指定して、基本的にはSAS経由でアクセスする方法があります。複数のアプリがアクセスKeyを持つのではなく、1か所で管理して、それ以外のアプリは、SASをもらうという設計です。

- データ オブジェクトのセキュリティを、Shared Access Signature と保存されているアクセス ポリシーで確保
- Shared Access Signature (SAS) は、セキュリティ トークンを含む文字列
- アクセス許可やアクセスの日時の範囲などの制約を指定することができる

以下は`SasSampleMain.exe`から`SasSampleWorker.exe`にSasUriだけを渡して、任意のAzureStorageBlobへのアクセスを許可しています。

~~~C#:SasSampleMain
    public class SasSampleMain
    {
        static async Task Main(string[] args)
        {
            CloudBlobContainer container = await BlobHelper.GetBlobContainer("sas-read-container");
            CloudBlockBlob blob = container.GetBlockBlobReference("SasRead.txt");

            // SasRead.txtをアップロード
            await blob.UploadTextAsync("StorageのShaaredAccessSignatureのテスト");

            // 読み取り専用のアクセスポリシーを作成
            SharedAccessBlobPolicy sasConstraints = new SharedAccessBlobPolicy
            {
                SharedAccessExpiryTime = DateTime.UtcNow.AddHours(2),
                Permissions = SharedAccessBlobPermissions.Read,
            };

            // SASトークン取得 
            string sasContainerToken = container.GetSharedAccessSignature(sasConstraints);
            // Uri作成
            string containerSasUri = String.Format("{0}{1}", container.Uri, sasContainerToken);


            CloudBlobContainer downContainer = await BlobHelper.GetBlobContainer("sas-write-container");

            // 書き込み専用のアクセスポリシーを作成
            SharedAccessBlobPolicy sasWriteConstraints = new SharedAccessBlobPolicy
            {
                SharedAccessExpiryTime = DateTime.UtcNow.AddHours(2),
                Permissions = SharedAccessBlobPermissions.Write
            };
            
            // SASトークン取得 
            string sasOutContainerToken = downContainer.GetSharedAccessSignature(sasWriteConstraints);
            // Uri作成
            string outContainerSasUri = String.Format("{0}{1}", downContainer.Uri, sasOutContainerToken);

            // 読取/書込SasUriを別プロセスの引数として実行
            string arg = $"{containerSasUri} SasRead.txt {outContainerSasUri} SasWrite.txt";
            Execute(arg);

        }

        private static string Execute(string args)
        {
            // 実行ファイルパスの取得
            var exePath = typeof(SharedAccessSignature.Worker.SasSampleWorker).Assembly.Location;
            var rootPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);

            var startInfo = new ProcessStartInfo()
            {
                Arguments = args,
                CreateNoWindow = true,
                UseShellExecute = false,
                RedirectStandardInput = true,

                RedirectStandardOutput = true,
                RedirectStandardError = true,
                FileName = exePath
            };

            var output = new StringBuilder();
            var timeout = TimeSpan.FromMinutes(5);

            using (Process proc = Process.Start(startInfo))
            {
                var stdout = new StringBuilder();
                var stderr = new StringBuilder();

                proc.OutputDataReceived += (sender, e) => { if (e.Data != null) { stdout.AppendLine(e.Data); } };
                proc.ErrorDataReceived += (sender, e) => { if (e.Data != null) { stderr.AppendLine(e.Data); } }; 
                proc.BeginOutputReadLine();
                proc.BeginErrorReadLine();

                var isTimedOut = false;

                if (!proc.WaitForExit((int)timeout.TotalMilliseconds))
                {
                    isTimedOut = true;
                    proc.Kill();
                }
                proc.CancelOutputRead();
                proc.CancelErrorRead();

                output.AppendLine(stdout.ToString());
                output.AppendLine(stderr.ToString());
                if (isTimedOut) throw new TimeoutException("timeout.");
            }

            return output.ToString();
        }
    }
~~~

Workerクラスの方では、
`containerSasUri`で`sas-read-container`の`SasRead.txt`を読み取って、
"_Sasで取得したデータ"という文字列を追加して、
`outContainerSasUri`で`sas-write-container`の`SasWrite.txt`にアップロードするシナリオです。

~~~C#:SasSampleWorker
    public class SasSampleWorker
    {
        static async Task Main(string[] args)
        {
            string outputStr = "";
            var setting = new
            {
                ReadContainerSas = args[0],
                ReadBlobName = args[1],
                WriteContainerSas = args[2],
                WriteBlobName = args[3],
            } as dynamic;
            
            using (MemoryStream outputStream = new MemoryStream())
            {
                // 読取SasUriを使用してアクセス
                await new CloudBlobContainer(new Uri(setting.ReadContainerSas))
                    .GetBlockBlobReference(setting.ReadBlobName)
                    .DownloadToStreamAsync(outputStream);

                outputStr = Encoding.UTF8.GetString(outputStream.ToArray());

                Console.WriteLine(outputStr); // StorageのShaaredAccessSignatureのテスト
            }

            byte[] data = Encoding.UTF8.GetBytes(outputStr + "_Sasで取得したデータ");

            
            using (var inputStream = new MemoryStream(data))
            {
                // 書込SasUriを利用してアクセス
                await new CloudBlobContainer(new Uri(setting.WriteContainerSas))
                    .GetBlockBlobReference(setting.WriteBlobName)
                    .UploadFromStreamAsync(inputStream);

                Console.WriteLine(Encoding.UTF8.GetString(inputStream.ToArray()));
            }
        }
    }
~~~

~~~text:SasRead.txt
StorageのShaaredAccessSignatureのテスト
~~~

~~~text:SasWrite.txt
StorageのShaaredAccessSignatureのテスト_Sasで取得したデータ
~~~

#これでも足りない場合
SecOpsの観点からいくと、クライアント暗号化した時も、SASを使用した時も、結局Storageのアクセスキーを運用でローテーションさせていくことになります。
次回は**Azure Key Vault**を使用してアクセスキーの自動ローテーションを試してみたいと思います。