手元では完璧に動く.exeをタスクスケジューラに登録したら、「設定ファイルが見つからない」と言って落ちた。保存済みパスワードを使うツールをサービス化したら、復号のところでCryptographicExceptionが飛んだ。ログイン済みのブラウザを操作する自動化スクリプトが、CIエージェント上では毎回ログイン画面から始まる──どれもWindowsの開発現場で繰り返し起きている事故で、原因はすべて同じ場所にあります。
私たちは無意識に「同じPCで動かしているのだから、同じ環境のはずだ」と考えます。しかしWindowsでは、この直感は成り立ちません。この記事が答える疑問は、ただ1つです。
コードを1文字も変えていないのに、実行のされ方を変えただけでアプリが壊れるのはなぜか。
対象読者は、業務アプリのサービス化・タスク化・CI化を行う開発者と、その種の「手元では動くのに」を調査する運用担当者です。前提環境はWindows 10/11で、検証コードはPowerShellで示します。難易度は中級です。
1. まず結論
Windowsの実行環境の単位はPCではなく、「誰として動いているか」──アクセストークンとSID──である。同じPCの中には、ユーザーの数だけ互いに見えない世界が併存しており、AppData・HKCU・DPAPIの鍵・ブラウザプロファイル・資格情報はすべてその世界ごとに別物になる。
共有されているのは、HKLMやProgramDataといったマシン全体の領域だけです。実行のされ方(ダブルクリック、タスク、サービス、IIS)が変わると実行ユーザーが変わり、実行ユーザーが変わると、この5つがまとめて入れ替わります。
flowchart TB
accTitle: 同じPCの中の2つの世界
accDescr: 同じPCはHKLMとProgramDataだけを共有し、あなたのSIDの世界と別SIDの世界がそれぞれ独自のAppData・レジストリハイブ・DPAPI鍵を持ち、ユーザー境界で互いに見えない
pc["同じPC"] --> shared["共有:HKLM・ProgramData"]
pc --> wa["あなたのSIDの世界"]
pc --> wb["別SIDの世界(SYSTEMなど)"]
wa --> ra["AppData・HKCU・DPAPI鍵"]
wb --> rb["別のAppData・別ハイブ・別の鍵"]
ra -.-|"ユーザー境界:互いに見えない"| rb
図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で対話シェルを開いて確認できる。
flowchart LR
accTitle: Windowsのユーザー境界の知識マップ
accDescr: SIDに紐づくユーザープロファイルがAppData・NTUSER.DAT・DPAPIマスターキーを抱え、ユーザーの証明書ストアがHKCUに保存され、資格情報マネージャーとChromium系ブラウザーのプロファイルがDPAPIに依存し、DPAPIのスコープ選択が復号失敗の有無を分け、タスクスケジューラとIISアプリプールがそれぞれログオン種別とloadUserProfileで構成され、S4Uログオンの制約・パスワードリセットの影響・ProgramDataや証明書ストアの推奨・ネットワークドライブとログオンセッション・whoamiやSysinternalsによる確認手段、LocalLow・UsrClass.dat・偽装・LocalService/LocalSystemアカウントとの関係までを含む関係図
security_identifier["SID(セキュリティ識別子)"]
windows_user_profile["ユーザープロファイル"]
dpapi["DPAPI"]
credential_manager["資格情報マネージャー(cmdkey)"]
ntuser_dat["NTUSER.DAT"]
hkcu["HKEY_CURRENT_USER(HKCU)"]
windows_service["Windowsサービス"]
local_appdata["%LOCALAPPDATA%"]
roaming_appdata["%APPDATA%(Roaming)"]
dpapi_master_key["DPAPIマスターキー"]
dpapi_currentuser_scope["DataProtectionScope.CurrentUser"]
dpapi_decryption_failure["DPAPI復号失敗"]
dpapi_localmachine_scope["DataProtectionScope.LocalMachine"]
currentuser_store["ユーザーの証明書ストア"]
chromium_browser_profile["Chromium系ブラウザーのプロファイル"]
task_scheduler["タスクスケジューラの無人実行"]
task_logon_type["ログオン種別(LogonType)"]
iis_apppool["IISアプリケーションプール"]
iis_load_user_profile["loadUserProfile設定"]
s4u_logon["S4Uログオン"]
efs_encryption["NTFS暗号化(EFS)ファイル"]
admin_password_reset["管理者によるパスワードのリセット"]
aspnet_core_data_protection["ASP.NET Core Data Protection"]
programdata_storage["%ProgramData%配下の保存先"]
localmachine_store["コンピューターの証明書ストア"]
mapped_network_drive["ネットワークドライブ(マップされたドライブ文字)"]
logon_session["ログオンセッション"]
uac_elevation["UAC昇格(管理者として実行)"]
whoami["whoamiコマンド"]
process_explorer["Process Explorer"]
procmon["Process Monitor(procmon.exe)"]
appdata_locallow["AppData\LocalLow"]
hkcu_classes["HKEY_CURRENT_USER\Software\Classes"]
impersonation["偽装(impersonation)"]
localservice_account["LocalServiceアカウント"]
localsystem_account["LocalSystemアカウント"]
psexec["PsExec"]
windows_user_profile -->|"前提とする"| security_identifier
ntuser_dat -->|"に保存される"| windows_user_profile
hkcu -.->|"に保存される"| ntuser_dat
hkcu -->|"用いるのは非推奨"| windows_service
local_appdata -->|"に保存される"| windows_user_profile
roaming_appdata -.->|"に保存される"| windows_user_profile
dpapi -->|"利用する"| dpapi_master_key
dpapi_master_key -.->|"に保存される"| windows_user_profile
dpapi_currentuser_scope -.->|"原因になり得る"| dpapi_decryption_failure
dpapi_localmachine_scope -.->|"軽減する"| dpapi_decryption_failure
credential_manager -->|"利用する"| dpapi
currentuser_store -->|"に保存される"| hkcu
chromium_browser_profile -.->|"に保存される"| local_appdata
chromium_browser_profile -->|"利用する"| dpapi
task_scheduler -->|"で構成できる"| task_logon_type
iis_apppool -->|"で構成できる"| iis_load_user_profile
s4u_logon -->|"両立しない"| efs_encryption
s4u_logon -->|"両立しない"| credential_manager
admin_password_reset -.->|"原因になり得る"| dpapi_decryption_failure
aspnet_core_data_protection -.->|"利用する"| dpapi
aspnet_core_data_protection -.->|"に保存される"| local_appdata
programdata_storage -->|"推奨される対応"| windows_service
localmachine_store -->|"推奨される対応"| windows_service
mapped_network_drive -->|"に保存される"| logon_session
mapped_network_drive -.->|"両立しない"| uac_elevation
security_identifier -->|"で確認できる"| whoami
security_identifier -->|"で確認できる"| process_explorer
windows_user_profile -->|"で確認できる"| procmon
appdata_locallow -->|"に保存される"| windows_user_profile
hkcu_classes -->|"に保存される"| local_appdata
impersonation -->|"推奨される対応"| windows_service
localservice_account -->|"利用する"| windows_user_profile
localsystem_account -->|"で確認できる"| 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つまり、別ユーザーのプロファイルは「まっさらな初期状態から始まる別物」です。
flowchart TB
accTitle: 起動経路からプロファイルまでの流れ
accDescr: ダブルクリック・タスクスケジューラ・サービスやIISのどの経路で起動しても、プロセスはアクセストークンのSIDを持ち、そのSIDに紐づくユーザープロファイル一式が実行環境になる
e1["ダブルクリック"] --> tok["プロセスのトークン(SID)"]
e2["タスクスケジューラ"] --> tok
e3["サービス・IIS"] --> tok
tok --> prof["SIDに紐づくプロファイル一式"]
prof -.-> note["別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配下になります。パスの解決自体は成功し、ファイルだけが「無い」。エラーメッセージからは原因が見えにくい、嫌らしい壊れ方をします。
flowchart TB
accTitle: %APPDATA%の解決はユーザーで変わる
accDescr: 同じコードが%APPDATA%を開いても、実行ユーザーがあなたならC:\Users配下、SYSTEMならsystemprofile配下に解決され、後者にはあなたの設定ファイルが存在しない
code["同じコード:%APPDATA%を開く"] --> q{"実行ユーザーは?"}
q -->|"あなた"| a["C:\Users\you\AppData\Roaming"]
q -->|"SYSTEM"| b["systemprofile配下のAppData"]
b -.-> miss["置いたはずのファイルが無い"]
図3: 環境変数は嘘をつかないが、解決先はトークンで変わる。
環境変数そのものにも同じ境界があります。システム環境変数はマシン共有ですが、ユーザー環境変数(PATHへの追記など)はユーザーごとです。「自分のターミナルでは通るコマンドが、サービスからは見つからない」の一因はここにあります。
対策は、パスを環境変数任せにせず「誰が読むデータか」で置き場所を決めることです。そのユーザーだけの設定ならAppData、マシン上の全ユーザー・サービスで共有するデータなら%ProgramData%配下に置き、ACLを設計します。データの置き場所の選び方は「Windowsアプリのローカルデータはどこに置くべきか」で詳しく扱っています。
flowchart TB
accTitle: 保存先は読者で決める
accDescr: そのユーザーだけが読むデータはAppDataやHKCUに、全ユーザーやサービスと共有するデータはProgramDataやHKLMに置き、後者はACL設計を伴う
q{"そのデータを読むのは誰か"} -->|"そのユーザーだけ"| f1["AppData・HKCU"]
q -->|"全ユーザー・サービス"| f2["ProgramData・HKLM"]
f1 -.-> w1["将来サービス化するなら再考"]
f2 -.-> w2["書き込み権限と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
flowchart TB
accTitle: HKCUという別名の実体
accDescr: アプリがHKCUを開くと、あなたのプロセスではHKEY_USERS配下のあなたのSIDキーに、LocalSystemのプロセスでは既定ユーザーのキーに振り替えられ、インストーラーが書いた値が見えなくなる
app["アプリのコード:HKCUを開く"] --> alias["HKCUは実体への別名"]
alias -->|"あなたのプロセス"| ha["HKEY_USERS配下のあなたのSID"]
alias -->|"SYSTEMのプロセス"| hd["HKEY_USERS\.DEFAULT"]
hd -.-> gone["書いたはずの値が存在しない"]
図5: HKCUに書くコードは、実行ユーザーが変わると読み書き先が黙って変わるコードである。
もう一段深い罠が、プロファイルがロードされていない状態です。タスクスケジューラの実行構成には「ログオン種別」という軸があり、「パスワードを保存しない」を選んだタスクはS4Uというログオンで動きます。S4Uではパスワードが保存されないため、ネットワークリソースと暗号化ファイル(EFS)にアクセスできません。9同じユーザー名を指定していても、対話ログオンと同じ環境がそこに再現されるとは限らない──「実行ユーザーが同じでも、ログオンのされ方という軸がもう1本ある」ことは覚えておく価値があります。タスクスケジューラの実行アカウントとログオン種別の設計は「タスクスケジューラで業務バッチを安定運用する」で掘り下げています。
flowchart TB
accTitle: タスクのログオン種別という第2の軸
accDescr: タスクスケジューラの実行構成は対話トークン・パスワード保存・S4Uのログオン種別を持ち、S4Uではパスワードが保存されずネットワークとEFSにアクセスできない
task["タスクの実行構成"] --> lt{"ログオン種別"}
lt -->|"対話トークン"| it["ログオン中のセッションで実行"]
lt -->|"パスワード保存"| pw["非対話でも資格情報を使える"]
lt -->|"S4U(パスワード非保存)"| 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つまり──暗号はユーザーの鍵で守られています。
flowchart TB
accTitle: DPAPIの鍵の連鎖
accDescr: ランダム生成されたユーザーのマスターキーはログオン資格情報から派生した鍵で保護され、そのマスターキーがアプリの秘密を暗号化しており、別ユーザーのマスターキーでは同じ暗号文を復号できない
pwd["ログオン資格情報"] -->|"派生鍵で保護"| mk["あなたのマスターキー"]
mk -->|"暗号化"| sec["アプリの秘密(保存パスワード等)"]
mk2["別ユーザーのマスターキー"] -.->|"復号できない"| sec
mk -.-> loc["保存先はプロファイル配下"]
図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「鍵がどこに置かれる構成なのか」を、loadUserProfile・setProfileEnvironment・ホスト形態とセットで意識しておく必要があります。
flowchart TB
accTitle: DPAPIスコープの選び方
accDescr: 復号できるべき主体がそのユーザーだけならCurrentUserスコープ、同じPCの複数の実行主体ならLocalMachineスコープを選び、後者はファイルACLで読者を絞る
q{"誰が復号できるべきか"} -->|"そのユーザーだけ"| cu["CurrentUserスコープ"]
q -->|"同じPCの複数の実行主体"| lm["LocalMachineスコープ"]
cu -.-> r1["別ユーザー実行で復号失敗"]
lm -.-> r2["読者はファイル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プロファイルフォルダーを別ユーザー・別マシンへコピーしても、鍵が合わずに開けない構造です。
flowchart TB
accTitle: ブラウザのログイン状態を支える2つの境界
accDescr: ブラウザプロファイルはLOCALAPPDATA配下にあり境界1に属し、Cookieの暗号鍵はDPAPIで保護され境界3に属するため、別ユーザーにはプロファイルも鍵も引き継がれない
prof["ブラウザプロファイル"] --> loc["置き場所はLOCALAPPDATA配下"]
prof --> key["Cookie暗号鍵はDPAPIで保護"]
loc -.-> ci["CIの実行ユーザーには空の別プロファイル"]
key -.-> copy["フォルダーコピーでは持ち出せない"]
図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が手元で通るのは金庫のおかげ、同じコマンドがサービスから失敗するのは金庫が別だから、という構図が頻出します。
flowchart TB
accTitle: 資格情報の金庫はユーザーごと
accDescr: あなたの金庫にはgitの資格情報やファイルサーバーの保存資格情報やRDPのパスワードが入っているが、サービス実行ユーザーの金庫は資格情報を投入しない限り空であり、それが認証エラーの正体になる
you["あなたの金庫"] --> g["gitの資格情報"]
you --> n["ファイルサーバーの保存資格情報"]
you --> r["RDPの保存パスワード"]
svc["サービス実行ユーザーの金庫"] -.-> empty["未投入なら空=認証エラーの正体"]
図10: 「手元では認証が通る」は「あなたの金庫が使える」の略でしかない。
証明書ストアにも同じ軸があります。ストアにはCert:\CurrentUserとCert:\LocalMachineの2系統があり、クライアント証明書をCurrentUserにインポートすると、その秘密鍵はユーザー単位で保護されます。サービスから使う証明書はLocalMachineストアに入れ、秘密鍵のACLでサービスアカウントに読み取りを許可するのが定石です。19なお、ユーザーストアの実体はレジストリのHKCU\Software\Microsoft\SystemCertificates配下──つまり境界2の内側──にあります。20ストアの選び方と権限設計は「Windows証明書ストア実務ガイド ── ユーザーとコンピューター、どちらに入れるか」を参照してください。
最後に、ユーザー境界の変種としてUAC昇格があります。UACが有効な管理者ユーザーのログオンでは、権限を制限した標準トークンと完全な管理者トークンという、リンクされた2つのトークンが作られます。21ネットワークドライブの割り当てはログオンセッション単位なので、「エクスプローラーではZ:が見えるのに、管理者として実行したツールからはZ:が無い」が起きます。22さらに別の管理者アカウントの資格情報で昇格した場合、そのプロセスのHKCUや金庫は「その管理者の」ものになり、境界2と境界5を同時に踏みます。「同じPC」どころか「同じユーザー」ですら、常に同じ世界とは限らないわけです。
flowchart TB
accTitle: UAC昇格が生む同一ユーザー内の分裂
accDescr: UAC有効な管理者のログオンは標準トークンと昇格トークンの2つを作り、標準トークン側で割り当てたネットワークドライブが昇格トークンのプロセスからは見えない
logon["管理者ユーザーのログオン"] --> t1["標準トークン"]
logon --> t2["昇格トークン"]
t1 --> d1["ここで割り当てたZ:ドライブ"]
t2 -.->|"別のログオンセッション"| d2["昇格したツールから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のスコープを意図して選ぶ。そしてサービスが使う証明書・資格情報は、実行アカウント自身のストア・金庫へセットアップ手順として明示的に投入する──「たまたま自分の環境にあったものを使う」を設計から排除します。
調査側は、「誰として動いているか」の確認から始めるのが最短です。
whoami /allをコードの起動直後にログへ出す(タスク・サービスに仕込めるなら最強の一手)- Process Explorerでプロセスのトークン(ユーザー・セッション)を確認する
- Process Monitorで「実際にどのパス・どのレジストリキーを読んだか」を見る。
PATH NOT FOUNDの行に期待と違うプロファイルのパスが映っていたら、この記事のどれかを踏んでいる psexec -s -i cmdでSYSTEMの世界に入り、向こう側から同じ操作を再現してみる
flowchart TB
accTitle: ユーザー境界トラブルの調査手順
accDescr: whoamiで実行ユーザーを確認し、Process Explorerでトークンを見て、Process Monitorで実際に読んだパスとレジストリキーを特定し、必要ならpsexecで相手の世界から再現する
s1["whoami /allで実行ユーザーを確認"] --> s2["Process Explorerでトークンを確認"]
s2 --> s3["ProcMonで実際のパス・キーを特定"]
s3 --> s4["psexecで相手の世界から再現"]
s3 -.-> hint["期待と違うプロファイルパスが手がかり"]
図12: 調査は症状のログではなく「誰として動いたか」から入ると迷子にならない。
10. まとめ
この記事の要点です。
- Windowsの実行環境の単位はPCではなく、アクセストークンとSID──「誰として動いているか」です。
- AppDataとユーザー環境変数は実行ユーザーのプロファイルへ解決され(境界1)、HKCUは呼び出し元で実体が変わる別名です(境界2)。3
- DPAPIのCurrentUserスコープはユーザーのマスターキーに依存し、別ユーザーからは復号できません(境界3)。12ブラウザのログイン状態(境界4)と資格情報マネージャー(境界5)は、その応用です。
- タスクのログオン種別・IISのプロファイル読み込み・UAC昇格という「同じユーザーでも世界が変わる」第2の軸があります。91021
- レビューで問うべきはひとつ──「これは、どのユーザーとして動いても正しいコードか?」
「手元で動く」は「そのユーザー・そのプロファイル・その金庫で動く」の略でしかありません。実行形態が変わる瞬間(サービス化・タスク化・CI化)は、コードが1文字も変わっていなくても環境移行です。そう扱うだけで、この種の事故の大半は設計段階で消せます。
関連記事
- Windowsアプリの機密情報保存 - DPAPIで平文設定を避ける
- Windowsアプリのデータ保存先の選び方 ── SQLite / JSON / レジストリ / Access 判断表
- タスクスケジューラのタスクが実行されない・0x1で終わる ── 原因の切り分けと安全な運用設計
- Windowsサービスのアカウント選定 ── LocalSystem・仮想アカウント・gMSAの使い分け
- Windows証明書ストア実務ガイド ── ユーザーとコンピューター、どちらに入れるか
関連する相談領域
合同会社小村ソフトでは、業務アプリのサービス化・タスク化に伴う実行環境の設計、「手元では動くのに」型の不具合調査、Windowsアプリの秘密情報管理の設計を扱っています。
参考リンク
-
Microsoft Learn, About User Profiles. ユーザープロファイルが初回ログオン時に作られること、プロファイルがレジストリハイブNTUSER.DAT(ログオン時にロードされHKEY_CURRENT_USERにマップされる)とファイルシステム上のプロファイルフォルダー群から成ることについて。 ↩ ↩2
-
Microsoft Learn, Local accounts. SYSTEM(S-1-5-18)・NETWORK SERVICE(S-1-5-20)・LOCAL SERVICE(S-1-5-19)がOSとサービスの実行に使われる既定のローカルシステムアカウントであることについて。 ↩
-
Microsoft Learn, LocalSystem Account. LocalSystemのトークンがNT AUTHORITY\SYSTEMを含むこと、どのログオンユーザーアカウントとも関連付かないこと、そのためHKEY_CURRENT_USERが既定ユーザーに関連付けられ、他ユーザーのプロファイルへアクセスするにはそのユーザーを偽装する必要があることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, LocalService Account. LocalServiceアカウントがHKEY_USERSの下に自分のサブキーを持ち、HKEY_CURRENT_USERがLocalServiceアカウントに関連付けられることについて。NetworkServiceも同様(NetworkService Account)。 ↩
-
Microsoft Learn, Application Pool Identities. アプリケーションプールがプール固有のIDで動くこと、IISが既定ではWindowsユーザープロファイルを読み込まないこと、LoadUserProfile属性をtrueにするとプロファイルを読み込めることについて。 ↩
-
Microsoft Learn, KNOWNFOLDERID. FOLDERID_RoamingAppData(%APPDATA%)・FOLDERID_LocalAppData(%LOCALAPPDATA%)・FOLDERID_LocalAppDataLowがユーザーごとの既知フォルダーとして定義されていることについて。 ↩
-
Microsoft Learn, Error occurs during desktop setup and desktop location is unavailable. ユーザープロファイルがNTUSER.DATとUsrClass.datという2つのハイブファイルを持ち、UsrClass.datがAppData\Local\Microsoft\Windows配下に置かれることについて。 ↩
-
Microsoft Learn, Services and the Registry. サービスがHKEY_CURRENT_USERやHKEY_CLASSES_ROOTへアクセスすべきでないこと、ユーザーを偽装する場合はRegOpenCurrentUser関数を使うべきことについて。 ↩
-
Microsoft Learn, logonType Simple Type. タスクのログオン種別にS4U・Password・InteractiveTokenがあること、S4Uログオンではパスワードが保存されず、ネットワークにも暗号化ファイルにもアクセスできないことについて。 ↩ ↩2
-
Microsoft Learn, Process Model Settings for an Application Pool. アプリケーションプールのprocessModelにloadUserProfile属性とsetProfileEnvironment属性があり、ワーカープロセスがユーザープロファイルを読み込むかどうかを制御することについて。 ↩ ↩2
-
Microsoft Learn, CryptProtectData function. CryptProtectDataが通常はログオンしているユーザーに関連付けられたセッションキーでデータを保護し、同じユーザーでの復号を前提とすること、CRYPTPROTECT_LOCAL_MACHINEフラグでマシン単位の保護に切り替えられることについて。 ↩
-
Microsoft Learn, Windows Data Protection. DPAPIがランダムに生成したマスターキーを、ユーザーのパスワードから派生した鍵で暗号化して保護すること、マスターキーがユーザープロファイル配下に保存されること、パスワード変更時にマスターキーが保護し直されることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, ProtectedData Class. DataProtectionScope.CurrentUserが保護したユーザーのみに復号を許し、LocalMachineが同じマシン上の任意のプロセスからの復号を許すことについて。 ↩
-
Microsoft Learn, Data Protection key management and lifetime in ASP.NET Core. ユーザープロファイルが利用可能な場合、鍵が%LOCALAPPDATA%\ASP.NET\DataProtection-Keysに保存されWindowsではDPAPIで暗号化されること、IISホストでプロファイルが使えない場合はワーカープロセスアカウントにACLされたHKLMレジストリへフォールバックすること、どの条件にも当てはまらない場合は鍵がプロセス終了とともに失われ保護済みペイロードが復号できなくなること、setProfileEnvironment属性が関わることについて。 ↩
-
Chromium project, User Data Directory. WindowsにおけるChromeのUser Dataディレクトリの既定が%LOCALAPPDATA%\Google\Chrome\User Dataであり、プロファイル(履歴・ブックマーク・Cookie等)がその配下に置かれることについて。 ↩
-
Google Security Blog, Improving the security of Chrome cookies on Windows. ChromeがWindowsでCookie等の暗号化にDPAPIを使ってきたこと、App-Bound Encryptionにより鍵をSYSTEM権限のサービス経由で保護し、復号を要求したアプリのIDまで検証するようになったことについて。 ↩
-
Microsoft Learn, cmdkey. cmdkeyコマンドで保存済みのユーザー名とパスワード(資格情報)の一覧表示・作成・削除ができることについて。 ↩
-
Microsoft Learn, Cached and Stored Credentials Technical Overview. 資格情報マネージャーに保存された資格情報がディスク上に置かれDPAPIで保護されること、そのユーザーとして動くプログラムがこのストアの資格情報にアクセスできることについて。 ↩
-
Microsoft Learn, Local Machine and Current User Certificate Stores. 証明書ストアにローカルマシンストア(マシン全体)とカレントユーザーストア(ユーザーごと)の2種類があることについて。 ↩
-
Microsoft Learn, System Store Locations. CERT_SYSTEM_STORE_CURRENT_USERのシステムストアがレジストリのHKEY_CURRENT_USER\Software\Microsoft\SystemCertificates配下に置かれることについて。 ↩
-
Microsoft Learn, How User Account Control works. UAC有効時、管理者ユーザーのログオンで標準ユーザートークンと完全な管理者アクセストークンという2つのリンクされたトークンが作られることについて。 ↩ ↩2
-
Microsoft Learn, Mapped drives are not available from an elevated prompt. 標準トークンでログオンしたセッションで割り当てたネットワークドライブが、昇格したプロセスから利用できないこと、およびその背景としてリンクされた2つのログオンセッションが別々にドライブ割り当てを持つことについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
PowerShellでの資格情報の安全な扱い ── 平文パスワードをスクリプトから追放する
PowerShellスクリプトの平文パスワードを安全な保管へ移行する手順を整理します。SecureStringの実像と限界、Export-ClixmlによるDPAPI保存の仕組み、SecretManagement/SecretStoreの使いどころまで解説します。
レジストリの32bit/64bitリダイレクトと仮想化の落とし穴 ── Wow6432Nodeと「書いたはずの値がない」問題
32bitアプリのHKLM\Software書き込みがWow6432Nodeへリダイレクトされる仕組み、UAC仮想化でVirtualStoreに転送される条件、COM登録がbitnessで分かれる実害、RegistryViewやreg.exe /reg:64での正しい読み書...
Power AutomateとPowerShell+タスクスケジューラの使い分け ── 自動化の道具を混ぜずに適材適所でつなぐ
PowerShell+タスクスケジューラの夜間バッチとPower Automateフローが社内に混在し始めた中小企業の情シス向けに、両者の得意分野の違い、どちらで作るかの判断表、SharePoint経由で疎結合につなぐ連携パターン、ライセンスと保守の注意点まで整理します。
PowerShellのエラー処理と再実行設計 ── try/catchが効かない罠からexit code・リトライの定石まで
PowerShellの終了エラーと非終了エラーの違い、try/catchが効かない罠と-ErrorAction Stopの定石、$LASTEXITCODEによる成否判定、exit code設計、指数バックオフのリトライまでを実務目線で整理します。
Windowsアプリのデータ保存先の選び方 ── SQLite / JSON / レジストリ / Access 判断表
Windowsデスクトップアプリのデータをどこに・何で保存するか。AppData/ProgramDataの使い分け、SQLite・JSONファイル・レジストリ・Access(.accdb)それぞれの得意分野と落とし穴を判断表つきで整理し、破損対策やビット数問題まで実務目線で...
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- エクスプローラーから起動すると動くアプリが、タスクスケジューラからだと設定ファイルを見つけられません。なぜですか?
- %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のままでは金庫に資格情報を投入しても使えません。まず「パスワードを保存する」構成やサービスアカウントへの切り替えを検討し、そのうえで必要なら実行アカウント自身の金庫に資格情報を投入してください。