動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

PowerShell で退職者の AD アカウントを棚卸しして自動 disable する(情シス1人運用)

バイブス父さん
現役の業務SE
2026年7月14日15 min read
PowerShell で退職者の AD アカウントを棚卸しして自動 disable する(情シス1人運用)

監査の人に、こう聞かれたことないですか??

「退職した人のアカウント、まだ有効なまま残ってませんよね?」

情シス1人で AD を回してると、これがけっこう刺さる。

入社時のアカウント作成はやるんだけど、退職時の無効化って、依頼が来ないと後回しになりがちなんですよね。気づいたら数年前に辞めた人のアカウントがまだ有効、みたいな。

どうも、ヒロポンです。

今回は PowerShell で Active Directory の退職者・休眠アカウントを棚卸しして無効化する手順を、情シス1人運用の業務SE向けにまとめます。手作業の棚卸しをやめて、洗い出しから Disable まで仕組みに寄せる話です。

先に一番大事なことだけ。いきなり Disable-ADAccount を全員に流すのは絶対にやめた方がいい。現役社員を止めて電話が鳴り止まなくなります。理由は後で詳しく書きます。

💡 AD アカウントを作る側(入社時のバルク登録)は 業務SE が初めて触る AD バルクユーザー作成 にまとめてます。今回はその、止める側の話。作成と棚卸しをセットで仕組みにすると、AD 運用がだいぶ楽になるんで、別タブで開いて後で読んでくださいな。

結論: いきなり Disable するな。洗い出し → 確認 → 猶予 → Disable の4段で

先に答えから。安全に回すなら、この順番を崩さない。

  1. 洗い出し: Search-ADAccount -AccountInactive で休眠アカウントを CSV に出す
  2. 人間の確認: CSV を開いて「本当に止めていいか」を目視で列にマークする
  3. 猶予期間: すぐ消さず、無効化予定として数日〜数週間置く
  4. Disable: まず -WhatIf で空撃ち → 結果を確認してから本番

このうち 2 の人間確認を飛ばすと事故ります

自動化したい気持ちは分かる。でも AD は「止めた瞬間にその人が仕事できなくなる」システムなんで、最後の引き金だけは人間が引く設計にしとくのが無難です。

なぜ「最終ログイン日」で即 disable すると事故るのか

一番やりがちなのが、LastLogonDate(最終ログイン日)を見て「N 日ログインしてないから休眠」と即断すること。

これ、最終ログイン日がリアルタイムじゃないんで危ないんです。

AD の lastLogonTimestampLastLogonDate の元になる属性)は、ログインのたびに更新されるわけじゃない。複製トラフィックを減らすために、既定で 9〜14 日に一度しか更新されませんmsDS-LogonTimeSyncInterval で制御)。

つまり昨日ログインした人でも、LastLogonDate は最大2週間くらい古い日付のまま。これが普通に起きる。地味に罠すぎませんか??

ここで閾値を「14 日ログインなし」みたいに短く取ると、先週使ったばかりの現役社員が『休眠』に混ざる。それを確認せず Disable すると、現役を止めて朝から電話地獄です。

だから閾値は 90 日みたいに余裕を持たせる。境目のアカウントは後述の lastLogon で個別確認する。

最短対処: コピペで動く3段スクリプト

では実際のスクリプト。洗い出し → 確認 → Disable の流れをそのまま貼れる形にします。

全体の流れはこんな感じ。

退職者 AD アカウント棚卸しの安全フロー: Search-ADAccount で洗い出し、CSV export、人間が目視確認、猶予期間、-WhatIf 空撃ち、本番 Disable の順に進む

Step 1: 休眠アカウントを CSV に洗い出す

まず、90 日ログインのない有効ユーザーを CSV に吐き出します。この段階では一切変更しません。ただの調査です。

Import-Module ActiveDirectory

$inactiveDays = 90

# -AccountInactive は lastLogonTimestamp を基準に休眠アカウントを探す
# -TimeSpan で「現在から N 日」を指定する
Search-ADAccount -AccountInactive -UsersOnly -TimeSpan (New-TimeSpan -Days $inactiveDays) |
    Where-Object { $_.Enabled } |
    Get-ADUser -Properties LastLogonDate, whenCreated, Description |
    Select-Object SamAccountName, Name, LastLogonDate, whenCreated, Description, DistinguishedName |
    Sort-Object LastLogonDate |
    Export-Csv -Path "C:\audit\inactive_users.csv" -NoTypeInformation -Encoding UTF8

-AccountInactive は Active Directory モジュールが lastLogonTimestamp を見て休眠判定してくれる switch。-TimeSpan (New-TimeSpan -Days 90) で「90 日ログインなし」を指定します。

