「同じPC」は同じ実行環境ではない ── AppData・HKCU・DPAPI・資格情報を隔てるユーザー境界

· · Windows, DPAPI, レジストリ, AppData, 資格情報, タスクスケジューラ

手元では完璧に動く.exeをタスクスケジューラに登録したら、「設定ファイルが見つからない」と言って落ちた。保存済みパスワードを使うツールをサービス化したら、復号のところでCryptographicExceptionが飛んだ。ログイン済みのブラウザを操作する自動化スクリプトが、CIエージェント上では毎回ログイン画面から始まる──どれもWindowsの開発現場で繰り返し起きている事故で、原因はすべて同じ場所にあります。

私たちは無意識に「同じPCで動かしているのだから、同じ環境のはずだ」と考えます。しかしWindowsでは、この直感は成り立ちません。この記事が答える疑問は、ただ1つです。

コードを1文字も変えていないのに、実行のされ方を変えただけでアプリが壊れるのはなぜか。

対象読者は、業務アプリのサービス化・タスク化・CI化を行う開発者と、その種の「手元では動くのに」を調査する運用担当者です。前提環境はWindows 10/11で、検証コードはPowerShellで示します。難易度は中級です。

1. まず結論

Windowsの実行環境の単位はPCではなく、「誰として動いているか」──アクセストークンとSID──である。同じPCの中には、ユーザーの数だけ互いに見えない世界が併存しており、AppData・HKCU・DPAPIの鍵・ブラウザプロファイル・資格情報はすべてその世界ごとに別物になる。

共有されているのは、HKLMProgramDataといったマシン全体の領域だけです。実行のされ方(ダブルクリック、タスク、サービス、IIS)が変わると実行ユーザーが変わり、実行ユーザーが変わると、この5つがまとめて入れ替わります。

同じPCの中の2つの世界同じPCはHKLMとProgramDataだけを共有し、あなたのSIDの世界と別SIDの世界がそれぞれ独自のAppData・レジストリハイブ・DPAPI鍵を持ち、ユーザー境界で互いに見えないユーザー境界:互いに見えない同じPC共有:HKLM・ProgramDataあなたのSIDの世界別SIDの世界(SYSTEMなど)AppData・HKCU・DPAPI鍵別のAppData・別ハイブ・別の鍵

図1: 実行環境を区切っているのはPCの筐体ではなくユーザー境界である。

以降、この境界を作っている仕組み(§2)を押さえてから、5つの境界──AppData(§3)、HKCU(§4)、DPAPI(§5)、ブラウザプロファイル(§6)、資格情報(§7)──を順に歩き、実行形態別のチェックリスト(§8)と設計・調査の指針(§9)にまとめます。

この記事の知識マップ

ユーザープロファイルは実行ユーザーのSIDを前提に作られ、%LOCALAPPDATA%・%APPDATA%(Roaming)・NTUSER.DATはそのプロファイル配下に保存され、CurrentUserスコープのDPAPIマスターキーもプロファイル配下に置かれる。ユーザーの証明書ストアの実体はHKCU配下のレジストリにある。通常のログオンではHKCU(Classes配下を除く)の実体はロードされたNTUSER.DATであり(LocalSystemでは既定ユーザーに関連付く)、サービスからHKCUを使うことは推奨されない。DPAPIはマスターキーで秘密を保護するため、CurrentUserスコープは別ユーザーからの復号失敗の原因になることがあり、LocalMachineスコープはそれを軽減する。資格情報マネージャーの中身はDPAPIで保護され、保存したユーザーの文脈でのみ使える。Chromium系ブラウザーのプロファイルは既定で%LOCALAPPDATA%配下にありDPAPIを利用するので、フォルダーコピーではログイン状態を持ち出せない。タスクスケジューラの実行はログオン種別で、IISアプリケーションプールのプロファイル読み込みはloadUserProfile設定で構成する。S4Uログオンは暗号化ファイル(EFS)とも資格情報マネージャーの金庫とも両立せず、管理者によるパスワードリセットはDPAPI復号失敗の原因になり得る。ASP.NET Core Data Protectionの鍵リングはプロファイルが使える環境で%LOCALAPPDATA%に置かれDPAPIで暗号化される。全ユーザー・サービスで共有するデータにはProgramData配下、サービスの証明書にはコンピューターの証明書ストアが推奨される。ネットワークドライブの割り当てはログオンセッションに保存され、UAC昇格したプロセスからは既定で見えない。実行ユーザーのSIDはwhoamiとProcess Explorerで、実際に読まれたプロファイル配下のパスはProcess Monitorで確認できる。AppData\LocalLowもプロファイル配下にあり、HKCU\Software\Classesの実体UsrClass.datは%LOCALAPPDATA%配下に置かれる。サービスがユーザーの設定を読む必要がある場合は偽装(impersonation)とRegOpenCurrentUserの利用が推奨される。LocalServiceアカウントは自分のユーザープロファイルを持ち、LocalSystemのコンテキストはPsExecで対話シェルを開いて確認できる。

