tech.guitarrapc.cóm

Technical updates

AWS Secret Manager を使ってASP.NET Core のシークレット情報を扱う

.NET Core で AWS において機微情報を扱うときに、AWS Secret Manager や System Manager の Parameter Store が候補に上がります。

ここでは、Secret Manager を使った ASP.NET Core での組み込みについて書いておきます。

目次

TL;DR

コード、並びにappsettings.json などgit commit から機微情報を排除し、CIで差し込むのではなく、実行環境に応じて安全に取得するように組むことでより安全なアプリケーションからのアクセスを実現できます。 AWS Secretを使うことで、許可された環境からでないと守る必要があるデータへのアクセスができないように制御します。

コードのサンプルは、GitHub にあげておきます。

github.com

AWS Secret の選択

AWS で機微情報を扱う方法としては、AWS System Manager の Parameter Store と、AWS Secret Manager の選択があります。

AWS Secret Managerは 料金以外の心配が少なく、スケーラブルな環境で一斉にデプロイしても問題が起こりにくいメリットがあります。 一方で、シークレット一件当たりの料金が$0.4/monthであるため、ユーザー一人一人の情報を扱う/毎度一意なテンポラリの情報を扱うといったシークレットの件数の増加で恐ろしいほどコストがかかります。

AWS Secret はシークレットの件数が増えると料金が跳ね上がる

aws.amazon.com

Secret Manager ではアプリごとに単一になる情報を取り扱うほうがコスト的にはいいでしょう。

アプリケーションの設定などはコストが抑えられて使いやすい

System Manager の Parameter Store は、同時アクセスの規定が明確でなく Rate Limit に到達する可能性はありますが、無料でできるためアクセス頻度の少ないパラメーターを保持するのにはとても優秀です。

今回は、アプリケーションの起動時に1回読み込まれるRDSの接続情報をAWS Secret Manager に保持させて読み込んでみましょう。

AWS SecretManager の用意

AWS Secret Manager を使うため、Secret Manager とそこにいれるデータを用意しておきます。 SecretManager の名前を test として、JSON で取得することを想定します。

{
  "ConnectionStrings": {
    "DATABASE": "YOUR_AWESOME_CONNECTION_STRINGS"
  }
}

AWS Secret Manager にデータをいれる

AWS Secret Manager とデータをterraform やAWS Console でも aws cli でサクッと作ります。

gist.github.com

自動更新や自動Expire も可能ですが、DBの接続先としてはあまりないので今回は静的に組み立てておきます。

これでtest が登録されます。

Secret Store での登録例

呼び出し元がSecret Managerにアクセスできるようにする

作ったAWS SecretManager を呼び出すときの権限を委譲するため、IAM RoleにこのAWS SecretManager の 読み取り権限をつけておきましょう。

ここでは、AWS が提供している "arn:aws:iam::aws:policy/SecretsManagerReadWrite" で代用します。

gist.github.com

あとはIAM Role につければAWS 側の環境は準備は完了です。(アプリケーションはSecret ARN を知る必要がありません。)

.NET Core で AWS Secret Manager の呼び出しを行う

AWS SecretManager をコンフィグの置き場としてみなすため、nuget パッケージで公開されているASSDK.SecretsManager を用います。 .NET Coreでも同じSDKでokです。

NuGet Gallery | AWSSDK.SecretsManager 3.3.100.13

生で使うときの各種言語のコードは、Secret Manager にシークレットを作成したときに下に出ています。

gist.github.com

適切なIAM Roleがある状態で実行するとSecretManager に格納した情報が secret に格納されたことがわかります。

しかし、このコードはIAM Role前提で認証が渡されることを想定されているため、ローカルでプロファイルを使って実行しようとするとうまく動きません。 ローカルでプロファイルを使って動かすようにしてみましょう。

gist.github.com

違いは単純です。 実行時に Profile から認証を取得するように CredentialProfileStoreChain を使って、AmazonSecretsManagerClient にこの認証を渡しているだけです。 なお、Profile を使って認証する場合は AWSSDK.SecurityToken nuget パッケージを追加してください、このパッケージがないと認証トークンのハンドルができません。

これでAWS Secret Manager を .NET Core から取得する方法は把握できました。

ASP.NET Core や Generic Host で AWS Secret を取り扱う

動かすだけなら動きましたが、このままのコードでは生すぎて使いにくさがあります。 実際にアプリに組み込む場合は、ASP.NET Core や Generic Host へ追加することになるので、HostBuilder からのチェーンでIConfiguration に突っ込みたいところです。

ASP.NET Core なら WebHostBuilder からのチェーンだとうれしいです。

WebHost.CreateDefaultBuilder(args)
    .AddAwsSecrets()
    .UseStartup<Startup>();

Generic Host なら HostBuilder からのチェーンでしょう。 わかりやすい例としてのコード例ならMicroBatchFramework で次のように.AddAwsSecrets() が出来れば嬉しいと感じます。

BatchHost.CreateDefaultBuilder()
    .AddAwsSecrets()
    .RunBatchEngineAsync<CredentialSample>(args);

ではこのようなコードを書けるように組んでみましょう。

ASP.NET Core で Secret Store から値を取得する

ASP.NET Core でサンプルプロジェクトを開始します。 わかりやすいようにView に取得結果を表示するので、MVCで行きましょう。

初期状態は次のように Program.cs が書かれています。

gist.github.com

ここに AWS Secret をConfigとして読み込むのですが、自分で書かずともKralizek.Extensions.Configuration.AWSSecretsManager パッケージがある程度いい感じになっているのでこれを使います。

