オンプレの IIS に配置した .NET 8 アプリが Azure Key Vault の認証エラーで起動できない――Visual Studio では動くのに本番だけ失敗する原因は、DefaultAzureCredential が利用できる資格情報が環境ごとに違うためです。IIS での環境変数の渡し方と、安全に運用する考え方をまとめます。
現象:Visual Studio では動くのに、オンプレ IIS だと起動時に落ちる
開発 PC で Visual Studio 2022 から実行すると、Connected Services で Azure Key Vault に接続でき、アプリも問題なく起動する。一方で、同じアプリをオンプレミスの Windows Server + IIS に配置すると、起動時に例外が発生し、Program.cs の AddAzureKeyVault(..., new DefaultAzureCredential()) 付近でプロセスが停止する――このパターンは非常によくあります。
典型的には、次のような例外メッセージの形で表れます(文言は環境により多少異なります)。
Azure.Identity.CredentialUnavailableException:
DefaultAzureCredential failed to retrieve a token from the included credentials.
... (EnvironmentCredential / ManagedIdentityCredential / VisualStudioCredential ... が失敗したログが続く)
まず押さえる:DefaultAzureCredential は「認証手段を順番に試す」仕組み
DefaultAzureCredential は、Azure への認証を 1 つに固定するのではなく、複数の認証方法を「成功するまで順番に試す」ための便利な仕組みです。逆に言うと、開発環境と本番環境で“使える認証手段”が変わると、同じコードでも結果が変わります。
| 認証手段(例) | どこで成功しやすいか | 前提条件・ポイント |
|---|---|---|
| 環境変数(Environment Credential) | オンプレ / VM / コンテナなど広く | AZURE_TENANT_ID、AZURE_CLIENT_ID、AZURE_CLIENT_SECRET などがプロセス環境変数として渡っている必要があります |
| マネージド ID(Managed Identity) | Azure VM / App Service / Functions / AKS など | Azure 側が発行する ID を利用。原則、秘密文字列の配布が不要で運用が楽です |
| 開発者ログイン情報(Visual Studio / Azure CLI など) | 開発 PC | 「その PC のログイン状態」が前提。IIS サーバーには基本的に存在しません |
Visual Studio で動いているのに IIS で動かないときは、ほぼ例外なく「開発 PC では使える認証手段が、本番 IIS プロセスでは使えない」ことが原因です。
なぜ Visual Studio 2022 で成功するのか
開発 PC では、次の要素が揃っていることが多く、DefaultAzureCredential の途中のどこかで認証が通ります。
- Visual Studio にサインインしている(Microsoft アカウント / 組織アカウント)
- Azure CLI(
az login)や Azure PowerShell でログイン済み - 開発用の設定(ユーザー シークレット、ローカルのキャッシュ)が存在する
つまり、開発者が“自分の資格情報”で Key Vault にアクセスできている状態です。ここが、本番環境にそのまま持ち込めない落とし穴になります。
なぜ オンプレ IIS だと失敗するのか:IIS は「別ユーザー」で動く
IIS に配置した ASP.NET Core アプリは、通常 アプリプールの ID(ApplicationPoolIdentity、ドメインユーザー、gMSA など)で動きます。これは開発者のログインユーザーとは別物です。
そしてオンプレ IIS サーバーには、開発 PC にあった次のような“便利な資格情報”が存在しない/参照できないケースがほとんどです。
- Visual Studio のサインイン情報(IIS サーバーに VS を入れていても、w3wp が参照できるとは限りません)
- Azure CLI のログインキャッシュ(存在しても別ユーザーのプロファイルに入ります)
- ユーザー シークレット(Development 限定で、サーバー配布が前提ではありません)
その結果、DefaultAzureCredential が試す認証手段がすべて失敗し、起動時に落ちます。ここまでが「なぜ VS では動いて IIS だと失敗するか」の本質です。
解決策の基本:IIS 上で“本番用の認証手段”を用意する
オンプレ IIS で Key Vault に接続したい場合、代表的な選択肢は次の 3 つです。
| 選択肢 | 秘密情報の扱い | おすすめ度 | 向いているケース |
|---|---|---|---|
| 環境変数でサービスプリンシパル(Client Secret) | クライアント シークレットが必要 | ○(導入が簡単) | すぐ直したい/まず動かしたい |
| 証明書ベースのサービスプリンシパル(Client Certificate) | 秘密文字列を置かず、証明書(秘密鍵)を OS で保護 | ◎(運用が堅い) | オンプレでもセキュリティ重視、長期運用 |
| マネージド ID | 基本的にシークレット不要 | ◎(使えるなら最優先) | Azure 上の実行基盤(VM / App Service 等) |
ここでは、まず多くの現場で採用される「環境変数」を中心に、IIS でハマりやすいポイントを具体的に説明します。
重要:IIS の「アプリ設定(appSettings)」は OS 環境変数ではない
質問で多いのが、「IIS の Configuration Editor で web.config に値を追加したのに、Environment.GetEnvironmentVariable() で見えない」というケースです。これは 入れた場所が違うのが原因であることがほとんどです。
| 設定した場所 | 何のための設定か | Environment.GetEnvironmentVariable() で見える? | ASP.NET Core の IConfiguration で扱える? |
|---|---|---|---|
<appSettings>(従来の web.config) | IIS / .NET Framework の設定に近い | × | 既定では ×(別途読み込み設定が必要) |
<aspNetCore><environmentVariables> | ANCM が起動するプロセスに渡す環境変数 | ○ | ○(既定の構成で環境変数は読み込まれる) |
| Windows のシステム環境変数 | OS レベルで全プロセスに適用(起動時に読み込まれる) | ○ | ○ |
つまり、Key Vault の認証に環境変数を使うなら、IIS の appSettings ではなく、ANCM の <environmentVariables> か OS の環境変数に設定する必要があります。
方法A:web.config の <environmentVariables> で渡す(最短で直したいとき)
ASP.NET Core を IIS でホストしている場合、IIS は ASP.NET Core Module(ANCM)を通じてアプリを起動します。web.config の <aspNetCore> 配下に <environmentVariables> を書くと、アプリのプロセス環境変数として渡せます。
例(値はダミーです。実際のシークレットは貼り付けないでください):
<configuration>
<system.webServer>
<aspNetCore processPath="dotnet" arguments="MyApp.dll" hostingModel="inprocess">
<environmentVariables>
<environmentVariable name="AZURE_TENANT_ID" value="00000000-0000-0000-0000-000000000000" />
<environmentVariable name="AZURE_CLIENT_ID" value="11111111-1111-1111-1111-111111111111" />
<environmentVariable name="AZURE_CLIENT_SECRET" value="__REPLACE_WITH_SECRET__" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</configuration>
ポイントは次のとおりです。
AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRETは スペルが 1 文字でも違うと認証手段として認識されませんweb.configを更新しただけでは反映されないことがあるため、アプリプールの再起動(Recycle)をセットで行う- この方法は「デプロイ成果物にシークレットが入る」形になりやすいので、後述のセキュリティ対策も併せて検討する
方法B:Windows のシステム環境変数として設定する(構成をアプリ外に出したいとき)
アプリの配布物に秘密情報を含めたくない場合は、Windows Server のシステム環境変数として設定する方法が分かりやすいです。設定は GUI でも可能ですが、重要なのは 設定後にプロセスを再起動することです。
- システム環境変数として
AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRETを追加 - IIS のアプリプールを Recycle(または IISRESET)して、w3wp / dotnet プロセスを再起動
環境変数はプロセス起動時に読み込まれるため、設定直後にアプリが落ち続ける場合は「再起動ができていない」「別のアプリプールが動いている」などを疑うと切り分けが早いです。
切り分け:本当に環境変数が見えているかを確認する
本番では値そのものをログに出すのは避けたいので、「存在するかどうか」だけを確認するのが安全です。起動直後に一時的にだけ入れるなら、次のようなログが役に立ちます。
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
logger.LogInformation("AZURE_TENANT_ID exists: {exists}", !string.IsNullOrEmpty(tenantId));
logger.LogInformation("AZURE_CLIENT_ID exists: {exists}", !string.IsNullOrEmpty(clientId));
logger.LogInformation("AZURE_CLIENT_SECRET exists: {exists}", !string.IsNullOrEmpty(clientSecret));
3 つがすべて true になっているのに認証が失敗する場合は、Key Vault 側の権限、ネットワーク到達性、テナント/クライアント ID の取り違えなど、別要因を疑います(後述)。
より意図が明確な実装:ClientSecretCredential を明示する
DefaultAzureCredential は便利ですが、環境によって“どの認証が使われたか”が変わります。運用でのブレを減らしたい場合は、サービスプリンシパルで固定するのが分かりやすいです。
using Azure.Identity;
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
// builder.Configuration.AddAzureKeyVault(new Uri(keyVaultUri), credential);
「環境変数が見えているのに DefaultAzureCredential が選ばれない」といった混乱が減り、障害時の切り分けも早くなります。
シークレット文字列を置かない選択:証明書ベース(Client Certificate)
オンプレ IIS で長期運用するなら、クライアント シークレットよりも 証明書ベースが堅いことが多いです。秘密鍵は Windows の証明書ストアで保護でき、アクセス権も OS 側で制御できます。
ざっくりした流れは次のとおりです。
- Microsoft Entra ID(Azure AD)でアプリ登録を作成し、証明書を資格情報として登録
- 同じ証明書(秘密鍵付き)を IIS サーバーの証明書ストア(通常は LocalMachine)に配置
- アプリプールの実行 ID に秘密鍵へのアクセス権を付与
- アプリはサムプリントなど“識別情報”だけを設定として持ち、認証に使用
例(証明書の取得部分は環境により実装が変わります):
using System.Security.Cryptography.X509Certificates;
using Azure.Identity;
X509Certificate2 LoadCertFromStore(string thumbprint)
{
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certs = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false);
if (certs.Count == 0) throw new InvalidOperationException("Certificate not found.");
return certs[0];
}
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var thumbprint = Environment.GetEnvironmentVariable("AZURE_CLIENT_CERT_THUMBPRINT");
var cert = LoadCertFromStore(thumbprint);
var credential = new ClientCertificateCredential(tenantId, clientId, cert);
この方式だと、アプリが保持するのはサムプリントなどの“公開しても致命傷になりにくい情報”で、秘密鍵は OS の管理領域に閉じ込められます。運用の手間は増えますが、漏えい時のインパクトが下がります。
別案:マネージド ID を使う(使える環境なら最優先)
Azure VM や App Service など、Azure 上の実行基盤を使っているなら、マネージド ID を検討する価値があります。アプリはシークレットを持たずにトークンを取得でき、ローテーションも不要になりやすいからです。
ただし「オンプレ IIS」という前提だと、そのままでは使えないことが多い点に注意が必要です。Azure 上の基盤(Azure VM、Azure App Service、Azure Functions、AKS など)か、Azure Arc などの仕組みで“Azure 側が ID を発行できる状態”になっているかを確認してください。
Key Vault 側の設定も確認:権限とスコープが合っているか
環境変数が正しく渡っていても、Key Vault 側の権限が不足していると認証後の操作で失敗します。よくあるのは次の 2 つです。
- アクセス ポリシー方式で、シークレットの
Get/List権限が付与されていない - Azure RBAC 方式で、Key Vault のスコープに必要なロール(例:Secrets User 相当)が付与されていない
| やりたいこと | 最低限必要になりやすい権限 | 不足すると起きること |
|---|---|---|
| アプリ設定として Key Vault のシークレットを読みたい | Secrets の Get(状況により List) | 起動時にシークレット取得に失敗し、設定値が空で例外になる |
| Key Vault に新しいシークレットを保存したい | Secrets の Set | 書き込みが Forbidden になり、更新処理が失敗する |
「認証エラーに見えるが、実は権限不足」もよくあるため、ログの内側(HTTP 403 など)まで確認すると判断が速くなります。
ネットワーク要因も意外と多い:プロキシ、TLS、時刻ずれ
オンプレから Azure に出る場合、認証そのものよりもネットワークの制約で失敗していることがあります。エラーメッセージが “認証できない” に寄るため、見落としやすいポイントです。
- サーバーから
login.microsoftonline.com(トークン発行)と Key Vault のエンドポイントに HTTPS で到達できるか - 企業プロキシがある場合、IIS のプロセスがプロキシ経由で外部に出られるか(WinHTTP / 環境変数のプロキシ設定など)
- サーバー時刻が大きくずれていないか(JWT の
nbf/expで弾かれる原因になります)
疑問の核心:「appsettings.json は危険扱いなのに、Tenant/Client/Secret は OK なの?」
ここは誤解されやすいのですが、結論から言うと クライアント シークレットは appsettings.json に入れても OK ではありません。危険性の種類が違うだけです。
| 項目 | 秘密情報か | 漏えいした場合のインパクト | 置き場所の考え方 |
|---|---|---|---|
| Tenant ID | 基本は秘密ではない(識別子) | 単体では直接侵入できないが、標的情報として利用され得る | 設定として持ってよいが、むやみに公開しない |
| Client ID | 基本は秘密ではない(識別子) | 単体では直接侵入できないが、攻撃の足がかりになり得る | 設定として持ってよいが、管理範囲は限定する |
| Client Secret | 明確に秘密 | 盗まれると「そのアプリとして」トークンを取られる可能性 | 平文ファイル・ソース管理に置かない。アクセス制御された保管が必須 |
appsettings.json や launchSettings.json が危険扱いされる理由は、次の性質があるからです。
- ソース管理(Git)に入りやすい/レビューや共有の過程で漏れやすい
- ビルド成果物として配布され、サーバー上で平文閲覧できることが多い
- 誤ってログやエラーページに出力されると、二次被害が大きい
一方で Tenant ID や Client ID は “識別子” なので、それ自体が秘密鍵ではないという整理になります。ただし、識別子でも「攻撃者にとっての手がかり」にはなるため、公開範囲を無制限にしてよいという意味ではありません。
現実解:Key Vault を本命の保管庫にしつつ、ブートストラップは最小限にする
「Key Vault からシークレットを取得するために、アプリ側にシークレットが必要」問題は、運用設計として次の形に落ち着くことが多いです。
- アプリが最初に持つのは、Key Vault にアクセスするための最小限の情報だけ
- アプリが必要とする DB パスワードや API キー等は、Key Vault を“唯一の正”として管理
- 可能ならシークレット文字列ではなく、マネージド ID または証明書でブートストラップする
オンプレ IIS の現場で実践しやすい落としどころを整理すると次のとおりです。
| 優先度 | ブートストラップ方法 | 理由 |
|---|---|---|
| 高 | 証明書ベース(Client Certificate) | 秘密文字列の配布を避け、OS の証明書管理とアクセス制御に寄せられる |
| 中 | 環境変数(Client Secret) | 導入が簡単。まず復旧したいときに強い。ただし保護とローテーションが必須 |
| 高(使えるなら最優先) | マネージド ID | 運用負荷が最も低い。シークレット管理が不要になりやすい |
オンプレ IIS でのセキュリティ実践チェックリスト
- 最小権限:Key Vault には必要なシークレットの読み取りだけ(書き込み権限をむやみに付けない)
- スコープ分離:開発・検証・本番でアプリ登録(クライアント ID)を分け、横展開の被害を抑える
- 期限とローテーション:クライアント シークレットには短めの期限を設定し、更新手順を運用に組み込む
- ファイル権限:
web.configを使う場合は NTFS の ACL を絞り、閲覧できるユーザーを限定する - アプリプール ID:実行アカウントを明確にし、不要な権限(ローカル管理者等)で動かさない
- ログの扱い:トラブルシュート時もシークレットの値は絶対に出さない(存在有無、失敗理由だけ出す)
まとめ:IIS で落ちるのは「環境の資格情報の差」。設定場所を揃えれば解決できる
Visual Studio で動くのにオンプレ IIS で落ちるとき、原因はほぼ「開発者のログイン情報に依存していた」ことです。IIS では DefaultAzureCredential が使える認証手段が限られるため、環境変数(正しい場所)、証明書、マネージド IDのいずれかで本番用の認証を成立させる必要があります。
特に「IIS の設定画面で入れたのに環境変数として見えない」場合は、<appSettings> と <aspNetCore><environmentVariables> を取り違えているケースが典型です。まずは設定場所を揃え、アプリプールを再起動し、Key Vault 側の権限とネットワークまで含めて確認する――これが最短の解決手順です。

コメント