Windowsのユーザー境界の知識マップSIDに紐づくユーザープロファイルがAppData・NTUSER.DAT・DPAPIマスターキーを抱え、ユーザーの証明書ストアがHKCUに保存され、資格情報マネージャーとChromium系ブラウザーのプロファイルがDPAPIに依存し、DPAPIのスコープ選択が復号失敗の有無を分け、タスクスケジューラとIISアプリプールがそれぞれログオン種別とloadUserProfileで構成され、S4Uログオンの制約・パスワードリセットの影響・ProgramDataや証明書ストアの推奨・ネットワークドライブとログオンセッション・whoamiやSysinternalsによる確認手段、LocalLow・UsrClass.dat・偽装・LocalService/LocalSystemアカウントとの関係までを含む関係図前提とするに保存されるに保存される用いるのは非推奨に保存されるに保存される利用するに保存される原因になり得る軽減する利用するに保存されるに保存される利用するで構成できるで構成できる両立しない両立しない原因になり得る利用するに保存される推奨される対応推奨される対応に保存される両立しないで確認できるで確認できるで確認できるに保存されるに保存される推奨される対応利用するで確認できるSID(セキュリティ識別子)ユーザープロファイルDPAPI資格情報マネージャー(cmdkey)NTUSER.DATHKEY_CURRENT_USER(HKCU)Windowsサービス%LOCALAPPDATA%%APPDATA%(Roaming)DPAPIマスターキーDataProtectionScope.CurrentUserDPAPI復号失敗DataProtectionScope.LocalMachineユーザーの証明書ストアChromium系ブラウザーのプロファイルタスクスケジューラの無人実行ログオン種別(LogonType)IISアプリケーションプールloadUserProfile設定S4UログオンNTFS暗号化(EFS)ファイル管理者によるパスワードのリセットASP.NET Core Data Protection%ProgramData%配下の保存先コンピューターの証明書ストアネットワークドライブ(マップされたドライブ文字)ログオンセッションUAC昇格(管理者として実行)whoamiコマンドProcess ExplorerProcess Monitor(procmon.exe)AppData\LocalLowHKEY_CURRENT_USER\Software\Classes偽装(impersonation)LocalServiceアカウントLocalSystemアカウントPsExec

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全33件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. 前提: 「誰として動くか」がすべてを決める

Windowsがユーザーを識別する本当のIDは、ユーザー名ではなくSID(セキュリティ識別子)です。すべてのプロセスはアクセストークンを持ち、そこに「誰として動いているか」=SIDが焼き込まれています。ファイルのACL判定も、レジストリの実体選択も、暗号鍵の選択も、このトークンを起点に行われます。

「今、自分のコードは誰として動いているのか」はwhoamiで確認できます。この種のトラブルシュートの第一歩は、例外なくこれです。

> whoami /user

ユーザー情報
----------------

ユーザー名      SID
=============== =============================================
desktop\you     S-1-5-21-3623811015-3361044348-30300820-1001

C:\Users\youというフォルダーは、このSIDに紐づくユーザープロファイルの実体です。プロファイルは「AppDataなどのフォルダー群」と「ユーザーレジストリハイブNTUSER.DAT」から成り、初回ログオン時に既定プロファイルから複製されて作られます。1つまり、別ユーザーのプロファイルは「まっさらな初期状態から始まる別物」です。

起動経路からプロファイルまでの流れダブルクリック・タスクスケジューラ・サービスやIISのどの経路で起動しても、プロセスはアクセストークンのSIDを持ち、そのSIDに紐づくユーザープロファイル一式が実行環境になるダブルクリックプロセスのトークン(SID)タスクスケジューラサービス・IISSIDに紐づくプロファイル一式別SIDなら初期状態の別プロファイル

図2: どの経路で起動されるかは、どのSIDのプロファイルを背負って動くかを決める。

ここで伏線がひとつあります。Windowsには、人間がログオンしなくても動く組み込みアカウントが複数あり、それぞれが独立した世界を持っています2

アカウント SID 世界の場所
SYSTEM (LocalSystem) S-1-5-18 プロファイルはC:\Windows\System32\config\systemprofile配下。HKCUは既定ユーザーに関連付く3
LocalService S-1-5-19 C:\Windows\ServiceProfiles\LocalService配下。HKEY_USERSに自分のサブキーを持つ4
NetworkService S-1-5-20 C:\Windows\ServiceProfiles\NetworkService配下。同上
IIS AppPool\<名前> S-1-5-82-… プール専用のID。プロファイルは既定では読み込まれない5

サービス・タスク・IISで動くコードの多くは、この「人間じゃないユーザー」の世界に振り分けられます。あなたが対話ログオンで整えた環境は、そこには存在しません。

3. 境界1: AppData ── 同じ環境変数が別の場所を指す

事故例: エクスプローラーから起動すると動くのに、タスクスケジューラから起動すると「設定ファイルが見つからない」。

