Solo IT Hub

バッチでパスワードを変更したらOutlookとTeamsの認証が消えた — net user(リセット)とNetUserChangePassword(変更)の違い

リセットと変更の違いで資格情報マネージャーが保持されるかが決まる net userによる「リセット」では旧パスワードを経由しないため資格情報マネージャーの中身を再暗号化できず失われる。NetUserChangePasswordによる「変更」では旧パスワードと新パスワードの両方を使うため資格情報が引き継がれる、という対比図。 同じ「パスワード変更」でも、資格情報が残るかは方式しだい net user(リセット) 新パスワードだけで上書き 旧PW 使われない 新PW 資格情報マネージャー 旧PWで暗号化 → 復号できず失効 Outlook・Teamsが再入力を要求 利点:旧PW不要 → 他人のアカウントも 変更できる(管理者権限) = 強力だが資格情報を巻き込む NetUserChangePassword(変更) 旧PWと新PWの両方を使う 旧PW 新PW 資格情報マネージャー 旧PWで復号 → 新PWで再暗号化 → 保持 Outlook・Teamsはそのまま使える 制約:旧PWが必須 → 自分が知っている アカウントしか変更できない = 安全だが旧PWの管理が前提 どちらもWindowsの同じSAMにパスワードを書き込むが、資格情報マネージャーの扱いだけが決定的に違う
// シリーズ:社内パスワード一斉更新プロジェクト(全4回)
  1. 第1回:バッチでパスワードを変更したらOutlookとTeamsの認証が消えた(この記事)
  2. 第2回:非エンジニアにバッチを配って一斉実行してもらうときの落とし穴
  3. 第3回:Windows・Microsoft 365・Google・NASで共通して使えるパスワードの文字
  4. 第4回:LanDiskのCSV一括登録で既存ユーザーのパスワードを更新する

背景:なぜバッチでパスワードを変更したのか

Active Directory を導入していない中小企業で、各PCのローカルアカウントのパスワードを一斉に更新するプロジェクトを担当した。
台数は数十台、利用者のITリテラシーにはかなりばらつきがあり、「画面の指示通りに手入力してください」では確実に入力ミスが出る顔ぶれだ。

そこで、利用者ごとに新パスワードを埋め込んだバッチファイル(.bat)を配り、ダブルクリックで実行してもらう方式にした。バッチの中身は、管理者権限への自動昇格、対象PC・対象IDの一致チェック、そしてパスワードの変更コマンドである。

パスワード変更の中核には、最初は素直に net user コマンドを選んだ。実行ポリシーに悩まされず、ダブルクリックで確実に動く。バッチ向きの定番だ。テストも通り、一斉配布の直前まで来ていた。

症状:サインインし直したら認証が吹き飛んだ

配布前にフライング実行した利用者から、こんな報告が来た。

「パソコンから一度サインアウトして、新しいパスワードでサインインしたら、Teamsからサインアウトしていた。Outlookを起動したらメールアカウントのパスワードも消えていて、再入力を求められた」

パスワードの変更そのものは成功している。新パスワードでログインもできる。なのに、Windowsログインとは別物のはずの Teams や Outlook の認証情報まで一緒に吹き飛んだ。
これは手順書を直せば済む話ではない。利用者の多くはメールアカウントのパスワードを自分では把握していない(初期セットアップ時に管理側が設定したまま)。Outlookが再入力を求めた時点で、本人にはもう打つ手がない。

この記事の結論(先に書きます)

原因は、Windowsにおける「パスワードのリセット」と「パスワードの変更」という、似て非なる2つの操作の違いにある。

つまり、バッチのパスワード変更コマンドを「リセット」から「変更」に差し替えれば、OutlookやTeamsを巻き込まずに済む。ただし「変更」方式には旧パスワードが必須という条件が付く。以下で、その仕組みと実装、そして実装中にハマったエラーを順に説明する。

「リセット」と「変更」は何が違うのか

どちらも最終的にはWindowsの同じSAM(アカウント情報データベース)に新しいパスワードを書き込む。利用者から見れば結果は同じ「パスワードが変わった」だ。決定的に違うのは資格情報マネージャーの扱いだけである。