www.nuget.org

これで、ConfigureAppConfigurationを使って次のようにシークレットを呼び出せるようになります。

gist.github.com

もしプロファイルを使いたければ、先程の例のように Profile を AWSCredential に使えばいいでしょう。 もちろんその場合は、AWSSDK.SecurityToken パッケージを追加します。

gist.github.com

既存のIndex ページに仮表示しましょう。 新規にIndexViewModel を用意して、既存のView となるIndex.cshtml に埋め込み、IndexController から IConfiguration経由でSecretManager から取得したデータをViewModel に埋めます。

gist.github.com

SecretManager の値はSecretStoreの名前:JSONキー で指定する必要があるので、Controller でIConfigurationから GetValue するときに注意がいります。

これでデバッグ実行すると、意図したとおりに取得して表示されたことがわかります。

SecretManager からデータを取得した結果

使いやすく修正する

さて、一見良いようですが実際に利用するときにはあまり使い勝手がありません。

  • このままだとAWS SecretManager に登録してあるすべての値を読んでしまいます
  • SecretManager のキー名をアプリが知る必要がある

そこで、コンフィグに指定した特定キーのSecret Storeのみ読み込むことと、Secret Storeの名前をConfigのキーで指定せずに済むように修正をいれます。

appsettings.json や appsettings.Development.json でシークレット名を指定できるようにマッピングクラスを用意し、これに対応したappsettings.json のセクションを作ります。*1

gist.github.com

あとは、このフィルタを効かせつつSecretManagerを読み込むようにAddSecretsManagerに軽くラップをかけた AwsSecretsConfigurationBuilderExtensions を用意します。

gist.github.com

これで、Program.cs では次のようにかけるようになりました。

gist.github.com

ローカル開発でProfileを使いたい場合は、次のようにかけます。

gist.github.com

HomeController でも、Secret Store の名前を知ることなく、JSONキーでほしいデータが取れるようになっています。

gist.github.com

実行してみると思ったとおりのデータが取れました、バッチリですね。

修正後、指定したキーのSecretStoreが取得できている

Secret Store から必要なシークレットのみ取得するフィルタ

実装を見てみましょう。

public static IWebHostBuilder AddAwsSecrets(this IWebHostBuilder hostBuilder, string prefix, string region, string profile) のシグネチャはプロファイル経由での読み込みようなので無視してok です。

実際にアプリから利用するのは、public static IWebHostBuilder AddAwsSecrets(this IWebHostBuilder hostBuilder, string region) シグネチャです。 AWS Secret Manager はregion 依存なので、適当にリージョンを合わせてください。 引数やAWS_REGION などの環境変数から取得するようにするのもいいでしょう。

今回、appsettings.json で必要なキーを指定しているので、Secret Manager に問い合わせる前にフィルタしている方をマッピングしています。

            // build partially
            var partialConfig = configurationBuilder.Build();
            var settings = new AwsSecretsManagerSettings();
            partialConfig.GetSection(nameof(AwsSecretsManagerSettings)).Bind(settings);

            // Filter which secret to load
            var allowedPrefixes = settings.SecretGroups
                .Select(x => $"{prefix}{x}")
                .ToArray();

あとは、Func である SecretFilter で対象のSecret Store があるか検査して読み込むだけです。

            configurationBuilder.AddSecretsManager(region: endpoint, credentials: credentials, configurator: opts =>
            {
                opts.SecretFilter = entry => HasPrefix(allowedPrefixes, entry);
                opts.KeyGenerator = (entry, key) => GenerateKey(allowedPrefixes, key);
            });

// 省略

        private static bool HasPrefix(IEnumerable<string> allowedPrefixes, SecretListEntry entry)
            => allowedPrefixes.Any(prefix => entry.Name.StartsWith(prefix));

SecretStore名を除く

これは単純ですね。

            configurationBuilder.AddSecretsManager(region: endpoint, credentials: credentials, configurator: opts =>
            {
                opts.SecretFilter = entry => HasPrefix(allowedPrefixes, entry);
                opts.KeyGenerator = (entry, key) => GenerateKey(allowedPrefixes, key);
            });

// 省略

        private static string GenerateKey(IEnumerable<string> prefixes, string secretValue)
        {
            // don't use '/' in your environment or secretgroup name.
            var prefix = prefixes.First(secretValue.StartsWith);

            // Strip the prefix
            var s = secretValue.Substring(prefix.Length + 1);
            return s;
        }

留意点

AWS Secret Store は、JSON や Environment Variables、引数のIConfiguration 処理後に読んでいるため、もし同じキーのコンフィグをAWS Secret Store から読んだ場合上書きされます。

まとめ

あくまで薄いラッパーなのでご自身の使いやすいように調整できるはずです。

例えばローカル開発向けに、「既存のConnectionStrings がもし定義されていたらSecret Store はみない」、とかも簡単ですね。

コードからシークレットを抜く、かと言って環境変数にいれるのではなく いわゆるSecret Store / KeyValt から取得するのは、やっておいて損はないのでさくっとどうぞ。

Generic Host も IWebHostBuilder が IHostBuilder になるだけでほぼ一緒です。

Ref

docs.aws.amazon.com

他言語

www.sambaiz.net

AWSSecretsManagerConfigurationExtensions のコード例

github.com

参考

Secure secrets storage for ASP.NET Core with AWS Secrets Manager (Part 1)

Secure secrets storage for ASP.NET Core with AWS Secrets Manager (Part 2)

*1:private class なのは処理の中で外に公開する必要がないからです