アプリの設定・キャッシュの標準置き場であるAppDataは、プロファイル配下、つまりユーザーごとに別のフォルダーです。%APPDATA%(Roaming)はプロファイルの移動に追従させたいユーザー設定、%LOCALAPPDATA%(Local)はマシンローカルなデータ・キャッシュ、LocalLowは低い完全性レベルのプロセス用という区分になっています。6

重要なのは、これらの環境変数が「実行しているユーザー」のプロファイルへ解決されることです。ユーザーAで保存した設定ファイルはC:\Users\a\AppData\Roaming\MyAppにありますが、SYSTEMで動くタスクが同じコードで%APPDATA%を開くと、解決先はsystemprofile配下になります。パスの解決自体は成功し、ファイルだけが「無い」。エラーメッセージからは原因が見えにくい、嫌らしい壊れ方をします。

%APPDATA%の解決はユーザーで変わる同じコードが%APPDATA%を開いても、実行ユーザーがあなたならC:\Users配下、SYSTEMならsystemprofile配下に解決され、後者にはあなたの設定ファイルが存在しないあなたSYSTEM同じコード:%APPDATA%を開く実行ユーザーは?C:\Users\you\AppData\Roamingsystemprofile配下のAppData置いたはずのファイルが無い

図3: 環境変数は嘘をつかないが、解決先はトークンで変わる。

環境変数そのものにも同じ境界があります。システム環境変数はマシン共有ですが、ユーザー環境変数(PATHへの追記など)はユーザーごとです。「自分のターミナルでは通るコマンドが、サービスからは見つからない」の一因はここにあります。

対策は、パスを環境変数任せにせず「誰が読むデータか」で置き場所を決めることです。そのユーザーだけの設定ならAppData、マシン上の全ユーザー・サービスで共有するデータなら%ProgramData%配下に置き、ACLを設計します。データの置き場所の選び方は「Windowsアプリのローカルデータはどこに置くべきか」で詳しく扱っています。

保存先は読者で決めるそのユーザーだけが読むデータはAppDataやHKCUに、全ユーザーやサービスと共有するデータはProgramDataやHKLMに置き、後者はACL設計を伴うそのユーザーだけ全ユーザー・サービスそのデータを読むのは誰かAppData・HKCUProgramData・HKLM将来サービス化するなら再考書き込み権限とACLを設計する

図4: 「サービス化したら読めない」は、設計時にこの分岐を飛ばした結果である。

4. 境界2: HKCU ── 「現在のユーザー」は呼び出し元で変わる

事故例: インストーラーが書き込んだはずのライセンス情報が、サービスから読むと「存在しない」。

HKEY_CURRENT_USER(HKCU)は独立したハイブではなく、呼び出し元プロセスのユーザーに応じて実体へ振り替えられる別名です。あなたのプロセスから見たHKCUの実体はHKEY_USERS配下のあなたのSIDのキーで、その中身はプロファイル内のNTUSER.DATがログオン時にロードされたものです(例外としてHKCU\Software\Classes配下だけは、UsrClass.datという別ファイルを実体とするユーザーごとのハイブです)。17

そしてLocalSystemのプロセスでは、HKCUは既定ユーザー(HKEY_USERS\.DEFAULT)に関連付けられます3インストーラーを管理者アカウントで実行してHKCUに書き、アプリをSYSTEMのサービスとして動かすと、書いた場所と読む場所が完全に食い違うわけです。Microsoftのドキュメントも、サービスからHKCUへアクセスすべきでなく、ユーザーの設定が必要ならユーザーを偽装したうえでRegOpenCurrentUserを使うよう明記しています。8

HKCUという別名の実体アプリがHKCUを開くと、あなたのプロセスではHKEY_USERS配下のあなたのSIDキーに、LocalSystemのプロセスでは既定ユーザーのキーに振り替えられ、インストーラーが書いた値が見えなくなるあなたのプロセスSYSTEMのプロセスアプリのコード:HKCUを開くHKCUは実体への別名HKEY_USERS配下のあなたのSIDHKEY_USERS\.DEFAULT書いたはずの値が存在しない

図5: HKCUに書くコードは、実行ユーザーが変わると読み書き先が黙って変わるコードである。

もう一段深い罠が、プロファイルがロードされていない状態です。タスクスケジューラの実行構成には「ログオン種別」という軸があり、「パスワードを保存しない」を選んだタスクはS4Uというログオンで動きます。S4Uではパスワードが保存されないため、ネットワークリソースと暗号化ファイル(EFS)にアクセスできません9同じユーザー名を指定していても、対話ログオンと同じ環境がそこに再現されるとは限らない──「実行ユーザーが同じでも、ログオンのされ方という軸がもう1本ある」ことは覚えておく価値があります。タスクスケジューラの実行アカウントとログオン種別の設計は「タスクスケジューラで業務バッチを安定運用する」で掘り下げています。

