.NET CoreでAWSにおいて機微情報を扱うときに、AWS Secret ManagerやSystem ManagerのParameter Storeが候補に上がります。
ここでは、Secret Managerを使ったASP.NET Coreでの組み込みについて書いておきます。
- 概要
- AWS Secret の選択
- AWS SecretManager の用意
- .NET Core で AWS Secret Manager の呼び出しを行う
- ASP.NET Core や Generic Host で AWS Secret を取り扱う
- 使いやすく修正する
- 留意点
- まとめ
- Ref
概要
コード、並びにappsettings.jsonなどgit commitから機微情報を排除し、CIで差し込むのではなく、実行環境に応じて安全に取得するよう組むことでより安全なアプリケーションからのアクセスを実現できます。
AWS Secretを使うことで、許可された環境からでないと守る必要があるデータへのアクセスができないように制御します。
コードのサンプルは、GitHubにあげておきます。
https://github.com/guitarrapc/dotnet-lab/tree/master/aws/AwsSecretStore
AWS Secret の選択
AWSで機微情報を扱う方法としては、AWS System ManagerのParameter Storeと、AWS Secret Managerの選択があります。
AWS Secret Managerは料金以外の心配が少なく、スケーラブルな環境で一斉にデプロイしても問題が起こりにくいのはメリットです。 一方で、シークレット一件当たりの料金が$0.4/monthであるため、ユーザー一人一人の情報を扱う/毎度一意なテンポラリの情報を扱うといったシークレットの件数の増加で恐ろしいほどコストがかかります。

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でサクッと作ります。
https://gist.github.com/guitarrapc/a607ac59128b2c6ab6cc7d8f57be60da
自動更新や自動Expireも可能ですが、DBの接続先としてはあまりないので今回は静的に組み立てておきます。
これでtestが登録されます。

呼び出し元がSecret Managerにアクセスできるようにする
作ったAWS SecretManagerを呼び出すときの権限を委譲するため、IAM RoleにこのAWS SecretManagerの読み取り権限をつけておきましょう。
ここでは、AWSが提供している"arn:aws:iam::aws:policy/SecretsManagerReadWrite"で代用します。
https://gist.github.com/guitarrapc/add57e243e096d7c58f24d9e05d6e5ac
あとはIAM RoleにつければAWS側の環境は準備完了です。(アプリケーションはSecret ARNを知る必要がありません。)
.NET Core で AWS Secret Manager の呼び出しを行う
AWS SecretManagerをコンフィグの置き場としてみなすため、nugetパッケージで公開されているASSDK.SecretsManagerを用います。 .NET Coreでも同じSDKでokです。
生で使うときの各種言語のコードは、Secret Managerにシークレットを作成したときに出ています。
https://gist.github.com/guitarrapc/aeab71ef418fcd68c6b095c59b77f3d2
適切なIAM Roleがある状態で実行するとSecretManagerに格納した情報がsecretに格納されたことがわかります。
しかし、このコードはIAM Role前提で認証が渡されることを想定されているため、ローカルでプロファイルを使って実行しようとするとうまく動きません。 ローカルでプロファイルを使って動かすようにしてみましょう。
https://gist.github.com/guitarrapc/00cd711892e948c41a52ec32e8f1f7c5
違いは単純です。
実行時に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が書かれています。
https://gist.github.com/guitarrapc/fbd2e506e6c4a0a9b4b8d0c09d3d9cc6
ここにAWS SecretをConfigとして読み込むのですが、自分で書かずともKralizek.Extensions.Configuration.AWSSecretsManagerパッケージがある程度いい感じになっているのでこれを使います。
https://www.nuget.org/packages/Kralizek.Extensions.Configuration.AWSSecretsManager/
これで、ConfigureAppConfigurationを使って次のようにシークレットを呼び出せるようになります。
https://gist.github.com/guitarrapc/0f246b7518ed2ecbd36a948f285e4e0d
もしプロファイルを使いたければ、先程の例のようにProfileをAWSCredentialに使えばいいでしょう。 もちろんその場合は、AWSSDK.SecurityTokenパッケージを追加します。
https://gist.github.com/guitarrapc/f8527f7d09de8a20f53b194aee42be56
既存のIndexページに仮表示しましょう。 新規にIndexViewModelを用意して、既存のViewとなるIndex.cshtml に埋め込み、IndexControllerからIConfiguration経由でSecretManagerから取得したデータをViewModelに埋めます。
https://gist.github.com/guitarrapc/e74126fa96d49825aff28db067766a41
SecretManagerの値はSecretStoreの名前:JSONキーで指定する必要があるので、ControllerでIConfigurationからGetValueするときに注意がいります。
これでデバッグ実行すると、意図したとおりに取得して表示されたことがわかります。

使いやすく修正する
さて、一見良いようですが実際に利用するときにはあまり使い勝手がありません。
- AWS SecretManagerに登録してあるすべての値を読んでしまう
- SecretManagerのキー名をアプリへ伝える必要がある
そこで、コンフィグに指定した特定キーのSecret Storeのみ読み込むことと、Secret Storeの名前をConfigのキーで指定せずに済むように修正をいれます。
appsettings.jsonやappsettings.Development.jsonでシークレット名を指定できるようにマッピングクラスを用意し、これに対応したappsettings.jsonのセクションを作ります。*1
https://gist.github.com/guitarrapc/419790a8a9861535bd78da7470021945
あとは、このフィルタを効かせつつSecretManagerを読み込むようにAddSecretsManagerに軽くラップをかけたAwsSecretsConfigurationBuilderExtensionsを用意します。
https://gist.github.com/guitarrapc/4ab2c3041873f50bbb58eee22cf00ec9
これで、Program.csでは次のようにかけるようになりました。
https://gist.github.com/guitarrapc/550e4bec8b697bf0bdbdc5c8236b9cc5
ローカル開発でProfileを使いたい場合は、次のようにかけます。
https://gist.github.com/guitarrapc/ed0f1ba5d3708787d4f332832e29d8c0
HomeControllerでも、Secret Storeの名前を知ることなく、JSONキーでほしいデータが取れるようになっています。
https://gist.github.com/guitarrapc/ee32a92191648f7ef2682a92ff6acf78
実行してみると思ったとおりのデータが取れました、バッチリですね。

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
他言語
AWSSecretsManagerConfigurationExtensionsのコード例
参考
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なのは処理の中で外に公開する必要がないからです