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