タスクのログオン種別という第2の軸タスクスケジューラの実行構成は対話トークン・パスワード保存・S4Uのログオン種別を持ち、S4Uではパスワードが保存されずネットワークとEFSにアクセスできない対話トークンパスワード保存S4U(パスワード非保存)タスクの実行構成ログオン種別ログオン中のセッションで実行非対話でも資格情報を使えるネットワークとEFSに届かない

図6: 同じ実行ユーザーでも、ログオンのされ方で使えるものが変わる。

IISのアプリケーションプールにも同じ軸があります。プールのワーカープロセスは既定ではユーザープロファイルを読み込まずに動き、loadUserProfileを有効にして初めてプロファイル配下(AppData・ハイブ)が使えるようになります。10この設定は次章のDPAPIと組み合わさって、Webアプリの定番事故を生んできました。

5. 境界3: DPAPI ── 暗号は「ユーザーの鍵」で掛かっている

事故例: アプリが保存した接続文字列のパスワードが、サービス化した途端にCryptographicExceptionで復号できない。

ここまでの2つは「見えない・無い」という境界でしたが、DPAPIは「見えているのに開けない」境界です。そして後述するブラウザと資格情報の章は、実はこの章の応用編です。

DPAPI(Data Protection API)は、Windowsがアプリに提供する「鍵管理不要の暗号化」です。CryptProtectDataや.NETのProtectedData.Protectを呼ぶだけで暗号化でき、鍵をコードで持ち回る必要がありません。11代わりに、その鍵はOSが管理するマスターキーです。CurrentUserスコープでは、ランダムに生成されたユーザーごとのマスターキーが、ログオン資格情報から派生した鍵で暗号化(保護)されてプロファイル配下に保存されます。12つまり──暗号はユーザーの鍵で守られています

DPAPIの鍵の連鎖ランダム生成されたユーザーのマスターキーはログオン資格情報から派生した鍵で保護され、そのマスターキーがアプリの秘密を暗号化しており、別ユーザーのマスターキーでは同じ暗号文を復号できない派生鍵で保護暗号化復号できないログオン資格情報あなたのマスターキーアプリの秘密(保存パスワード等)別ユーザーのマスターキー保存先はプロファイル配下

図7: マスターキー自体は乱数で、資格情報はそれを守る鍵の材料である。復号できるのは鍵の持ち主だけになる。

体験するのが一番早いので、PowerShellでの最小再現を置いておきます。

# ユーザーAのセッションで: CurrentUserスコープで暗号化して共有場所に保存
Add-Type -AssemblyName System.Security
$bytes = [Text.Encoding]::UTF8.GetBytes("secret")
$enc = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, "CurrentUser")
[Convert]::ToBase64String($enc) | Set-Content C:\ProgramData\demo.bin

# 別ユーザー(たとえばPsExecでSYSTEMになって)復号を試みる
$enc = [Convert]::FromBase64String((Get-Content C:\ProgramData\demo.bin))
[Security.Cryptography.ProtectedData]::Unprotect($enc, $null, "CurrentUser")
# → CryptographicException: 現在の状態では無効なデータです

押さえるべきポイントは3つです。

  • スコープが2つある。 CurrentUserはユーザーのマスターキー、LocalMachineはマシン共通の鍵で暗号化します。13サービスと対話ユーザーの両方から読みたい秘密は、最初からLocalMachineを選び、読者はファイルACLで絞ります。ただし共用端末では「同じマシンの誰でも復号できる」ことが逆にリスクになる点に注意してください。
  • ユーザーの資格情報と運命共同体。 マスターキーを守る鍵はログオン資格情報から派生するため、ローカルアカウントのパスワードを管理者がリセットすると過去の暗号文が復号できなくなることがあります(ユーザー自身のパスワード変更操作では、マスターキーが新しいパスワード由来の鍵で保護し直されるため復号能力を引き継ぎます)。12
  • 気づかないうちに乗っている。 ASP.NET CoreのData Protection(Cookie認証などの保護)は、ユーザープロファイルが使える環境では鍵リングを%LOCALAPPDATA%\ASP.NET\DataProtection-Keysに置き、DPAPIで暗号化します。プロファイルが使えない場合、IISホストならHKLMレジストリ配下(ワーカープロセスアカウントにACLされた場所)へフォールバックしますが、どちらにも当てはまらない環境では鍵がプロセス内限りの一時キーになり、再起動のたびに保護済みデータ(認証Cookieなど)が無効になります。14「鍵がどこに置かれる構成なのか」を、loadUserProfilesetProfileEnvironment・ホスト形態とセットで意識しておく必要があります。
DPAPIスコープの選び方復号できるべき主体がそのユーザーだけならCurrentUserスコープ、同じPCの複数の実行主体ならLocalMachineスコープを選び、後者はファイルACLで読者を絞るそのユーザーだけ同じPCの複数の実行主体誰が復号できるべきかCurrentUserスコープLocalMachineスコープ別ユーザー実行で復号失敗読者はファイルACLで絞る

図8: スコープは「たまたま動いた方」ではなく、復号者の設計として選ぶ。

