1. 从一次“手滑”说起:为什么要把防内鬼当成系统问题
如果你所在的团队管着上百台服务器,日常操作基本都靠 OpenClaw 这类统一运维平台下发命令,那你大概率体会过那种后怕的瞬间:某条Remove-Item -Recurse指向了不该指的位置,某个Stop-Service停掉了核心应用,某个reg delete删掉了不该动的配置项。大多数时候是误操作,但偶尔也可能是“主观故意”。无论是哪一种,事后如果只能翻聊天记录、查登录日志,那基本等于没有防线。
我这次对 OpenClaw 的命令通道做了一次完整改造,核心就三件事:高危命令在执行前先被拦下来、走审批流程、批准后才能执行;所有命令从登录到执行到输出全程留痕;只要有命令被放行,事后能按时间线完整回溯,谁在哪个会话里干了什么一目了然。整套体系基于 PowerShell 构建,因为 OpenClaw 管理的节点绝大多数是 Windows,PowerShell 既是命令语言,也是审计数据最丰富的来源。
这篇文章不是讲理论,而是把我在实际落地中的设计取舍、规则写法、审批流实现、审计链路搭建,以及踩过的坑都记录下来。适合正在做内部运维平台加固、Windows 主机安全审计、或者被合规要求追着要“操作留痕”的同学参考。哪怕你没用 OpenClaw,只要你的运维工具支持自定义命令策略,这套思路也能直接平移过去。
2. 方案选型:为什么是 PowerShell 加三道门
2.1 三个现实约束决定了技术路线
动手之前我列过一个约束清单,当时考虑过 Python 脚本做监听、考虑过在网关层做流量解析、也考虑过在每台机器上装商业 Agent。最终放弃这些路线的理由很现实:
第一,OpenClaw 本身使用 PowerShell Remoting 协议和节点通信,命令在通道里就是纯文本的 PowerShell 代码,这意味着我们可以在不改动节点端架构的前提下,在命令进入执行通道前做一次“语义检查”,这是成本最低的拦截点。
第二,PowerShell 自带的事件日志、转录日志、脚本块日志,能把命令的“原文”和“执行结果”完整记录下来,不需要额外埋点。Windows 事件日志里其实已经包含了大量信息,只是默认没打开。
第三,商业 Agent 的采购周期太长,而且为了一个审批功能引入一套重型产品,对团队来说并不划算。我们要的是“在现有平台上长出一层控制能力”,不是另起炉灶。
2.2 三道防线:入口拦截、审批中心、审计归档
整个体系最后收敛成三个独立组件,各管一段:
第一道是入口拦截器。它挂在 OpenClaw 的命令下发服务里,每条命令在执行前都必须先经过规则引擎。规则命中“高危”,命令不会进入执行队列,而是进入审批中心等待。
第二道是审批中心。这是一个独立于 OpenClaw 的小型服务,有自己的数据库和独立的审批账号体系。高危命令在这里挂起,审批人看到命令原文、执行目标、操作人、执行上下文,做出同意或拒绝的决定。审批中心不直接参与命令执行,它只下发“允许执行”的令牌。
第三道是审计归档。节点端的 PowerShell 日志、OpenClaw 的操作日志、审批中心的决策记录,统一汇总到日志平台,按会话 ID 做关联,形成完整时间线。
三道门分开部署的原因很简单:如果拦截和审批放在同一个进程里,管理员一台机器登录上去改了配置就全绕过了。独立的审批中心意味着“会执行命令的人”和“能审批命令的人”默认是被拆开的,这是防内鬼的基础。
注意:方案里最关键的设计不是“拦截得多狠”,而是“避免自己审自己”。命令的发起、执行、审批、审计四类权限必须落到不同角色身上,否则防线形同虚设。
3. 高危命令规则设计:怎么定义“高危”才不会被绕过
3.1 高危命令的四类画像
设计规则引擎之前,我们先把高危命令分了类,这个分类决定了规则怎么写:
第一类是破坏类,典型的就是Remove-Item、Clear-Content、Format-Volume、Stop-Service、Restart-Computer,以及各种del、rd。这类命令一旦执行,轻则服务中断,重则数据丢失。匹配的时候不能只看命令名,还要看参数,Remove-Item带了-Recurse和-Force的破坏性远大于不带参数的情况。
第二类是信息泄露类,典型的是Get-Content读配置文件、Get-ChildItem -Recurse遍历目录、Export-Csv导出数据。还有网上的数据外发通道,比如Invoke-WebRequestPOST 到外部地址。这类命令日常使用频率高,不能一刀切禁止,但必须对目标路径和 URL 做额外判断。
第三类是权限提升类,包括修改本地用户组、修改注册表自启动项、关闭 Windows Defender 的Set-MpPreference -DisableRealtimeMonitoring、创建计划任务的Register-ScheduledTask。这类命令往往是攻击链路上的关键动作,一旦放行,后续防御基本失效。
第四类是绕过类,指那些本身不直接干坏事、但常被用来绕过监控的手段,比如起一个cmd.exe隔离子进程、用-EncodedCommand传一段编码后的命令、通过 WMI 远程执行、用Start-Process拉起一个隐形进程。
分类完成后,规则引擎就不再是“一串黑名单关键词”了,而是按类别配置不同策略:破坏类大多是“直接拦截,必须审批”,泄露类更多是“记录内容并通知”,绕过类则直接阻断并触发告警。
3.2 规则匹配的粒度:命令名、参数、上下文
规则匹配只做字符串扫,是最初级的做法。比如你写一个正则匹配Remove-Item,那么用户写成Remove-Item -LiteralPath也能匹配到,没问题。问题在于有人会写成ri -r C:\data,ri是Remove-Item的别名,如果规则不解析别名,一下就放过去了。
所以我们的规则引擎分三个层次:
第一层是命令名规范化。先把命令里的前缀Microsoft.PowerShell.Management\Remove-Item这类全名摘出来,再把别名解析回正式命令名,比如ri映射到Remove-Item,cls映射到Clear-Host。这一步用 PowerShell 自带的解析器就能做,因为Get-Alias能拿到所有内置别名映射。
第二层是参数解析。命令名命中之后,要看参数有没有触碰敏感组合开关。比如Remove-Item是否带-Recurse,New-Item是否操作了HKLM:\Software\...,Set-ItemProperty的 Path 是不是HKLM开头。用一个参数白名单/黑名单表去匹配。
第三层是上下文判断。同样的命令在不同主机上风险等级不同。比如停止某个服务的命令,在测试机上执行可能只是正常操作,在核心生产节点上执行就是高危。规则引擎需要拿到 OpenClaw 里的主机标签,命中prod标签的主机,规则阈值自动调高一档。
3.3 规则表设计:版本化、可热更新
规则本身需要版本化,否则每次调整规则还得重新发布服务。我们的规则表结构大概是这样的:
| 字段 | 说明 |
|---|---|
| RuleId | 规则唯一编号 |
| Category | 破坏 / 泄露 / 提权 / 绕过 |
| Pattern | 命令名或正则表达式 |
| RequiredParams | 必须同时出现的参数关键词 |
| Scope | all / 指定主机标签 |
| Action | block / audit / prompt |
| Priority | 规则优先级,命中多条时取最高 |
| Enabled | 是否启用 |
规则文件放在单独配置目录,使用 JSON 存储。OpenClaw 网关启动时加载一次,之后每隔两分钟检查配置版本,有更新就自动拉取。这样做的好处是,紧急情况下只改配置文件就能把某条命令彻底锁死,不需要重启服务。
个人建议:规则表宁可写得多而细,也不要图省事故意写宽泛。宽泛的正则必然误伤正常操作,团队成员被拦几次之后就会产生“这个系统不专业”的抵触情绪。把规则拆细,配合白名单动态放行,体验会好很多。
4. 审批流实现:从拦截到放行的完整闭环
4.1 命令被拦下之后发生了什么
一条命令命中高危规则之后,OpenClaw 网关并不会立刻拒绝它,而是生成一个审批单。审批单里至少包含以下信息:
- 操作者账号
- 会话来源 IP
- 目标主机名和主机标签
- 命令完整原文
- 命令输出预览(部分命令可以先安全地查一下,比如
Get-Item类) - 命中规则编号和风险等级
审批单进入审批中心后,状态是pending。审批人可以是轮值同学,也可以按命令类别分发到不同负责人。为了不让流程卡住,我们做了两级超时策略:超时 30 分钟未处理,系统自动升级通知到上一级;超时 2 小时仍未处理,默认拒绝执行。
“超时默认拒绝”这条很重要。刚开始我们设的是“超时自动放行”,后来在一次演练里发现,命令在半夜挂起后没人处理,结果两个小时后自动执行了,正好赶上一次变更窗口,差点出事。从安全角度,审批系统宁可让人烦,也不能让命令悄悄溜走。
4.2 审批最小权限原则:谁审批,谁背锅
审批中心权限模型上做了严格划分,这是全项目里我认为最值得讲的部分:
执行者不能审批自己的命令。命令发起人的账号和审批人的账号是分开的,系统里做硬校验,发起人账号永远没有审批权限。
审批人分为两层:普通审批人可以同意“常规高危”命令,比如测试环境重启服务;但涉及“权限提升”“绕过类”的命令,必须由 Level-2 审批人处理,这类人通常是安全负责人或组长。Level-2 审批人的账号是只读的,可以查看审计数据,但不能直接登录执行服务器。
所有审批动作都要求填写“理由”,拒绝时必须写原因。理由会被记录并把结果回传给发起人,方便团队复盘。这个设计不是为了一层皮,而是事后回看时,每一条执行过的命令都能找到当时的决策上下文,避免“当时不知道为什么要放行”的糊涂账。
4.3 放行令牌机制:审批通过不等于命令直接执行
审批通过后,命令不会直接进入执行通道,而是生成一个一次性令牌,令牌里包含审批单 ID、命令原文的哈希、有效时间。OpenClaw 网关校验令牌后,才会把命令真正发给目标主机。
这个令牌机制解决了一个隐蔽问题:如果审批通过后直接把命令原样下发,那么审批人同意的“命令 A”和执行时实际执行的“命令 B”,中间如果被人篡改,审计链路里就存在断点。令牌里存命令哈希,网关在最后下发前再做一次哈希比对,不一致就直接拒绝。
有效时间我们设置为 10 分钟。超过了就得重新审批。实测下来,几分钟内发起人一般都会立刻执行,10 分钟足够正常操作,同时也能防止“审批一次,反复使用”的漏洞。
5. PowerShell 日志审计:把每一滴痕迹都收进来
5.1 三条日志线索必须同时开
审批流解决的是“高危命令怎么控制”,审计解决的是“所有操作怎么还原”。PowerShell 端有三条日志线索,少了哪个都不完整:
第一条是 PowerShell 事件日志,重点看两个事件 ID:4103(模块日志,记录管道执行过的 cmdlet)和 4104(脚本块日志,记录执行的代码块原文)。4104 尤其重要,因为-EncodedCommand这类被编码过的命令,在执行时会先解码,而脚本块日志记录的是解码后的原始内容,这是还原真相的关键。
第二条是 PowerShell 转录日志,也就是 Start-Transcript 体系。它会把会话里所有输入和输出写入指定文件。第一个坑是 PowerShell 5.1 默认不会启用系统级转录,需要组策略或注册表打开,具体在HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription下配置EnableTranscripting。第二个坑是转录日志默认只记录交互式会话,脚本执行不会记录,需要把EnableInvocationHeader一并打开,并在 OpenClaw 发起会话时显式启动转录路径。
第三条是操作系统层的事件日志,包括 4624(登录成功)、4625(登录失败)、4688(进程创建)这类安全事件,以及 Sysmon 的进程创建、网络连接事件。这一步是为了捕获“PowerShell 之外”的痕迹,比如有人从 PowerShell 里Start-Process cmd.exe拉起子进程,PowerShell 日志里不一定有执行细节,但 Sysmon 的进程创建事件一定会有。
5.2 审计字段的统一:靠会话 ID 串起来
日志只有收进来还不够,还得把不同来源的日志按同一条线索串起来。我们在 OpenClaw 的命令通道里做了一件事:每次用户建立会话,就生成一个全局唯一的 SessionId,并且明文写入每个命令执行时的工作目录。
具体操作是在发起端构造命令时,先把 SessionId 以注释形式挂在命令头部,比如# {{SessionId: 8f3a2c7e2b9d4a1f}}。PowerShell 脚本块日志会把这段注释原样记录下来,事件日志的 4104 事件里也能找到它。OpenClaw 自身的操作日志本身就带 SessionId,这样三份日志只要做一次字符串匹配,就能把同一条命令的前因后果拼起来。
实践证明,这个设计比单纯依赖时间戳关联靠谱得多。因为命令并发量高的时候,同一秒内可能有多条命令在多个主机上执行,靠时间轴去对根本对不上。
5.3 日志采集落地方案和存储规划
日志采集我们用了轻量转发代理,在每台 Windows 节点上部署一个任务计划,每十分钟把新产生的事件日志和转录文件推送到日志平台。推送失败就保留本地,下次启动时补传。
存储上有一个必须提前算好的问题:PowerShell 4104 事件量非常大,尤其开了脚本块日志之后,CPU 密集型脚本会产生海量事件。我们按每天约 2GB 原始日志的体积做了估算,保留 90 天热数据加上一年冷归档,压缩后约 700GB。如果日志平台磁盘预算不够,建议早期就直接定“按节点保留”,优先保核心业务主机,边缘节点只保留 30 天。
6. 实操过程记录:从搭骨架到跑通全流程
6.1 规则引擎的关键代码骨架
规则引擎不复杂,但有几个容易写错的地方。核心逻辑是这样一段 PowerShell,把命令文本解析成 AST,再对 AST 做参数匹配:
# 规则字段示例 $rule = [pscustomobject]@{ RuleId = 'R-1001' Category = 'destroy' CommandName = 'Remove-Item' RequiredParams = @('-Recurse', '-Force') Scope = 'tag:prod' Action = 'block' } # 使用 AST 解析命令文本 $tokens = $null $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseInput($commandText, [ref]$tokens, [ref]$errors) # 遍历所有命令元素,检查参数是否命中规则 foreach ($cmdAst in $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true)) { $cmdName = $cmdAst.GetCommandName() $paramMap = @{} foreach ($el in $cmdAst.CommandElements) { if ($el -is [System.Management.Automation.Language.CommandParameterAst]) { $paramMap[$el.Name] = $true } } if ($cmdName -eq $rule.CommandName) { $needParamsSatisfied = $rule.RequiredParams | Where-Object { -not $paramMap.ContainsKey($_) } if ($needParamsSatisfied.Count -eq 0) { Write-Warning "高危命令命中:$cmdName" break } } }这段代码用的是 PowerShell 自带的 AST 解析器,不依赖正则硬扣字符串,可以识别注释、字符串拼接、变量替换后的语义。比如用户写Remove-Item -Path "$env:TEMP\cache" -Recurse -Force,AST 解析后依然能正确提取-Recurse和-Force。
6.2 审批中心的核心接口
审批中心是个很小的内部 Web 服务,OpenClaw 网关在命令命中高危规则后,调用它的 REST API 创建审批单:
$body = @{ sessionId = $submitContext.SessionId operator = $submitContext.UserName hostName = $submitContext.HostName commandText = $commandText ruleId = $rule.RuleId riskLevel = $rule.Category } | ConvertTo-Json Invoke-RestMethod -Uri 'http://approval-center.internal/v1/tickets' ` -Method Post ` -ContentType 'application/json' ` -Body $body审批人通过 Web 页面看到命令行上下文后,点“批准”或“拒绝”。批准时后端生成 token:
$token = [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) $ticketHash = Get-FileHash -InputStream ([System.IO.MemoryStream]::New([Text.Encoding]::UTF8.GetBytes($commandText))) -Algorithm SHA256 # token 绑定审批单 ID 和命令哈希 New-Object psobject -Property @{ TicketId = $ticket.Id Token = $token CommandHash = $ticketHash.Hash ExpireAt = (Get-Date).AddMinutes(10) }网关拿着 token 到审批中心做一次校验,校验通过并且命令哈希一致,才会真正把命令下发到目标节点。这个流程跑通之后,“审批通过但命令被人改了”这类绕过路径就断了。
6.3 审计数据的提取脚本
审计侧我写了一个专门的提取器,定期把 4104 事件拉出来落库:
$events = Get-WinEvent -FilterHashtable @{ LogName = 'Microsoft-Windows-PowerShell/Operational' Id = 4104 } -MaxEvents 5000 foreach ($evt in $events) { $scriptBlockText = $evt.Properties[1].Value $sessionId = $null if ($scriptBlockText -match '#\{\{SessionId: ([a-f0-9\-]+)\}\}') { $sessionId = $Matches[1] } [pscustomobject]@{ Timestamp = $evt.TimeCreated SessionId = $sessionId Script = $scriptBlockText User = $evt.Properties[0].Value } }提取器处理过-EncodedCommand的场景,因为 4104 事件里记录的是解码后的命令文本,所以审计数据里看到的是真实内容,这一点在排查问题时特别有用。
7. 常见问题与排查技巧实录
7.1 误拦截太多,成员抵触
上线第一周,团队成员反馈最多的就是“正常命令也被拦”。排查发现是规则里的参数表写得过宽,比如Get-ChildItem -Recurse在某些主机上读取项目文件目录也被判定为泄露类,触发审批。
解决办法是把规则 Scope 精确到主机标签,另外加了一个“白名单模式”:同一操作者在同一主机上,一周内被放行超过三次的同类命令,系统自动加入该操作者的个人白名单,后续不再拦截。个人白名单是有上限的,最多 20 条,防止白名单膨胀成黑账户。
7.2 日志缺失,查不到命令执行记录
有几次被拦下来的命令需要举证,去翻 4104 日志发现根本没有记录。最后定位是组策略没刷下去,PowerShell 5.1 的脚本块日志默认关闭,部分节点 GPO 刷新周期是四小时,导致当天新装的机器没有日志。
处理措施是部署时统一通过注册表直接写入,并且写完后用Get-ItemProperty校验,而不是等 GPO。另外转录日志路径如果配置在 C 盘系统盘,磁盘满会导致转录直接停止,所以转录目录别放系统盘,单独挂一个日志盘,并且定期压缩轮转。
7.3 命令编码问题导致匹配失效
有人会把命令拆成字符串拼接再Invoke-Expression,比如"Remove-" + "Item"。虽然 AST 解析可以还原字符串内容,但正则规则如果写在“命令名”层面就匹配不到。所以规则引擎要多加一条检查:命令文本里出现Invoke-Expression和字符串拼接组合,直接作为“绕过类”处理。
另外 Base64 命令也很常见。OpenClaw 网关层面可以弱校验:如果命令文本长度超过 200 且近似 Base64 特征,并且解析失败,就转人工审批。虽然可能误拦截一些正常编码参数,但安全性优先。
7.4 性能压测数据
上线前做了压测,模拟每天 8 万条命令的规模。加了预编译正则和 AST 解析缓存之后,单条命令的平均检查耗时在 4ms 左右,只有第一次解析同一类命令才需要走完整 AST,后续都会命中缓存。OpenClaw 网关原先单次命令下发平均 150ms,加固后只增加了约 6% 的延迟,对日常操作基本无感。
比较需要注意的反而是日志采集端的性能。4104 事件日志全量打开后,会有少量 CPU 开销,我们在一台 8 核机器上实测约增加 2% 的 CPU 使用率。如果节点是 4 核以下的老机器,建议把脚本块日志的-IncludeInvocationHeader打开后就别再额外加什么全量记录,避免拖垮生产环境。
8. 延伸扩展:这套防线还能怎么用
整套系统跑通之后,有几个方向可以直接扩展,我个人觉得最实用的是“命令风险评分”。现在我们的规则引擎是黑白名单式的,命中一个规则就判断,但真实场景里命令往往是多个操作的组合,单一规则判断不够精准。
可以给每条命令综合打分:破坏类正则命中加 20 分、命令作用在核心标记主机上加 30 分、执行时段是凌晨加 10 分、发起账号是管理员组成员加 15 分。超过 50 分进审批,超过 80 分直接阻断并告警。这样比单条规则要柔性和智能得多。
另外,审计数据也可以用来做“月度安全报告”,每月自动汇总放行的高危命令、审批拒绝比例、单人高频操作列表。这个报告的价值不只是给安全团队看,还会变成团队内部操作规范改进的依据。如果有同事连续申请删除某类命令,说明应该有更合理的自动化途径去完成这件事,而不是反复人工审批。
最后再分享一个小技巧:审批中心的数据库早期我用的是本地 SQLite,数据量上来之后查询性能明显下降,后来换成了独立的关系数据库,性能完全不是问题。所以从一开始就按独立数据库设计,别把日志存储和审批库放一起,否则数据量大了之后,审批和审计互相拖慢,体验会很差。