.NET Core で AWS において機微情報を扱うときに、AWS Secret Manager や System Manager の Parameter Store が候補に上がります。
ここでは、Secret Manager を使った ASP.NET Core での組み込みについて書いておきます。
目次
- 目次
- TL;DR
- AWS Secret の選択
- AWS SecretManager の用意
- .NET Core で AWS Secret Manager の呼び出しを行う
- ASP.NET Core や Generic Host で AWS Secret を取り扱う
- 使いやすく修正する
- 留意点
- まとめ
- Ref
TL;DR
コード、並びにappsettings.json などgit commit から機微情報を排除し、CIで差し込むのではなく、実行環境に応じて安全に取得するように組むことでより安全なアプリケーションからのアクセスを実現できます。 AWS Secretを使うことで、許可された環境からでないと守る必要があるデータへのアクセスができないように制御します。
コードのサンプルは、GitHub にあげておきます。
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 でサクッと作ります。
自動更新や自動Expire も可能ですが、DBの接続先としてはあまりないので今回は静的に組み立てておきます。
これでtest が登録されます。
呼び出し元がSecret Managerにアクセスできるようにする
作ったAWS SecretManager を呼び出すときの権限を委譲するため、IAM RoleにこのAWS SecretManager の 読み取り権限をつけておきましょう。
ここでは、AWS が提供している "arn:aws:iam::aws:policy/SecretsManagerReadWrite"
で代用します。
あとはIAM Role につければAWS 側の環境は準備は完了です。(アプリケーションはSecret ARN を知る必要がありません。)
.NET Core で AWS Secret Manager の呼び出しを行う
AWS SecretManager をコンフィグの置き場としてみなすため、nuget パッケージで公開されているASSDK.SecretsManager を用います。 .NET Coreでも同じSDKでokです。
生で使うときの各種言語のコードは、Secret Manager にシークレットを作成したときに下に出ています。
適切なIAM Roleがある状態で実行するとSecretManager に格納した情報が secret
に格納されたことがわかります。
しかし、このコードはIAM Role前提で認証が渡されることを想定されているため、ローカルでプロファイルを使って実行しようとするとうまく動きません。 ローカルでプロファイルを使って動かすようにしてみましょう。
違いは単純です。
実行時に 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 が書かれています。
ここに AWS Secret をConfigとして読み込むのですが、自分で書かずともKralizek.Extensions.Configuration.AWSSecretsManager
パッケージがある程度いい感じになっているのでこれを使います。
これで、ConfigureAppConfigurationを使って次のようにシークレットを呼び出せるようになります。
もしプロファイルを使いたければ、先程の例のように Profile を AWSCredential に使えばいいでしょう。 もちろんその場合は、AWSSDK.SecurityToken パッケージを追加します。
既存のIndex ページに仮表示しましょう。 新規にIndexViewModel を用意して、既存のView となるIndex.cshtml に埋め込み、IndexController から IConfiguration経由でSecretManager から取得したデータをViewModel に埋めます。
SecretManager の値はSecretStoreの名前:JSONキー
で指定する必要があるので、Controller でIConfigurationから GetValue
するときに注意がいります。
これでデバッグ実行すると、意図したとおりに取得して表示されたことがわかります。
使いやすく修正する
さて、一見良いようですが実際に利用するときにはあまり使い勝手がありません。
- このままだとAWS SecretManager に登録してあるすべての値を読んでしまいます
- SecretManager のキー名をアプリが知る必要がある
そこで、コンフィグに指定した特定キーのSecret Storeのみ読み込むことと、Secret Storeの名前をConfigのキーで指定せずに済むように修正をいれます。
appsettings.json や appsettings.Development.json でシークレット名を指定できるようにマッピングクラスを用意し、これに対応したappsettings.json のセクションを作ります。*1
あとは、このフィルタを効かせつつSecretManagerを読み込むようにAddSecretsManager
に軽くラップをかけた AwsSecretsConfigurationBuilderExtensions
を用意します。
これで、Program.cs では次のようにかけるようになりました。
ローカル開発でProfileを使いたい場合は、次のようにかけます。
HomeController でも、Secret Store の名前を知ることなく、JSONキーでほしいデータが取れるようになっています。
実行してみると思ったとおりのデータが取れました、バッチリですね。
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 なのは処理の中で外に公開する必要がないからです