DPAPIとスコープ選択、Credential Managerとの使い分けの詳細は「Windowsアプリの機密情報保存 - DPAPIで平文設定を避ける」にまとめています。

6. 境界4: ブラウザプロファイル ── 「ログイン済み」はユーザーの持ち物

事故例: 手元ではPlaywrightがログイン済みセッションで動くのに、CIエージェント上では毎回ログイン画面から始まる。

ブラウザは、ここまでの境界1と境界3をフルコースで踏んでいる代表例です。ChromeやEdgeのプロファイル(履歴・Cookie・拡張機能・保存パスワード)は%LOCALAPPDATA%配下のUser Dataフォルダーにあります。15つまりブラウザの「ログイン済み」という状態は、Windowsユーザーの持ち物です(境界1)。

さらにCookieや保存パスワードは平文で置かれているわけではありません。プロファイル内に格納された暗号鍵で暗号化されており、その鍵自体がDPAPIで保護されています(境界3)。近年のChromeはこれにアプリ束縛暗号化(App-Bound Encryption)を重ね、鍵の復号にSYSTEM権限のサービスを経由させることで、「どのユーザーか」に加えて「どのアプリが要求したか」まで検証するようになりました。16プロファイルフォルダーを別ユーザー・別マシンへコピーしても、鍵が合わずに開けない構造です。

ブラウザのログイン状態を支える2つの境界ブラウザプロファイルはLOCALAPPDATA配下にあり境界1に属し、Cookieの暗号鍵はDPAPIで保護され境界3に属するため、別ユーザーにはプロファイルも鍵も引き継がれないブラウザプロファイル置き場所はLOCALAPPDATA配下Cookie暗号鍵はDPAPIで保護CIの実行ユーザーには空の別プロファイルフォルダーコピーでは持ち出せない

図9: 「ログイン済みを持っていく」は境界1と境界3の両方に阻まれる。

自動化での含意を整理しておきます。まず前提として、Selenium/Playwrightは既定で使い捨ての一時プロファイルを作って起動するため、同じユーザーで動かしても普段のログイン状態は使われません。ではプロファイルの永続ディレクトリを指定して普段の状態を使い回せばよいかというと、そこに立ちはだかるのがこの章のユーザー境界です──CIエージェントやサービスの実行ユーザーには、そのプロファイル自体が存在せず、コピーしても鍵が合いません。対策は「ログイン済みプロファイルをコピーする」ことではなく、テスト用アカウントのログイン手順をコード化するか、自動化ツール側のストレージステート機構(Cookieの明示的な保存・復元)を使うことです。逆に言えば、この境界はセキュリティ機能そのものでもあります。別ユーザーからCookieが抜けないことが、共用マシンの前提を守っています。

7. 境界5: 資格情報と証明書 ── 金庫はユーザーごとに別

事故例: cmdkeyで保存したファイルサーバーの資格情報が、タスク実行時には使われず認証エラーになる。

Windowsの資格情報マネージャー(cmdkey /listで見えるもの)は、ユーザーごとの金庫です。17保存された資格情報の実体はディスク上に置かれますが、例によってDPAPIで保護されているため、復号して使えるのは保存したユーザーの文脈だけです。18ここに入っているものは意外に多く、ネットワークドライブ・ファイルサーバーの保存資格情報、git-credential-managerが保存したGitのトークン、RDP接続の保存パスワード、各種アプリがCredential API経由で保存したシークレットなどが該当します。

どれも「対話ログオンしたあなた」の金庫に貯めたものです。サービスやタスクの実行ユーザーの金庫は、そのアカウントとして資格情報を投入するセットアップをしない限り空です。git pullが手元で通るのは金庫のおかげ、同じコマンドがサービスから失敗するのは金庫が別だから、という構図が頻出します。

資格情報の金庫はユーザーごとあなたの金庫にはgitの資格情報やファイルサーバーの保存資格情報やRDPのパスワードが入っているが、サービス実行ユーザーの金庫は資格情報を投入しない限り空であり、それが認証エラーの正体になるあなたの金庫gitの資格情報ファイルサーバーの保存資格情報RDPの保存パスワードサービス実行ユーザーの金庫未投入なら空=認証エラーの正体

図10: 「手元では認証が通る」は「あなたの金庫が使える」の略でしかない。

証明書ストアにも同じ軸があります。ストアにはCert:\CurrentUserCert:\LocalMachineの2系統があり、クライアント証明書をCurrentUserにインポートすると、その秘密鍵はユーザー単位で保護されます。サービスから使う証明書はLocalMachineストアに入れ、秘密鍵のACLでサービスアカウントに読み取りを許可するのが定石です。19なお、ユーザーストアの実体はレジストリのHKCU\Software\Microsoft\SystemCertificates配下──つまり境界2の内側──にあります。20ストアの選び方と権限設計は「Windows証明書ストア実務ガイド ── ユーザーとコンピューター、どちらに入れるか」を参照してください。