文字列で -TimeSpan "90.00:00:00" と書いてもいいけど、New-TimeSpan の方が読み間違えなくていい感じです。

-Encoding UTF8 を付けないと Excel で開いた時に日本語が化けるんで、忘れないように。あと出力先の C:\audit\ フォルダは事前に作っておくこと。無いと Export-Csv がそこで止まります。

Step 2: 人間が CSV を確認して「止めていい」列を付ける

出た CSV を Excel で開いて、Disable みたいな列を1つ追加。退職確定の人だけ yes を手で入れる。ここが人間のゲートです。

判断に迷う境目のアカウント(最終ログインが閾値ギリギリのやつ)は、lastLogon(非複製・DC ごとに持ってる正確な値)を各 DC に問い合わせて確認します。

# 特定ユーザーの「本当の」最終ログインを全 DC から拾って最大値を取る
$sam = "t.yamada"
Get-ADDomainController -Filter * | ForEach-Object {
    $u = Get-ADUser $sam -Server $_.HostName -Properties lastLogon
    [PSCustomObject]@{
        DC        = $_.HostName
        LastLogon = [DateTime]::FromFileTime($u.lastLogon)
    }
} | Sort-Object LastLogon -Descending | Select-Object -First 1

lastLogon は複製されない代わりに、そのDCでの正確なログイン時刻を持ってる。だから全 DC を回して一番新しい値を取れば、複製遅延に惑わされず「本当に使ってないか」が分かります。

Step 3: -WhatIf で空撃ち → 本番 Disable

確認済み CSV を読んで、yes の人だけ無効化します。必ず最初は -WhatIf を付ける

# ① まず -WhatIf で「何が無効化されるか」だけ表示(実際には変更しない)
Import-Csv "C:\audit\inactive_users_checked.csv" |
    Where-Object { $_.Disable -eq "yes" } |
    ForEach-Object {
        Disable-ADAccount -Identity $_.SamAccountName -WhatIf
    }

出力を目で見て、対象が想定通りだと確認できたら、-WhatIf を外して本番実行。ついでに Description に無効化した理由と日付を刻んでおくと、後で監査に聞かれた時にいい感じに一発で答えられます。

