iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
IT Operation

系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server系列 第 17 篇

Day 17|AD 帳號生命週期自動化:建立、停用與離職帳號該怎麼安全處理?

  • 分享至 

  • xImage
  •  

前面 Day 13~Day 16,我們一直都在做:
Query
Audit
Report

例如:
Get-ADUser
Get-ADComputer
Get-ADGroup
Get-ADGroupMember

這些操作大部分都屬於:
Read Only

查錯了,最多就是報表不正確。
但從 Day 17 開始,事情不一樣了。
今天第一次會碰到:
New-ADUser
Set-ADUser
Disable-ADAccount
Move-ADObject
Remove-ADGroupMember

這些 Cmdlet 會真正修改 Active Directory。
所以今天真正的主題不是:
「怎麼用 PowerShell 停用帳號?」

而是:
怎麼讓 AD Change Automation 有足夠的安全機制,不會因為 Script 寫錯就一次改壞一批帳號?

Read Automation 跟 Change Automation 差很多
前幾天:
Get-ADUser -Filter *

即使 Query 寫得不好,大多就是:
抓太多資料
報表不好看
跑比較久

但今天如果寫:
Disable-ADAccount

條件寫錯,結果可能直接影響使用者登入。
所以從今天開始,我希望所有修改 AD 的 Script 都先遵守這個流程:
Input
↓
Validation
↓
Preview
↓
Approval
↓
Action
↓
Verification
↓
Log

而不是:
CSV
↓
直接修改 AD

今天第一個重要指令:-WhatIf
PowerShell 很多會修改系統的 Cmdlet 都支援:
-WhatIf

例如:
Disable-ADAccount -Identity "user01"
-WhatIf

它不會真的停用帳號。
而是告訴你:
如果真的執行,我準備做什麼?

這對 Change Automation 非常重要。
先查,再改
假設今天 HR 通知:
user01
2026/09/30 離職

我不會第一行就:
Disable-ADAccount user01

而是先:
$User = Get-ADUser -Identity "user01"
-Properties `
Department,
Description,
Manager,
MemberOf,
LastLogonDate,
Enabled

然後確認:
$User |
Select-Object `
Name,
SamAccountName,
Enabled,
Department,
LastLogonDate,
DistinguishedName

可能:
Name : User One
SamAccountName : user01
Enabled : True
Department : Sales
LastLogonDate : 2026/09/24
DistinguishedName : CN=User One,OU=Sales,OU=Taipei,...

第一步先確認:
我即將修改的,真的是我要修改的那個帳號。

帳號生命週期其實不只有離職
企業 AD User 大概會經歷:
建立
↓
啟用
↓
屬性修改
↓
Department / Title / Manager 異動
↓
Group Membership 異動
↓
停用
↓
保留
↓
最後才可能刪除

所以真正的:
User Lifecycle Automation

至少可以分成:
Onboarding
Maintenance
Offboarding

今天會先把這三個階段串起來。
第一階段:Onboarding
新人進公司時,IT 可能收到:
姓名:王小明
帳號:ming.wang
Department:IT
Title:System Engineer
Email:ming.wang@contoso.com
OU:Taipei\Users

人工流程可能是:
開 ADUC
↓
New User
↓
填姓名
↓
填帳號
↓
設定密碼
↓
填 Department
↓
填 Email
↓
加入 Group
↓
確認

人少沒有問題。
但如果:
每週很多新人
多個辦公室
固定帳號格式
固定 Group

就很適合做 Automation。
先確認帳號不存在
假設要建立:
ming.wang

第一步不要直接:
New-ADUser

先查:
$ExistingUser = Get-ADUser -Filter 'SamAccountName -eq "ming.wang"'
-ErrorAction SilentlyContinue

然後:
if ($null -ne $ExistingUser) {

Write-Host "Account already exists."

}

因為:
Automation 最怕假設輸入永遠正確。

OU 也要先確認
例如:
$TargetOU =
"OU=Users,OU=Taipei,DC=contoso,DC=com"

先:
Get-ADOrganizationalUnit -Identity $TargetOU
-ErrorAction Stop