最後に、ユーザー境界の変種としてUAC昇格があります。UACが有効な管理者ユーザーのログオンでは、権限を制限した標準トークンと完全な管理者トークンという、リンクされた2つのトークンが作られます。21ネットワークドライブの割り当てはログオンセッション単位なので、「エクスプローラーではZ:が見えるのに、管理者として実行したツールからはZ:が無い」が起きます。22さらに別の管理者アカウントの資格情報で昇格した場合、そのプロセスのHKCUや金庫は「その管理者の」ものになり、境界2と境界5を同時に踏みます。「同じPC」どころか「同じユーザー」ですら、常に同じ世界とは限らないわけです。

UAC昇格が生む同一ユーザー内の分裂UAC有効な管理者のログオンは標準トークンと昇格トークンの2つを作り、標準トークン側で割り当てたネットワークドライブが昇格トークンのプロセスからは見えない別のログオンセッション管理者ユーザーのログオン標準トークン昇格トークンここで割り当てたZ:ドライブ昇格したツールからZ:が見えない

図11: 昇格は「同じユーザーの別世界」を作る。境界はSIDだけの話ではない。

8. 実行形態別チェックリスト

ここまでの5つの境界を、実行形態ごとに「何を踏むか」で整理します。新しい実行形態にコードを載せるときの点検表として使ってください。

実行形態 誰として動くか 踏みやすい境界と症状
タスクスケジューラ 登録時に指定したアカウント。S4Uならパスワード非保存 境界1・2・3・5 ── %APPDATA%が別の場所を指す。S4Uはネットワーク・EFS不可。DPAPI復号失敗。保存資格情報が使えない
Windowsサービス SYSTEM/LocalService/NetworkService/サービスアカウント 境界1〜5すべて ── SYSTEMならsystemprofile側のAppData・HKCUは既定ユーザー、LocalService/NetworkServiceはServiceProfiles配下の各自の世界。いずれでも、あなたの鍵・金庫・ブラウザ状態は別世界
IISアプリプール IIS AppPool\<名前>(プール固有のID) 境界1・2・3・5 ── 既定でプロファイル未ロード。Data Protectionの鍵置き場が変わる。CurrentUser証明書の秘密鍵に届かない
RunAs/UAC昇格 指定したユーザー、または同一ユーザーの別トークン 同一アカウントの昇格ならSIDは同じ(HKCU・金庫もそのまま)で、分裂するのはログオンセッション単位のネットワークドライブ割り当て。別アカウントでRunAs/昇格したときはSIDごと変わるため境界1〜5すべてを踏み得る(まず現れやすいのはHKCUと金庫の入れ替わり)
CI/CDエージェント エージェントのサービスユーザー(対話ログオン歴なしが多い) 境界1〜5すべて ── ブラウザプロファイルもログイン状態も無く(境界4)、gitの資格情報も無い(境界5)。開発者のHKCU設定やDPAPI保護データも読めない(境界2・3)
RDP/共用サーバー 同じユーザーの多重セッション、または多数ユーザーが同居 同一ユーザーの多重セッションはSIDが同じなので同じAppData・HKCUを共有し、だからこそ書き込みが競合する(境界を跨ぐのではなく共有が問題)。別ユーザーが同居する側はSIDが違うので境界1〜5どおり互いに別世界

9. 設計とトラブルシュートの指針

設計側の原則は、ここまでの各章で見たとおり「読者が誰かで置き場所を決める」に尽きます。ユーザー固有の設定はAppData/HKCUへ、マシン全体で共有するデータはProgramData/HKLMへ、秘密情報はDPAPIのスコープを意図して選ぶ。そしてサービスが使う証明書・資格情報は、実行アカウント自身のストア・金庫へセットアップ手順として明示的に投入する──「たまたま自分の環境にあったものを使う」を設計から排除します。

調査側は、「誰として動いているか」の確認から始めるのが最短です。

  1. whoami /allをコードの起動直後にログへ出す(タスク・サービスに仕込めるなら最強の一手)
  2. Process Explorerでプロセスのトークン(ユーザー・セッション)を確認する
  3. Process Monitorで「実際にどのパス・どのレジストリキーを読んだか」を見る。PATH NOT FOUNDの行に期待と違うプロファイルのパスが映っていたら、この記事のどれかを踏んでいる
  4. psexec -s -i cmdでSYSTEMの世界に入り、向こう側から同じ操作を再現してみる
ユーザー境界トラブルの調査手順whoamiで実行ユーザーを確認し、Process Explorerでトークンを見て、Process Monitorで実際に読んだパスとレジストリキーを特定し、必要ならpsexecで相手の世界から再現するwhoami /allで実行ユーザーを確認Process Explorerでトークンを確認ProcMonで実際のパス・キーを特定psexecで相手の世界から再現期待と違うプロファイルパスが手がかり

図12: 調査は症状のログではなく「誰として動いたか」から入ると迷子にならない。

10. まとめ

