ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

ASP.NET Core アプリケーションで Azure Key Vault の使い方を改めて考えてみた

11 月に .NET 6 がリリースされることもあり、現時点での ASP.NET Core アプリケーションにおける Azure Key Vault の使い方を改めて考えてみました。
データベースの接続文字列や API Key などのセンシティブな情報は、アプリケーションのリポジトリ内では管理したくありません。
アプリケーションとセンシティブな情報を分離させる構成を実現する方法にはいつくかありますが、現時点では次の方針がシンプルでいいのではと考えています。

  • Azure の本番環境では、Azure Key Vault を使って Managed Service Identity(MSI)で認証する
  • ローカルの開発環境では、ASP.NET Core の Secret Manger を使う
  • 単体テストでは、センシティブな情報のモッククラスを作る

上記の方針に従って、実際に構成を作って試してみます。

Azure Key Vault にデータベースの接続文字列を登録する

ASP.NET Core アプリケーションからアクセスする SQL Database の接続文字列を登録するシナリオを例に説明していきます。
Key Vault の Secret に WebApp20--SqlConnection という名前で、SQL Connection for Key Vault. という Value を登録します。

f:id:TonyTonyKun:20210912095856p:plain

名前に WebApp20 のプレフィックスを付けた理由は、1つの Key Vault でフロントエンドやバックエンドなど複数のアプリケーションの構成を管理する想定だからです。
JSON の階層構造で管理したいので -- で区切っています。Key Vault の名前には : を使うことができないで注意してください。

ASP.NET Core アプリケーションで Azure Key Vault を参照する

.NET 5 の Web API アプリケーションを作成し、必要な NuGet パッケージをインストールします。

Install-Package Azure.Identity -Version 1.4.1
Install-Package Azure.Extensions.AspNetCore.Configuration.Secrets -Version 1.2.1

開発環境以外では MSI を使って Key Vault に接続するように構成します。

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureAppConfiguration((hostingContext, config) =>
            {
                var settings = config.Build();

                if (!hostingContext.HostingEnvironment.IsDevelopment())
                {
                    var keyVaultUrl = settings["KeyVaultUrl"];
                    config.AddAzureKeyVault(new Uri(keyVaultUrl), new DefaultAzureCredential());
                }
            });

            webBuilder.UseStartup<Startup>();
        });

接続する Key Vault の URL は、appsettings.json で定義しておきます。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "KeyVaultUrl": "https://xxx.vault.azure.net/"
}

アプリケーションで SQL Database の接続文字列を扱いやすくするため、POCO クラスにマッピングします。

public class MySettings
{
    public string SqlConnection { get; set; }
}

Key Vault の Secret から自分のアプリケーション情報だけを取得したいので、WebApp20 のプレフィックスを指定しています。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MySettings>(Configuration.GetSection("WebApp20"));

    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication20", Version = "v1" });
    });
}

動作確認のため、SQL Database の接続文字列を返す API を作ります。

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly MySettings _settings;

    public ValuesController(IOptions<MySettings> optionsAccessor)
    {
        _settings = optionsAccessor.Value;
    }

    [HttpGet]
    public ActionResult<string> Get()
    {
        return Ok(_settings.SqlConnection);
    }
}

Azure 本番環境

ASP.NET Core アプリケーションをデプロイする Azure Web Apps の MSI を有効化し、Key Vault の Access policies を設定します。

$ az webapp identity assign --name "<Web Apps Name>" --resource-group "<Resource Group Name>"
{
  "principalId": "xxx",
  "tenantId": "yyy",
  "type": "SystemAssigned",
  "userAssignedIdentities": null
}

$ az keyvault set-policy --name "<Key Vault Name>" --object-id "xxx" --secret-permissions get list

これで Web Apps 上の ASP.NET Core アプリケーションは、MSI で Key Vault に認証して SQL Database の接続文字列を取得できます。

f:id:TonyTonyKun:20210912114101p:plain

Azure ポータルから設定する場合は、こちらの記事を参照してください。
gooner.hateblo.jp

ローカル開発環境

Visual Studio の機能でローカル開発環境から Key Vault に接続することはできますが、環境によって上手く認証できなかったり、デバッグ実行が遅くなったりするので、今回は使いません。
ASP.NET Core の Secret Manger を使って、プログラマーが開発用の構成情報を管理してコードを書いていくようにします。
docs.microsoft.com

secrets.json ファイルには、このように SQL Database の接続文字列を定義します。

{
  "WebApp20": {
    "SqlConnection": "SQL Connection for Secret Manager."
  }
}

Visual Studio のプロジェクトを右クリックしたユーザーシークレットの管理から定義してもいいですが、アプリケーションのリポジトリとは別で管理する JSON ファイルを dotnet user-secrets コマンドで読み込ませるほうが便利です。

$ type .\webapp20-secrets.json | dotnet user-secrets set --project "C:\Users\thara\source\repos\WebApplication20\WebApplication20"

これでローカル環境では、secrets.json から取得した SQL Database の接続文字列を使って開発できます。

単体テスト環境

単体テストを実行する際には、テストプロジェクト内で MySettings クラスの モックを作り、SQL Database の接続文字列を渡すようにします。

public class ValuesControllerTest
{
    [Fact]
    public void Get_ReturnsOkResult()
    {
        var settings = new MySettings()
        {
            SqlConnection = "SQL Connection for Unit Test."
        };
        IOptions<MySettings> options = Options.Create(settings);
        var controller = new ValuesController(options);

        // Act
        var actionResult = controller.Get();

        // Assert
        var result = Assert.IsType<OkObjectResult>(actionResult.Result);
        Assert.Equal(settings.SqlConnection, (result.Value as string[])[0]);
    }
}

これでローカル開発でも CI/CD のパイプラインでも、実行環境の影響を受けずに単体テストを実行することができます。

まとめ

現時点での ASP.NET Core アプリケーションにおける Azure Key Vault の使い方を改めて考えてみました。
アプリケーションの要件やチームの文化などにもよりますが、ベースはこの方針がいいかなと思っています。
セキュリティという観点では、Key Vault 自体を閉域網構成で管理したい話もあるので、そちらは追加で検討が必要です。