如果 OU 根本不存在,就應該:
停止
↓
修正設定

而不是讓 Script 繼續建立到錯誤的位置。
密碼不要寫在 Script 裡
不建議:
$Password = "Company123!"

尤其不要把:
Password
Admin Password
Service Account Password

放在:
.ps1
.csv
.txt

這種純文字檔案裡。
Lab 可以先:
$Password = Read-Host "Enter temporary password"
-AsSecureString

使用者輸入時不會直接顯示明文。
建立第一個 AD User
例如:
$Password = Read-Host "Enter temporary password"
-AsSecureString

然後先用:
New-ADUser -Name "Ming Wang"
-GivenName "Ming" -Surname "Wang"
-SamAccountName "ming.wang" -UserPrincipalName "ming.wang@contoso.com"
-Department "IT" -Title "System Engineer"
-Path "OU=Users,OU=Taipei,DC=contoso,DC=com" -AccountPassword $Password
-Enabled $true -ChangePasswordAtLogon $true
-WhatIf

注意最後:
-WhatIf

所以現在只是 Preview。
Preview 看起來正確,再執行
確認:
User Name 正確
SamAccountName 正確
UPN 正確
OU 正確
Department 正確

之後,按照公司變更流程核准,再進行正式執行。
這個概念很重要:
Script 寫完
≠
可以直接跑 Production

中間應該有:
Review
Test
Approval

建立完要 Verification
不要看到 Cmdlet 沒有跳 Error,就認為工作結束。
可以重新查:
Get-ADUser -Identity "ming.wang"
-Properties Department, Title, Enabled | Select-Object
Name,
SamAccountName,
Enabled,
Department,
Title,
DistinguishedName

這叫:
Post-Change Verification

也就是:
Change
↓
重新 Query
↓
確認結果

Maintenance:人員異動
User Lifecycle 不只有建立跟離職。
例如:
Sales
↓
IT

Department 改變。
可以:
Set-ADUser -Identity "user01"
-Department "IT" `
-WhatIf

職稱:
Set-ADUser -Identity "user01"
-Title "Senior System Engineer" `
-WhatIf

Description:
Set-ADUser -Identity "user01"
-Description "IT Department" `
-WhatIf

一樣先:
Query
Preview
Approval
Change
Verify

Group Membership 也屬於 Lifecycle
例如新人需要:
IT-Team
VPN-Users
FileServer-IT-RW

可以使用:
Add-ADGroupMember -Identity "IT-Team"
-Members "ming.wang" `
-WhatIf

VPN:
Add-ADGroupMember -Identity "VPN-Users"
-Members "ming.wang" `
-WhatIf

但權限 Group 跟一般 Attribute 不一樣。
因為:
加入 Group 本身可能就代表取得系統權限。

所以正式流程最好知道:
誰申請?
誰核准?
加入哪個 Group?
什麼時間?
誰執行?

接下來進入 Offboarding
離職流程通常比建立帳號風險更高。
假設:
user01

要離職。
第一件事情,我建議先做:
Before-State Snapshot
也就是在修改以前,把帳號現況保存下來。
保存帳號資訊
例如:
$User = Get-ADUser -Identity "user01"
-Properties `
Department,
Title,
Description,
LastLogonDate,
MemberOf,
Enabled

再保存:
$User |
Select-Object Name, SamAccountName, Enabled, Department, Title, Description, LastLogonDate, DistinguishedName | Export-Csv
-Path "C:\Temp\user01_Before.csv" -NoTypeInformation
-Encoding UTF8

Group Membership 也先存起來
Day 16 已經學過:
Get-ADPrincipalGroupMembership