この記事の要点です。

  • Windowsの実行環境の単位はPCではなく、アクセストークンとSID──「誰として動いているか」です。
  • AppDataとユーザー環境変数は実行ユーザーのプロファイルへ解決され(境界1)、HKCUは呼び出し元で実体が変わる別名です(境界2)。3
  • DPAPIのCurrentUserスコープはユーザーのマスターキーに依存し、別ユーザーからは復号できません(境界3)。12ブラウザのログイン状態(境界4)と資格情報マネージャー(境界5)は、その応用です。
  • タスクのログオン種別・IISのプロファイル読み込み・UAC昇格という「同じユーザーでも世界が変わる」第2の軸があります。91021
  • レビューで問うべきはひとつ──「これは、どのユーザーとして動いても正しいコードか?」

「手元で動く」は「そのユーザー・そのプロファイル・その金庫で動く」の略でしかありません。実行形態が変わる瞬間(サービス化・タスク化・CI化)は、コードが1文字も変わっていなくても環境移行です。そう扱うだけで、この種の事故の大半は設計段階で消せます。

関連記事

関連する相談領域

合同会社小村ソフトでは、業務アプリのサービス化・タスク化に伴う実行環境の設計、「手元では動くのに」型の不具合調査、Windowsアプリの秘密情報管理の設計を扱っています。

参考リンク

  1. Microsoft Learn, About User Profiles. ユーザープロファイルが初回ログオン時に作られること、プロファイルがレジストリハイブNTUSER.DAT(ログオン時にロードされHKEY_CURRENT_USERにマップされる)とファイルシステム上のプロファイルフォルダー群から成ることについて。  2

  2. Microsoft Learn, Local accounts. SYSTEM(S-1-5-18)・NETWORK SERVICE(S-1-5-20)・LOCAL SERVICE(S-1-5-19)がOSとサービスの実行に使われる既定のローカルシステムアカウントであることについて。 

  3. Microsoft Learn, LocalSystem Account. LocalSystemのトークンがNT AUTHORITY\SYSTEMを含むこと、どのログオンユーザーアカウントとも関連付かないこと、そのためHKEY_CURRENT_USERが既定ユーザーに関連付けられ、他ユーザーのプロファイルへアクセスするにはそのユーザーを偽装する必要があることについて。  2 3

  4. Microsoft Learn, LocalService Account. LocalServiceアカウントがHKEY_USERSの下に自分のサブキーを持ち、HKEY_CURRENT_USERがLocalServiceアカウントに関連付けられることについて。NetworkServiceも同様(NetworkService Account)。 

  5. Microsoft Learn, Application Pool Identities. アプリケーションプールがプール固有のIDで動くこと、IISが既定ではWindowsユーザープロファイルを読み込まないこと、LoadUserProfile属性をtrueにするとプロファイルを読み込めることについて。 

  6. Microsoft Learn, KNOWNFOLDERID. FOLDERID_RoamingAppData(%APPDATA%)・FOLDERID_LocalAppData(%LOCALAPPDATA%)・FOLDERID_LocalAppDataLowがユーザーごとの既知フォルダーとして定義されていることについて。 

  7. Microsoft Learn, Error occurs during desktop setup and desktop location is unavailable. ユーザープロファイルがNTUSER.DATとUsrClass.datという2つのハイブファイルを持ち、UsrClass.datがAppData\Local\Microsoft\Windows配下に置かれることについて。 

  8. Microsoft Learn, Services and the Registry. サービスがHKEY_CURRENT_USERやHKEY_CLASSES_ROOTへアクセスすべきでないこと、ユーザーを偽装する場合はRegOpenCurrentUser関数を使うべきことについて。 

  9. Microsoft Learn, logonType Simple Type. タスクのログオン種別にS4U・Password・InteractiveTokenがあること、S4Uログオンではパスワードが保存されず、ネットワークにも暗号化ファイルにもアクセスできないことについて。  2

  10. Microsoft Learn, Process Model Settings for an Application Pool. アプリケーションプールのprocessModelにloadUserProfile属性とsetProfileEnvironment属性があり、ワーカープロセスがユーザープロファイルを読み込むかどうかを制御することについて。  2

  11. Microsoft Learn, CryptProtectData function. CryptProtectDataが通常はログオンしているユーザーに関連付けられたセッションキーでデータを保護し、同じユーザーでの復号を前提とすること、CRYPTPROTECT_LOCAL_MACHINEフラグでマシン単位の保護に切り替えられることについて。 

  12. Microsoft Learn, Windows Data Protection. DPAPIがランダムに生成したマスターキーを、ユーザーのパスワードから派生した鍵で暗号化して保護すること、マスターキーがユーザープロファイル配下に保存されること、パスワード変更時にマスターキーが保護し直されることについて。  2 3

  13. Microsoft Learn, ProtectedData Class. DataProtectionScope.CurrentUserが保護したユーザーのみに復号を許し、LocalMachineが同じマシン上の任意のプロセスからの復号を許すことについて。 

  14. Microsoft Learn, Data Protection key management and lifetime in ASP.NET Core. ユーザープロファイルが利用可能な場合、鍵が%LOCALAPPDATA%\ASP.NET\DataProtection-Keysに保存されWindowsではDPAPIで暗号化されること、IISホストでプロファイルが使えない場合はワーカープロセスアカウントにACLされたHKLMレジストリへフォールバックすること、どの条件にも当てはまらない場合は鍵がプロセス終了とともに失われ保護済みペイロードが復号できなくなること、setProfileEnvironment属性が関わることについて。 

  15. Chromium project, User Data Directory. WindowsにおけるChromeのUser Dataディレクトリの既定が%LOCALAPPDATA%\Google\Chrome\User Dataであり、プロファイル(履歴・ブックマーク・Cookie等)がその配下に置かれることについて。 

  16. Google Security Blog, Improving the security of Chrome cookies on Windows. ChromeがWindowsでCookie等の暗号化にDPAPIを使ってきたこと、App-Bound Encryptionにより鍵をSYSTEM権限のサービス経由で保護し、復号を要求したアプリのIDまで検証するようになったことについて。 

  17. Microsoft Learn, cmdkey. cmdkeyコマンドで保存済みのユーザー名とパスワード(資格情報)の一覧表示・作成・削除ができることについて。 

  18. Microsoft Learn, Cached and Stored Credentials Technical Overview. 資格情報マネージャーに保存された資格情報がディスク上に置かれDPAPIで保護されること、そのユーザーとして動くプログラムがこのストアの資格情報にアクセスできることについて。 

  19. Microsoft Learn, Local Machine and Current User Certificate Stores. 証明書ストアにローカルマシンストア(マシン全体)とカレントユーザーストア(ユーザーごと)の2種類があることについて。 

  20. Microsoft Learn, System Store Locations. CERT_SYSTEM_STORE_CURRENT_USERのシステムストアがレジストリのHKEY_CURRENT_USER\Software\Microsoft\SystemCertificates配下に置かれることについて。 

  21. Microsoft Learn, How User Account Control works. UAC有効時、管理者ユーザーのログオンで標準ユーザートークンと完全な管理者アクセストークンという2つのリンクされたトークンが作られることについて。  2

  22. Microsoft Learn, Mapped drives are not available from an elevated prompt. 標準トークンでログオンしたセッションで割り当てたネットワークドライブが、昇格したプロセスから利用できないこと、およびその背景としてリンクされた2つのログオンセッションが別々にドライブ割り当てを持つことについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

