香港服务器运行 Windows Server 时,如何启用 Just-in-Time(JIT)管理,降低管理员账号被滥用风险?

那是一个闷热的周三夜里,香港葵涌机房的湿度报警刚刚消下去,短信又跳了出来:关键业务 VM 有可疑 RDP 探测。我提起随身包进机房,风扇在机柜间低鸣,值守同事递来一杯冰水。我没开 3389 的公网入站,但外层设备上刚做过临时联调,窗口忘关就会出事。那晚之后,我把“JIT 管理”彻底落进所有 Windows Server 的基线,目标只有一个:需要时临时开、超时自动收,最小权限可审计。这篇就是我当时的落地方案与坑点记录。
我们的现场与约束
| 维度 | 参数/现状 |
|---|---|
| 机房与区域 | 香港(HK)、两套机柜,双上联(IX + 运营商) |
| 服务器形态 | 物理 + 虚拟混合:Dell R650(2×Xeon Silver 4310、256GB RAM、2×1.92TB NVMe)+ VMware ESXi 7.0;部分是独立香港服务器(单租户) |
| OS 版本 | Windows Server 2019/2022 Standard(Datacenter 少量) |
| 管理入口 | 跳板机(Bastion,域内),默认禁公网;生产节点默认禁 3389 入站 |
| 账号策略 | 域账号 + LAPS(Windows LAPS,集成 Entra ID/AD),禁共享本地 admin 密码 |
| 审计 | Windows 事件日志 + Sysmon,汇总到 SIEM(Elastic) |
| 典型风险 | 临时开口子后忘关;管理员长期驻留本地 Administrators;脚本留后门;审批不到位 |
核心目标:在网络、身份、命令面三层实现 JIT:
- 网络 JIT:RDP/WinRM 端口只在审批通过的时段、对特定源 IP 开放,超时自动收口。
- 身份 JIT:管理员权限按需临时授予(TTL),到点自动回收(移出本地 Administrators 或失效临时账号/口令)。
- 命令面 JIT(JEA):不需要“全能管理员”的操作,走 Just Enough Administration 限制命令集。
你不一定有 Azure 或固定厂商设备。本文方案:有云走云(PIM/JIT 互锁)、没云走原生(Windows 防火墙 + 计划任务 + JEA),都能落地。
总体流程(一图胜千言)
运维发起“临时维护”工单(期望时长、目标主机、源 IP)。
审批通过 → 自动化脚本在目标主机执行:
- 新建仅对申请人 源 IP 的临时 RDP 放行(TTL=60 分钟可配)。
- 将申请人域账号临时加入本地 Administrators(TTL 同步)。
- 创建到点清理的计划任务:删除防火墙规则 + 移出管理员组。
若操作只需有限命令(如重启服务/读日志),则通过 JEA 端点,无需提权至本地 admin。
全程写日志、事件落盘,推送到 SIEM;异常可一键强制回收。
基线收紧(落地前的必做)
1. 关公网 3389(前置防火墙/云安全组 + 本机防火墙双保险)
2. 强制 NLA、禁弱加密;RDP 仅在跳板机侧开放。
3. 启用 Windows LAPS(新版 Windows LAPS,支持 AD/Entra),禁共享本地 admin 口令。
4. 开启审计:
- 本地组成员变更(Event 4732/4733)、特权登录(4672)、登录成功(4624)
- 防火墙规则变更(4946/4947)
- PowerShell 脚本与模块日志(Module + Script Block + Transcription)
这一步是“地基”。没有它,JIT 只是“把门拉上但没插闩”。
方案一:网络 JIT——RDP/WinRM 只对指定源 IP、指定时间开放
1)最小可用脚本(目标主机上执行/远程推送执行)
# JIT-RDP.ps1
param(
[Parameter(Mandatory=$true)][string]$SourceIP,
[int]$DurationMinutes = 60,
[string]$Tag = "$(Get-Date -Format 'yyyyMMddHHmmss')"
)
$ruleName = "JIT-RDP-$Tag"
Write-Host "Creating temporary firewall allow rule: $ruleName for $SourceIP"
# 1. 开口子(仅入站、仅 TCP 3389、仅指定源 IP)
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3389 -RemoteAddress $SourceIP | Out-Null
# 2. 到点自动清理(计划任务)
$removeScript = "Remove-NetFirewallRule -DisplayName `"$ruleName`""
$time = (Get-Date).AddMinutes($DurationMinutes)
$action = New-ScheduledTaskAction -Execute 'PowerShell.exe' -Argument "-NoProfile -WindowStyle Hidden -Command `$ErrorActionPreference='Stop'; $removeScript"
$trigger = New-ScheduledTaskTrigger -Once -At $time
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
Register-ScheduledTask -TaskName $ruleName -Action $action -Trigger $trigger -Principal $principal | Out-Null
# 3. 审计落盘
$log = "C:\ProgramData\JIT\JIT-RDP.log"
New-Item -ItemType Directory -Force (Split-Path $log) | Out-Null
"[$(Get-Date -Format 'u')] ADD $ruleName Source=$SourceIP TTL=$DurationMinutes" | Out-File -FilePath $log -Append -Encoding utf8
Write-Host "Done. Rule will be removed at $time"
实战建议:
- 只允许单个或极窄网段源地址;禁止 Any。
- 跳板机到业务机的 RDP/WinRM 再做一层名单制(仅域运维组)。
- 如果你使用厂商防火墙(如 FortiGate),用其 API 做同样的“临时策略 + schedule”,和本机防火墙双收口。
方案二:身份 JIT——管理员权限临时授予、自动回收
2.1 域账号临时加入本地 Administrators(计划任务到点回收)
# JIT-Admin.ps1
param(
[Parameter(Mandatory=$true)][string]$SamAccount, # 如 corp\alice
[int]$DurationMinutes = 60,
[string]$Tag = "$(Get-Date -Format 'yyyyMMddHHmmss')"
)
$taskName = "JIT-ADMIN-$Tag"
# 1. 加入本地 Administrators
net localgroup Administrators "$SamAccount" /add
# 2. 回收计划任务
$removeCmd = "net localgroup Administrators `"$SamAccount`" /delete"
$time = (Get-Date).AddMinutes($DurationMinutes)
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/c $removeCmd"
$trigger = New-ScheduledTaskTrigger -Once -At $time
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal | Out-Null
# 3. 记录
$log = "C:\ProgramData\JIT\JIT-Admin.log"
New-Item -ItemType Directory -Force (Split-Path $log) | Out-Null
"[$(Get-Date -Format 'u')] ADD $SamAccount TTL=$DurationMinutes task=$taskName" | Out-File -FilePath $log -Append -Encoding utf8
Write-Host "Granted temporary admin to $SamAccount. Will be revoked at $time"
配套 GPO:禁用“将域用户自动添加到本地管理员”的旧策略;只通过上述 JIT 脚本临时添加。
审计点:4732/4733(本地组变更)、4688(进程创建,记录脚本执行)、PowerShell 脚本块日志。
2.2 Windows LAPS(新)+ 时间敏感取用(可选)
场景:不想把域账号加进本地 Administrators,但又需要短时做“本地 admin”。
做法:启用 Windows LAPS(组策略/Entra),将本地管理员口令随取随用,并配置短 TTL 和 强制轮换。
取口令完成后立刻触发 RotateNow;或者把 JIT 窗口结束时执行强制轮换,避免密码残留。
注意旧版 LAPS(MSI)与新版 Windows LAPS配置差异较大,域/Entra 环境要统一标准,避免两套并存。
方案三:命令面 JIT(JEA)——只给“刚好够用”的命令
很多现场根本不需要“管理员桌面”。比如应用卡死,只需要 Restart-Service 到指定服务。用 JEA 定义角色能力和受限会话,用户连上来只能执行白名单命令。
3.1 Role Capability(例:允许查看/重启特定服务)
C:\Program Files\WindowsPowerShell\Modules\Company.JEA\RoleCapabilities\ServiceOps.psrc
@{
GUID = '0b6f7b2e-1c2a-4c42-b1e0-aaaa1111bbbb'
Author = 'Ops'
VisibleCmdlets = @(
@{ Name = 'Get-Service'; Parameters = @{ Name='Name'; ValidateSet=@('IISADMIN','W3SVC','Spooler') } },
@{ Name = 'Restart-Service'; Parameters = @{ Name='Name'; ValidateSet=@('W3SVC') } }
)
VisibleProviders = @()
ScriptsToProcess = @()
ModulesToImport = @()
}
3.2 会话配置
C:\Program Files\WindowsPowerShell\Modules\Company.JEA\JEASvcOps.pssc
@{
SessionType = 'RestrictedRemoteServer'
TranscriptDirectory = 'C:\ProgramData\JEA\Transcripts'
RunAsVirtualAccount = $true
RoleDefinitions = @{
'CORP\ServiceOpsGroup' = @{ RoleCapabilities = 'ServiceOps' }
}
}
注册并启用:
Register-PSSessionConfiguration -Name 'JEA-ServiceOps' -Path 'C:\Program Files\WindowsPowerShell\Modules\Company.JEA\JEASvcOps.pssc' -Force
Restart-Service WinRM
这样绝大部分“看与重启”类操作都不需要“提管理员”。JIT 窗口只在极少数场景打开,风险面小得多。
把三层串起来:一键启用/回收(示例编排脚本)
# Invoke-JIT.ps1 —— 由跳板机执行,通过 WinRM 到目标主机
param(
[Parameter(Mandatory=$true)][string]$Target, # 目标主机 FQDN
[Parameter(Mandatory=$true)][string]$Operator, # 申请人,如 corp\alice
[Parameter(Mandatory=$true)][string]$SourceIP, # 申请人源 IP
[int]$Minutes = 60
)
$session = New-PSSession -ComputerName $Target -Authentication Negotiate
Invoke-Command -Session $session -ScriptBlock {
param($Operator,$SourceIP,$Minutes)
& "C:\Ops\JIT-RDP.ps1" -SourceIP $SourceIP -DurationMinutes $Minutes
& "C:\Ops\JIT-Admin.ps1" -SamAccount $Operator -DurationMinutes $Minutes
} -ArgumentList $Operator,$SourceIP,$Minutes
Write-Host "[$Target] JIT window granted to $Operator from $SourceIP for $Minutes minutes."
在我们环境里,这个脚本由审批流(Power Automate/ServiceNow)触发,变量从工单里带入。审批通过才会远程到目标机执行。
验收指标(实施前后对比)
| 指标 | 改造前 | 改造后(目标) |
|---|---|---|
| 3389 暴露时长/周 | 120 分钟 | ≤ 10 分钟 |
| 临时管理员留存事件/月 | 3–5 起 | 0(全部自动回收) |
| 审批平均耗时 | 15–20 分钟 | 5–8 分钟(半自动) |
| 未授权登录告警/月 | 偶发 | 显著下降 |
| 审计可追溯性 | 低(口头记录) | 高(日志 + 工单 + 事件) |
常见坑与现场解法
计划任务不执行/不清理
原因:未以 SYSTEM/最高权限运行;机器时钟漂移;脚本路径有空格未转义。
解法:用 -UserId SYSTEM -RunLevel Highest;统一 NTP;所有命令转义并走 cmd /c 或 PowerShell -Command。
源 IP 变化导致连不上
让申请人先通过跳板机(固定出口),或工单要求固定出口 IP。
避免对公网动态宽带直开口子。
Windows LAPS 与旧版 LAPS 混用
统一迁移到 Windows LAPS;梳理 GPO,确认只生效一套;完成后强制轮换一次。
JEA 被“聪明人”绕过
RoleCapability 严格限制 VisibleCmdlets 和参数;禁用 Invoke-Expression、Add-Type 等;记录 Transcript,异常上 SIEM 告警。
运维觉得麻烦
让脚本“一键开窗”,自动填工单字段;默认 30–60 分钟 TTL,超时必须重申。
统计每周开窗次数,结合 SRE 回顾减少不必要的人工接入。
防火墙规则名冲突
规则命名带时间戳/Guid;清理任务按DisplayName 精确匹配。
服务器重启期间
清理任务用 Absolute Time 触发,重启后依然有效;同时在启动脚本里加“兜底清理”(发现过期 JIT 规则即删)。
最后,把审计做满
事件订阅:
- 4732/4733(本地组变更)
- 4946/4947(防火墙规则变更)
- 4624/4625(登录)+ 4672(特权)
- PowerShell Script Block 与 Transcript
落 SIEM 后做三类告警:
- JIT 窗口外的 3389 入站尝试;
- 本地 Administrators 里出现非 JIT 任务添加的成员;
- 口令未轮换/计划任务失败。
附:最小可用清单(可直接抄)
C:\Ops\JIT-RDP.ps1(上文脚本)
C:\Ops\JIT-Admin.ps1(上文脚本)
Company.JEA 模块:RoleCapabilities\ServiceOps.psrc + JEASvcOps.pssc
GPO:
- 启用 Windows LAPS,配置 TTL 与强制轮换
- 开启 PowerShell 日志(Script Block、Module、Transcription)
- 审计策略:账户管理、特权使用、对象访问
- SIEM 解析与告警三条规则
结尾:又一次“深夜开窗”
两个月后的一次版本回退,应用只需重启服务。年轻同事在工单里点了“JEA-ServiceOps”,我手机“叮”一声,审批通过。没有大开管理员、没有放行公网 RDP,他在跳板机上跑了两条命令,窗口 30 分钟后自己关上。机房里的风还是从走廊尽头吹来,但我知道,门是开的、也是会自己关上的。这就是 JIT 在我环境里的意义——不牺牲效率的前提下,把风险留在可控的时间盒里。