所以:
$Groups =
Get-ADPrincipalGroupMembership `
-Identity "user01"

保存:
$Groups |
Select-Object Name, GroupCategory, GroupScope, DistinguishedName | Export-Csv
-Path "C:\Temp\user01_Groups_Before.csv" -NoTypeInformation
-Encoding UTF8

為什麼?
因為幾個月後可能有人問:
user01 離職以前原本在哪些 Group?

至少我們有 Change Evidence。
離職第一個真正的動作:Disable
Preview:
Disable-ADAccount -Identity "user01"
-WhatIf

確認後才執行正式變更。
接著馬上確認:
Get-ADUser -Identity "user01" | Select-Object
Name,
SamAccountName,
Enabled

預期:
Enabled : False

這就是:
Action
↓
Verification

更新 Description
停用後,也可以依公司規範留下資訊。
例如:
Disabled 2026-09-25 - Offboarding

Preview:
Set-ADUser -Identity "user01"
-Description "Disabled 2026-09-25 - Offboarding" `
-WhatIf

但 Description 的格式最好由公司統一。
不要每個 IT:
A 寫離職
B 寫Disabled
C 寫Delete later

最後沒辦法查。
Move 到 Disabled OU
假設公司建立:
OU=DisabledUsers

可以:
$User = Get-ADUser `
-Identity "user01"

Preview:
Move-ADObject -Identity $User.DistinguishedName
-TargetPath "OU=DisabledUsers,DC=contoso,DC=com" `
-WhatIf

這樣可以把:
Active Users

跟:
Disabled Users

分開管理。
也方便套用不同:
GPO
Delegation
Retention Policy
Administrative Process

不要一停用就直接 Remove-ADUser
不建議離職流程變成:
Disable
↓
Delete

比較合理通常是:
Disable
↓
Move Disabled OU
↓
保留一段時間
↓
確認不再需要
↓
依 Policy 清理

因為刪除跟停用最大的差別是:
Disable
→ Object 還在

Delete
→ Object 被刪除

保留帳號一段時間通常更有利於:
稽核
追蹤
還原
後續確認

實際保存多久則應由公司的帳號與資料保留政策決定。
Group Membership 可以全部清掉嗎?
這裡也不要太快。
網路上很容易看到做法:
取得所有 Group
↓
全部 Remove

但我不建議無條件這麼做。
因為可能存在:
稽核用途 Group
License Group
特殊 Workflow Group
歷史保留需求

比較安全的做法是:
先產生 Group Review List。

例如:
$Groups =
Get-ADPrincipalGroupMembership `
-Identity "user01"

查看:
$Groups |
Select-Object Name

再依公司的 Offboarding Policy 決定:
哪些 Group 立即移除?
哪些保留?
哪些需要 Owner 核准?

針對 Sensitive Group 移除
假設公司規定離職時必須立即移除:
VPN-Users
Server-Admins
FileServer-Admins

可以先定義:
$SensitiveGroups = @(
"VPN-Users"
"Server-Admins"
"FileServer-Admins"
)

然後:
foreach ($GroupName in $SensitiveGroups) {

$IsMember = Get-ADGroupMember `
    -Identity $GroupName `
    -ErrorAction SilentlyContinue |
Where-Object {
    $_.SamAccountName -eq "user01"
}


if ($null -ne $IsMember) {

    Remove-ADGroupMember `
        -Identity $GroupName `
        -Members "user01" `
        -WhatIf

}

}

這樣比:
把所有 Group Membership 全刪掉

更容易控制。
Direct Membership 跟 Nested Membership 又出現了
假設:
user01
↓
IT-Admins
↓
Server-Admins

user01 可能不是:
Server-Admins 的 Direct Member

所以:
Remove-ADGroupMember -Identity "Server-Admins"
-Members "user01"

並不能解決所有 Nested Permission 問題。
真正需要查:
user01
到底在哪個 Direct Group?
哪些 Direct Group 又帶來高權限?

這就是 Day 16 為什麼要先學:
Direct
Recursive

兩種 Group Membership。
Domain Users 不要當成一般 Group 全部清
一般 AD User 通常會有 Primary Group。
多數情況是:
Domain Users

它的處理機制跟一般 memberOf Membership 不完全相同。
所以不要寫一支:
遍歷所有群組
↓
無條件全部 Remove

的 Offboarding Script。
帳號生命週期 Automation 越重要,越應該:
Explicit > Magic