# ② 確認できたら本番。無効化 + 理由を Description にスタンプ
Import-Csv "C:\audit\inactive_users_checked.csv" |
    Where-Object { $_.Disable -eq "yes" } |
    ForEach-Object {
        Disable-ADAccount -Identity $_.SamAccountName
        Set-ADUser -Identity $_.SamAccountName `
            -Description "退職棚卸により無効化 $(Get-Date -Format 'yyyy-MM-dd')"
    }

これで「いつ・何のアカウントを・なぜ止めたか」が AD 上に残る。棚卸しの証跡がそのまま残るんで、監査対応がめちゃくちゃ楽になります!!

ハマりポイント: 知らないと現役社員を止める

ハマり1: lastLogonTimestamp は最大14日ずれる(誤 disable の主因)

これがこの記事で一番言いたいやつ。さっきの「なぜ事故るのか」の実害バージョンです。

LastLogonDate を見て「30 日ログインなし」で切ると、実際には1週間前にログインした人が混ざる。理由は複製遅延で、値が 9〜14 日古いから。

俺が最初にこれをやった時、閾値を短めに取って CSV を出したら、明らかに現役の営業担当が数人リストに入ってて青ざめました。

あのまま Disable してたら、現役の営業を数人まとめて止めて、朝から問い合わせが殺到してたはずです。

対策は2つ。閾値を 90 日など余裕を持って取る。そして境目は Step 2 の lastLogon 全 DC 確認で裏を取る。この2段構えで誤検知はほぼ消えます!!

ハマり2: サービスアカウント / 共有アカウントを巻き込む

休眠判定に引っかかるのは、退職者だけじゃない。バッチ処理用のサービスアカウントや、対話ログインしない共有アカウントも「ログインしてない」扱いで出てきます。

これを止めると、夜間バッチが全部コケる。しかも原因が AD だと気づきにくいんで、復旧に時間がかかる。

ん?なんで急にバッチが落ちてるん??ってなって、犯人が棚卸しスクリプトだった、みたいな。

対策は、洗い出しの段階でサービスアカウントを除外すること。命名規則(svc- プレフィックスとか)や専用 OU で分けてるなら、そこを Where-Object で弾きます。

Search-ADAccount -AccountInactive -UsersOnly -TimeSpan (New-TimeSpan -Days 90) |
    Where-Object { $_.Enabled -and $_.SamAccountName -notlike "svc-*" } |
    Get-ADUser -Properties LastLogonDate |
    Select-Object SamAccountName, Name, LastLogonDate |
    Export-Csv "C:\audit\inactive_users.csv" -NoTypeInformation -Encoding UTF8

ハマり3: -WhatIf を付け忘れて全部即 Disable

Disable-ADAccount-WhatIf を付けないと、その場で本当に無効化します。取り消し(Enable-ADAccount)はできるけど、100 人分やっちゃってから戻すのは骨が折れる。

対策はシンプル。Disable 系のスクリプトは最初から -WhatIf を書いた状態で保存しておく。本番で外す時だけ意識的に消す。

「デフォルトは空撃ち」を癖にしとくと、事故りようがないです。

現場メモ: 棚卸しを仕組み化すると監査が楽になる

俺が情シス寄りの業務を持ってた頃、AD の退職者アカウントが数年分たまってて、監査で毎回「これ誰ですか」を手で調べてました。1回の棚卸しに半日。しんどい。

で、上の3段スクリプトを組んで、月1で CSV を出す運用に変えた。

そうすると、たまってた分が一気に片付くだけじゃない。「いつ・誰が・なぜ止めたか」が Description に残るようになる。

次の監査で「退職者のアカウント管理は?」と聞かれた時、CSV と AD の Description を見せるだけで話が終わる。

手作業の棚卸しって、やってる時は「作業」なんだけど、仕組みにすると「証跡」に変わるんですよね。ここが情シス1人運用だと効いてくる。

スクリプトそのものより、人間確認のゲートを残したまま自動化するっていう設計思想の方が大事かなと思ってます。

まとめ

退職者 AD アカウントの棚卸し、安全に回すポイントはこれだけ。

  • いきなり Disable しない。洗い出し → 人間確認 → 猶予 → Disable の順を守る
  • LastLogonDate は 9〜14 日ずれる。閾値は 90 日など余裕を持って、境目は lastLogon を全 DC で確認
  • サービスアカウント / 共有アカウントは洗い出し段階で除外
  • Disable-ADAccount は必ず -WhatIf で空撃ちしてから本番

手作業でやってた棚卸しをスクリプトに寄せると、退職者の取りこぼしが減る。それだけじゃなく、監査で聞かれた時にすぐ答えられる証跡が勝手に残る。

情シス1人で回してるなら、月1の CSV 洗い出しから始めるのがいい感じにハマると思います。

動作確認メモ: 今回のスクリプトは PowerShell の構文と、AD 以外のロジック(TimeSpan 計算・CSV 入出力・フィルタ)を検証済みです。Search-ADAccount / Disable-ADAccount などの AD cmdlet はドメイン環境が必要なため、本番前に必ずテスト OU + -WhatIf 空撃ちで挙動を確認してください。

よくある質問

Search-ADAccount と Get-ADUser -Filter、どっちで洗い出すべき?

まずは Search-ADAccount -AccountInactive でいいです。lastLogonTimestamp を見た休眠判定を AD 側がやってくれるので楽。細かい条件(特定 OU だけ、特定属性で絞る等)を足したくなったら Get-ADUser -Filter に移る、くらいの順番でいけます。

無効化じゃなくて削除しちゃダメ?

いきなり削除はおすすめしません。Disable なら間違えても Enable-ADAccount で戻せるけど、削除するとグループ所属や属性が消えて復旧が大変です。無効化 → 数ヶ月猶予 → それでも問題なければ削除、の段階を踏むのが安全。監査的にも「消した記録」より「無効化した記録」の方が追いやすいです。

完全自動化(人間確認なし)にはできないの?

技術的にはできますが、おすすめしません。lastLogonTimestamp の遅延やサービスアカウントの混入があるので、最後の Disable だけは人間のゲートを残す設計が安全です。自動化するのは「洗い出しと CSV 出力」まで。引き金は人が引く、が事故らないラインです。

-WhatIf の代わりに -Confirm でもいい?

用途が違います。-WhatIf は「何が起きるか一覧で見る」空撃ち、-Confirm は「1件ずつ本当にやるか聞く」対話確認です。大量のアカウントを一括で確認するなら、まず -WhatIf で全体を見て、個別に慎重にやりたい時だけ -Confirm を使う、と使い分けます。

次に読むべき記事

以上!

同じ「退職者アカウント放置してた」経験ある情シス、けっこう多いと思うんで、どんどんシェア待ってるぜ!!

執筆者

バイブス父さん — 業務 SE 7 年(正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SIer 正社員から独立し、複数のフリーランスエージェント経由で現場を渡り歩いた経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524(日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について