エクスプローラーから起動すると動くアプリが、タスクスケジューラからだと設定ファイルを見つけられません。なぜですか?
%APPDATA%などの環境変数が「実行しているユーザー」のプロファイルに解決されるためです。タスクをSYSTEMや別アカウントで実行すると、環境変数は別のプロファイル(SYSTEMならsystemprofile配下)を指し、あなたが保存した設定ファイルはそこに存在しません。タスクの実行アカウントを確認し、共有すべきデータはProgramData配下に置くのが恒久対策です。
ProtectedData.Protectで保存したパスワードが、サービス化したら復号できなくなりました。
CurrentUserスコープのDPAPIは、暗号化したユーザーのマスターキーに依存します。サービスの実行アカウントが違えばマスターキーも別物なので、復号はCryptographicExceptionで失敗します。サービスと対話ユーザーの両方から読む秘密はLocalMachineスコープ+ファイルACLで設計し直すか、暗号化した本人のアカウントでサービスを実行してください。
SYSTEMで動くプロセスからHKCUを読むとどうなりますか?
LocalSystemのプロセスではHKEY_CURRENT_USERが既定ユーザー(HKEY_USERS\.DEFAULT)に関連付けられるため、対話ユーザーがHKCUに書いた値は見えません。マシン全体で共有する設定はHKLMに置き、どうしてもユーザーの設定を読む必要がある場合は、そのユーザーを偽装したうえでRegOpenCurrentUserを使います。
ログイン済みのChrome/Edgeのプロファイルを、CIマシンにコピーして使えますか?
基本的に使えません。ChromeやEdgeなどChromium系ブラウザーのプロファイルはユーザーの%LOCALAPPDATA%配下にあり、Cookieや保存パスワードの暗号鍵はそのユーザーのDPAPIで保護されています。別ユーザー・別マシンにフォルダーをコピーしても鍵が合わず復号できません(Firefoxなど独自のプロファイル保護を持つブラウザーは事情が異なります)。自動化ではテスト用アカウントのログイン手順をコード化するか、自動化ツールのストレージステート機構を使ってください。
cmdkeyで保存した資格情報が、タスクスケジューラ実行時に使われません。
資格情報マネージャーの金庫はユーザーごとに別で、保存したのは対話ログオンしたあなたの金庫だからです。加えて「パスワードを保存しない」(S4U)構成のタスクはネットワーク資格情報を持たずに動くため、S4Uのままでは金庫に資格情報を投入しても使えません。まず「パスワードを保存する」構成やサービスアカウントへの切り替えを検討し、そのうえで必要なら実行アカウント自身の金庫に資格情報を投入してください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る