明確定義要移除哪些權限,比「我猜所有 Group 都可以刪」安全。
把 Offboarding 流程做成 Preview
可以先建立:
$UserName = "user01"

$DisabledOU =
"OU=DisabledUsers,DC=contoso,DC=com"

取得 User:
$User = Get-ADUser -Identity $UserName
-Properties `
Enabled,
Department,
Title,
Description,
LastLogonDate

顯示:
$User |
Select-Object `
Name,
SamAccountName,
Enabled,
Department,
Title,
LastLogonDate,
DistinguishedName

接著全部先:
Disable-ADAccount -Identity $UserName
-WhatIf

Description:
Set-ADUser -Identity $UserName
-Description "Disabled $(Get-Date -Format 'yyyy-MM-dd') - Offboarding" `
-WhatIf

Move:
Move-ADObject -Identity $User.DistinguishedName
-TargetPath $DisabledOU `
-WhatIf

這就是:
Dry Run / Preview。

不要只靠 Write-Host
真正執行 Change,最好有 Log。
Day 6 我們已經寫過:
function Write-Log {

param (
    [string]$Message,
    [string]$Level = "INFO"
)

$Time =
    Get-Date -Format "yyyy-MM-dd HH:mm:ss"

"$Time [$Level] $Message" |
Add-Content `
    -Path $LogFile `
    -Encoding UTF8

}

今天 Change Log 至少可以記:
Timestamp
Account
Action
Result
Operator
Message

例如:
2026-09-25 05:30:00 [INFO] user01 - Begin offboarding
2026-09-25 05:30:01 [INFO] user01 - Account disabled
2026-09-25 05:30:02 [INFO] user01 - Removed from VPN-Users
2026-09-25 05:30:03 [INFO] user01 - Moved to DisabledUsers
2026-09-25 05:30:04 [INFO] user01 - Verification completed

幾個月後才知道:
這個帳號到底被誰的 Automation 做過什麼。

Error Handling 更重要了
以前 Report Script 錯一筆:
少一筆資料

已經不好。
但 Change Script 如果:
Disable 成功
Move OU 失敗
Group Remove 做一半

就可能留下:
Partial State

所以今天一定要:
try
catch

例如:
try {

Disable-ADAccount `
    -Identity $UserName `
    -ErrorAction Stop

Write-Log `
    "$UserName disabled successfully"

}
catch {

Write-Log `
    "$UserName disable failed: $($_.Exception.Message)" `
    "ERROR"

}

但 try/catch 不能讓你誤以為有 Transaction
這裡值得特別注意。
假設 Script:
Step 1 Disable → Success
Step 2 Remove Group → Success
Step 3 Move OU → Failed

catch 可以知道 Step 3 失敗。
但 Active Directory 不會因為 Step 3 失敗,就自動:
把 Step 1、2 Undo

所以:
這不是 Database Transaction。

Change Automation 最好每一步:
可驗證
可記錄
知道失敗後怎麼處理

Verification 要重新從 AD 查
例如 Disable 後:
$VerifyUser = Get-ADUser -Identity $UserName
-Properties `
Enabled,
Description

確認:
$VerifyUser |
Select-Object `
SamAccountName,
Enabled,
Description,
DistinguishedName

如果期待:
Enabled = False

但結果:
Enabled = True

那整個 Change 就不應該被標記:
Success

我會把 Status 分得更清楚
例如:
Preview
Success
Failed
Partial
Skipped

而不是只有:
OK
NG

因為實際 Change Automation 很容易有:
帳號找不到
→ Skipped

Disable 成功但 Move 失敗
→ Partial

所有步驟成功
→ Success

只做 WhatIf
→ Preview

這對後面的 Report 非常重要。
建立 Offboarding CSV
未來 HR / IT 可以提供:
SamAccountName,OffboardingDate,Ticket
user01,2026-09-30,INC001001
user02,2026-10-01,INC001002

但我不建議 Script 一讀到 CSV 就自動執行。
第一階段先:
Import CSV
↓
Validate Account
↓
產生 Preview Report

例如:
$Requests =
Import-Csv "C:\Temp\Offboarding.csv"

然後:
foreach ($Request in $Requests) {

$User = Get-ADUser `
    -Identity $Request.SamAccountName `
    -Properties Enabled `
    -ErrorAction SilentlyContinue

[PSCustomObject]@{

    SamAccountName =
        $Request.SamAccountName

    UserExists =
        ($null -ne $User)

    CurrentlyEnabled =
        if ($null -ne $User) {
            $User.Enabled
        }
        else {
            $null
        }

    OffboardingDate =
        $Request.OffboardingDate

    Ticket =
        $Request.Ticket
}

}

