.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なのは処理の中で外に公開する必要がないからです