観点リセット(net user変更(NetUserChangePassword
旧パスワード不要必須
資格情報マネージャー失効する(再暗号化できない)保持される(再暗号化される)
Outlook / Teams 等への影響再サインイン・再入力が必要影響なし
他人のアカウントを変更できる(管理者権限があれば)できない(旧PWを知る本人のみ)
主な用途パスワードを忘れた人の救済、不明なアカウントの強制変更本人が使うアカウントの安全な更新

なぜリセットすると資格情報が消えるのか

Windowsの資格情報マネージャー(やブラウザ・各種アプリが保存するパスワード)は、DPAPI(Data Protection API)という仕組みで暗号化されている。この暗号鍵は、最終的にそのユーザーのログインパスワードから導出される。

本人が「設定 → パスワードの変更」から旧パスワードを入力して変える場合、Windowsは旧パスワードで鍵を復元し、保存済みデータを新パスワード由来の鍵で暗号化し直す。だから何も失われない。

ところが管理者によるリセット(net user)は旧パスワードを通らない。Windowsは「旧パスワードで暗号化された資格情報を、復号する手立てがない」状態になる。安全側に倒して、それらを使えなくする(失効させる)。これがOutlookやTeamsの認証が消える理由だ。

補足:今回変更したのはWindowsのローカルログインパスワードだけで、Microsoft 365 側のパスワードは何も変わっていない。利用者は従来のメールパスワードをもう一度入力すれば元通り使える。問題は「そのパスワードを本人が知らない」という運用面にあった。技術的には正常な挙動である。

「危険なコマンド」の利点と、「安全なコマンド」の制約

ここで強調したいのは、リセットが一方的に悪いわけではないということだ。それぞれに用途がある。

リセット(net user)の利点:旧PWを知らなくても変更できる

リセットは旧パスワードを要求しない。これは裏を返すと、管理者権限さえあれば、自分が旧パスワードを知らないアカウントのパスワードでも変更できるということだ。

REM 管理者権限のコマンドプロンプトで1行打つだけ
net user 対象のユーザーID 新しいパスワード

退職者の残骸アカウントや、誰も現在のパスワードを覚えていない共有アカウントなど、今のパスワードがわからないアカウントに、新しいパスワードを強制的に設定し直せる。こうすると、古いパスワードを知っている人はもうログインできなくなり、管理者側がそのアカウントを管理下に取り戻せる。「旧パスワードで誰でも入れてしまう状態」を断ち切りたいセキュリティ棚卸しの場面では、この強引さがそのまま武器になる。

同時にこれはリスクでもある。管理者権限を持つ古いアカウント(残骸アカウントなど)の旧パスワードを知っている人がいれば、その人はコマンド1行で他人の現役アカウントのパスワードを勝手に書き換えられる。「使用中アカウントのパスワードだけ強化しても、管理者権限の残骸アカウントを放置していたら片手落ち」というのは、こういう理屈だ。

変更(NetUserChangePassword)の制約:旧PWが必須

一方の「変更」方式は安全だが、旧パスワードを渡せないと動かない。今回のように新パスワードしかバッチに埋めていない設計のままでは使えない。
そこで管理用のExcelに「旧パスワード」列を新設し、現行パスワードを記入したうえで、バッチ生成時に旧PWと新PWの両方を埋め込む形に作り替えた。旧パスワードを把握・管理できることが、この方式を選べる前提条件になる。

実装:バッチから NetUserChangePassword を呼ぶ

「変更」をバッチから実行する方法はいくつかあるが、最終的に安定したのは Win32 API の NetUserChangePassword を PowerShell の P/Invoke 経由で呼ぶ形だった。バッチ内のパスワード変更部分は、概ね次のような1行になる(読みやすさのため整形)。

REM 環境変数に旧PW・新PWをセットしてから呼ぶ
set "TARGET_USER=<ユーザーID>"
set "OLD_PASSWORD=<旧パスワード>"
set "NEW_PASSWORD=<新パスワード>"

%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -Command ^
  "$sig='[DllImport(\"netapi32.dll\",CharSet=CharSet.Unicode)]public static extern int NetUserChangePassword(string d,string u,string o,string n);'; ^
   $t=Add-Type -MemberDefinition $sig -Name N -Namespace W -PassThru; ^
   exit $t::NetUserChangePassword($env:COMPUTERNAME,$env:TARGET_USER,$env:OLD_PASSWORD,$env:NEW_PASSWORD)"

if %errorlevel% equ 0 ( echo パスワード変更:成功 ) else ( echo パスワード変更:失敗 )

ポイントは、パスワードを set "VAR=..." で環境変数に格納し、子プロセスのPowerShellへ $env: で渡していること。こうするとバッチ側(cmd.exe)が # などの記号を誤解釈せずに済む。NetUserChangePassword の戻り値は 0 が成功、0以外がエラーコードで、これをそのまま exit で返してバッチ側の %errorlevel% で判定する。

ここまでにハマった3つのエラー(同じ轍を踏まないために)

「変更」方式に切り替えるまで、3回つまずいた。いずれもエラーが画面に出ず、静かに失敗するのが厄介だった。

① ADSI の ChangePassword が「アクセスが拒否されました」

最初は手軽に見えた ADSI(WinNTプロバイダ)方式を試した。

$u = [ADSI]"WinNT://./<ユーザーID>"
$u.ChangePassword("旧パスワード", "新パスワード")

ところが管理者権限でも非管理者でも「アクセスが拒否されました」で失敗した。リモートデスクトップ接続中だったことや、ADSI WinNTプロバイダ側の制約が絡んでいた可能性が高い。深追いせず、Win32 APIの NetUserChangePassword に切り替えた。

② ドメインに $null を渡すと 2221(ユーザーが見つからない)

NetUserChangePassword の第1引数(ドメイン/サーバー名)に $null を渡したところ、net user では確かに存在するアカウントなのに 2221(ユーザーが見つからない)が返った。
ローカルアカウントは、第1引数に自分のコンピューター名($env:COMPUTERNAME)を明示すると解決した。$null だとローカルSAMをうまく検索できないケースがある。

③ DllImport の CharSet 構文ミスで「静かに」失敗

これが一番たちが悪かった。Add-Type に渡すC#コードの中で、CharSet列挙体をPowerShellの構文で書いてしまっていた

// 誤り(PowerShell構文がC#に混入)
[DllImport("netapi32.dll",CharSet=[System.Runtime.InteropServices.CharSet]::Unicode)]

// 正しいC#構文
[DllImport("netapi32.dll",CharSet=CharSet.Unicode)]

C#のコンパイルが失敗するのだが、そのコンパイルエラーが >nul 2>&1 で握り潰されていたため、画面上は何事もなく進み、結果だけが「失敗」になる。原因が見えないまま再実行を繰り返す羽目になった。

教訓:デバッグ中は >nul 2>&1 を一時的に外してエラーを表示させること。 出力抑制は本番では親切でも、原因調査では完全に足を引っ張る。「成功に見えるのに結果が失敗」という現象の裏には、抑制されたエラーが隠れていることが多い。

どちらを使うべきか — 判断の指針

最後に、両方式の使い分けを整理しておく。

「資格情報を引き継ぎたいか/引き継ぐ必要がないか」を最初に決めれば、選ぶコマンドは自ずと決まる。今回の一斉更新は前者だったので、遠回りしたが「変更」方式が正解だった。

まとめ

バッチでローカルアカウントのパスワードを変えたら、OutlookとTeamsの認証が消えた——。
犯人は net user が行う「リセット」だ。資格情報マネージャーは旧パスワードに紐づいて暗号化されているため、旧パスワードを経由しないリセットでは再暗号化できず、まとめて失効する。

旧パスワードと新パスワードの両方を使う「変更」方式(NetUserChangePassword)に切り替えれば、認証情報は引き継がれる。引き換えに旧パスワードの管理が必須になる。
似た名前の操作でも、「旧パスワードを通るか通らないか」が資格情報の生死を分ける。これを押さえておけば、パスワード運用の設計を一段安全にできる。