這份才是:
Change Preview Report。

今天完整範例:安全版 Offboarding Preview
這一版故意只做 Preview,不直接修改 Production AD。

==========================================

AD Offboarding Preview

Day 17

==========================================

Import-Module ActiveDirectory

==========================================

Configuration

==========================================

$UserName =
"user01"

$DisabledOU =
"OU=DisabledUsers,DC=contoso,DC=com"

$SensitiveGroups = @(
"VPN-Users",
"Server-Admins",
"FileServer-Admins"
)

$ReportFolder =
"C:\Temp\ADReports"

$Date =
Get-Date -Format "yyyyMMdd"

==========================================

Validate Report Folder

==========================================

if (-not (Test-Path $ReportFolder)) {

New-Item `
    -Path $ReportFolder `
    -ItemType Directory |
    Out-Null

}

==========================================

Validate User

==========================================

try {

$User = Get-ADUser `
    -Identity $UserName `
    -Properties `
        Enabled,
        Department,
        Title,
        Description,
        LastLogonDate `
    -ErrorAction Stop

}
catch {

Write-Error `
    "User query failed: $($_.Exception.Message)"

return

}

==========================================

Before State

==========================================

$BeforeState = [PSCustomObject]@{

SamAccountName =
    $User.SamAccountName

Name =
    $User.Name

Enabled =
    $User.Enabled

Department =
    $User.Department

Title =
    $User.Title

LastLogonDate =
    $User.LastLogonDate

DistinguishedName =
    $User.DistinguishedName

CheckTime =
    Get-Date

}

$BeforeState |
Export-Csv -Path "$ReportFolder\$($UserName)_Before_$Date.csv"
-NoTypeInformation `
-Encoding UTF8

==========================================

Group Snapshot

==========================================

$Groups =
Get-ADPrincipalGroupMembership `
-Identity $UserName

$Groups |
Select-Object Name, GroupCategory, GroupScope, DistinguishedName | Export-Csv
-Path "$ReportFolder$($UserName)Groups_Before$Date.csv" -NoTypeInformation
-Encoding UTF8

==========================================

Sensitive Group Review

==========================================

$SensitiveGroupReview =
foreach ($GroupName in $SensitiveGroups) {

    $IsMember =
        $Groups.Name -contains $GroupName

    [PSCustomObject]@{

        SamAccountName =
            $UserName

        GroupName =
            $GroupName

        IsMember =
            $IsMember

        PlannedAction =
            if ($IsMember) {
                "Remove"
            }
            else {
                "None"
            }
    }
}

$SensitiveGroupReview |
Export-Csv -Path "$ReportFolder\$($UserName)_GroupReview_$Date.csv"
-NoTypeInformation `
-Encoding UTF8

==========================================

Preview AD Changes

==========================================

Write-Host ""
Write-Host "===== PREVIEW ONLY ====="
Write-Host ""

Disable-ADAccount -Identity $UserName
-WhatIf

foreach ($Item in $SensitiveGroupReview) {

if ($Item.IsMember) {

    Remove-ADGroupMember `
        -Identity $Item.GroupName `
        -Members $UserName `
        -Confirm:$false `
        -WhatIf
}

}

Set-ADUser -Identity $UserName
-Description "Disabled $(Get-Date -Format 'yyyy-MM-dd') - Offboarding" `
-WhatIf

Move-ADObject -Identity $User.DistinguishedName
-TargetPath $DisabledOU `
-WhatIf

Write-Host ""
Write-Host "No Active Directory changes were made."
Write-Host "Review the preview and CSV reports before approval."

這支 Script 最大的重點
最後一句是:
No Active Directory changes were made.

因為:
-WhatIf

還在。
也就是:
Input
↓
Query
↓
Backup Current State
↓
Group Review
↓
Preview
↓
STOP

到這裡先停。
這比:
寫好 Script
↓
直接跑 Production

安全很多。
Production 版本應該再加 Approval Gate
未來可以做:
Offboarding.csv
↓
Validation
↓
Preview Report
↓
Ticket / Manager Approval
↓
Approved.csv
↓
Change Script
↓
Verification
↓
Change Report

也就是把:
Request

和:
Execution

分開。
這會比用:
按 Y 確認

更加適合企業流程。
Onboarding 其實也可以使用相同架構
例如:
NewUsers.csv
↓
Validation
↓
帳號是否重複?
OU 是否存在?
Group 是否存在?
UPN 是否重複?
↓
Preview
↓
Approval
↓
Create User
↓
Add Group
↓
Verify
↓
Report

所以今天學到的不只是:
New-ADUser
Disable-ADAccount

而是一個可以套用到很多 Automation 的模式:
Detect
Validate
Preview
Approve
Change
Verify
Audit

為什麼我一直強調 Preview?
因為 Automation 有一個特性:
它不只讓正確的事情變快,也會讓錯誤的事情變快。

人工操作:
弄錯一個帳號

可能影響一個人。
Automation:
foreach 500 Users

條件寫錯,可能一下影響很多帳號。
所以越是批次、自動化、權限高的 Script,就越需要:
Validation
WhatIf
Scope
Approval
Logging
Verification

Day 17 小結
今天是整個系列的一個重要分界點。
Day 1~16 大部分在做:
Read
Query
Audit
Report

Day 17 第一次正式開始:
Change

主要接觸:
New-ADUser

Set-ADUser

Add-ADGroupMember

Disable-ADAccount

Remove-ADGroupMember

Move-ADObject

但比這些 Cmdlet 更重要的是今天建立的 Change Workflow:
Input
↓
Validation
↓
Before-State Snapshot
↓
Preview / WhatIf
↓
Approval
↓
Action
↓
Verification
↓
Log / Report

尤其是離職帳號,我們不再把它想成:
Disable User

而是一整個 Lifecycle:
確認帳號
↓
保存原始狀態
↓
保存 Group Membership
↓
Disable
↓
清理必要的敏感權限
↓
更新標記
↓
Move Disabled OU
↓
Verification
↓
Retention
↓
依 Policy 決定未來是否 Delete

真正好的 Automation 不是:
「什麼都自動做。」

而是:
「把適合自動化的部分做快,同時保留必要的確認、核准與追蹤。」

這也是從「會寫 PowerShell」走向「能把 PowerShell 放進企業維運流程」非常重要的一步。
Day 18 預告
Day 18|AD 批次建立帳號:從 CSV Onboarding 到自動建立 User
Day 17 我們已經知道:
New-ADUser

可以建立一個帳號。
但真正的 Automation 不會只建立:
1 個 User

而是開始處理:
NewUsers.csv

姓名
帳號
Department
Title
OU
UPN
需要的 Group

Day 18 我們會建立:
HR / IT Input CSV
↓
Import-Csv
↓
欄位驗證
↓
SamAccountName 重複檢查
↓
UPN 重複檢查
↓
OU 是否存在
↓
Group 是否存在
↓
Preview
↓
建立 AD User
↓
加入 Group
↓
Result Report

並特別處理一個很重要的問題:
100 個新人裡面如果第 37 個建立失敗,前面成功的 36 個跟後面的 63 個該怎麼辦?
Day 18 會正式開始做第一支比較完整的 CSV-driven AD Onboarding Automation。


上一篇
Day 16|AD Group 自動化盤點:誰在哪個群組?權限到底從哪裡來?
下一篇
Day 18|AD 批次建立帳號:從 CSV Onboarding 到自動建立 User
系列文
系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言