Skip to content

feat(sandbox): Windows 沙箱提升模式——kn sandbox setup(专用沙箱账户、WFP 断网、按工作区隔离) - #165

Merged
xiaozhou26 merged 22 commits into
mainfrom
feat/windows-sandbox-elevated
Nov 5, 2026
Merged

xiaozhou26 merged 22 commits into
mainfrom
feat/windows-sandbox-elevated

Conversation

@xiaozhou26

@xiaozhou26 xiaozhou26 commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #164

概要

Windows 原生沙箱的"提升"级别:管理员批准一次 kn sandbox setup 后,命令以专用的本地沙箱账户运行(mechanism: "sandbox-user"),断网运行由 WFP 拦截(network_control: true),并且修正了 #145 中"用过的工作区对任何低完整性进程都可写"的局限。未 setup 时保留 #145 的 low-integrity 路径,断网运行继续 fail-closed,原因中提示 kn sandbox setup。

注:可执行文件名是 kn,所以命令、提示与文档统一写作 kn sandbox setup。

命令与接口

  • kn sandbox setup | uninstall | status(--status 等同 status,均可加 --json)。setup/uninstall 通过 ShellExecuteExW("runas") 以同一可执行文件和内部标记提权,进程已是管理员时直接执行;从不静默提权。拒绝 UAC(ERROR_CANCELLED)返回 ErrSetupDeclined,什么都不改。两者都可重复执行(setup 即修复)。
  • POST /v1/sandbox/setup(需要后端令牌):在后端所在用户的桌面上弹出 UAC,等待完成(最多 10 分钟,客户端断开不取消已批准的 setup),返回 {version,status,run_sandbox};403 declined、409 busy、501 unsupported、500 failed、405。每次调用让 capabilityRevision 加一。卸载只能在终端里执行。
  • 能力:run_sandbox 增加 setup_available(Windows 上为 true)与 setup_hint(英文短句;未安装时说明好处,安装但自检失败时说明问题并建议重新运行;正常时为空);capabilities.sandbox_setup: 1 已登记到 capabilities.json 与 docs/liveagent-backend.md。
  • sandbox.backend 接受 sandbox-user(未 setup 时报错,不退回);low-integrity 强制使用 feat(sandbox): Windows 原生按次沙箱(低完整性级别令牌 + 完整性标签,含读屏蔽) #145 的路径。

setup 对本机的改动

按 Windows 用户区分:名字取自用户 SID 的 SHA-256 前 6 个十六进制字符,每个用户一套,互不影响。

  • 本地标准用户 KBrainSbx-<key>-On(联网)与 KBrainSbx-<key>-Off(断网):32 字符随机密码、不可修改、不过期;SpecialAccounts\UserList 中隐藏;授予 SeInteractiveLogonRight(CreateProcessWithLogonW 需要),拒绝网络登录和远程桌面登录。
  • 本地组 KBrainSbx-<key>(两个账户,ACL 都授予它)与 KBrainSbx-<key>-Offline(只有断网账户)。
  • WFP:持久的提供者、子层(权重 0xFFFF)和 8 个阻止过滤器(v4/v6 的 connect、recv-accept、listen、resource-assignment),条件为 ALE_USER_ID 属于 -Offline 组;带 CLEAR_ACTION_RIGHT,其他子层的放行规则不能覆盖。安装在一个 WFP 事务中先删后加。
  • 状态文件 <数据目录>\sandbox\windows-sandbox-user.json:账户名、SID 和 DPAPI(本机范围、用户 SID 作附加熵)加密的密码。DACL 为受保护的 D:P(D;;FA;;;<组>)(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;<用户>),所有者设为用户;数据目录本身也被屏蔽。windows-sandbox-acl-roots.txt 记录添加过 ACE 的每个路径,供卸载使用。

为什么回环也阻止:本机很多服务只监听 127.0.0.1 且不验证调用者(开发服务器、数据库、K-brain 后端自身等),保留回环等于让断网命令可以借它们访问网络或用户数据。命令与 K-brain 之间只用标准输入输出和 job,不需要回环。已知例外:DNS Client、BITS 等代替调用者联网的系统服务不受用户 SID 过滤(断网命令能解析域名、不能连接),已写入局限。

运行路径

两阶段启动

CreateProcessWithLogonW 是无特权进程以另一账户启动进程的唯一方式,但命令行限 1024 字符、不接受属性列表,所以分两步:

  1. 第一阶段(用户身份的启动器):读取状态文件,用 DPAPI 解密密码;以不允许删除共享的方式锁定受保护路径;建 kill-on-close job;环境块 = 过滤后的 cmd.Env(feat(sandbox): 沙箱命令不继承凭据类环境变量 #157)+ 专用临时目录 + 每个可写根的 Git safe.directory(GIT_CONFIG_COUNT/KEY/VALUE;凭据过滤去掉了 GIT_CONFIG_KEY_n 时整组替换,避免 Git 报错)+ 第二阶段的规格(放在环境变量里以避开命令行限制)。以 CREATE_SUSPENDED|CREATE_UNICODE_ENVIRONMENT|CREATE_DEFAULT_ERROR_MODE、LOGON_WITH_NO_PROFILE 启动第二阶段,标准句柄继承,加入 job 后再恢复。第二阶段在启动器自己的桌面上启动:seclogon 只为这个桌面授权,在指定的其他桌面上第二阶段初始化会卡住或 DLL 初始化失败(CI 上实测)。不加载沙箱账户的用户配置,也不合并它的环境。
  2. 第二阶段(沙箱账户):把自己进程、线程和令牌默认 DACL 收紧到 SYSTEM 与本账户;生成命令的令牌,然后用与 feat(sandbox): Windows 原生按次沙箱(低完整性级别令牌 + 完整性标签,含读屏蔽) #145 相同的 runConfined 在 job 中启动命令(token 和 inheritDesktop 是 confinedProcess 新增的两个字段)。命令留在第一阶段的 job 里(带界面限制的 job 不能嵌套,CreateProcessAsUser 会返回 ERROR_NOT_SUPPORTED)。交互桌面(WinSta0)上,命令运行在第一阶段创建的私有桌面上。seclogon 会把调用者(用户)的登录会话 SID 放进沙箱账户的令牌(CI 上实测:两个账户的令牌都带用户的 S-1-5-5-0-x),用户桌面、WinSta0 和用户进程的默认 DACL 都授权给这个 SID;因此命令的令牌(联网和断网)都把它设为仅拒绝(CreateRestrictedToken 的 SidsToDisable),断网受限令牌的限制 SID 也不再包含它,新对象的默认 DACL 为 SYSTEM、账户和 RESTRICTED。测试 PrivateDesktop 验证命令运行在私有桌面上且打不开用户的桌面;私有桌面和窗口站授予账户 SID,断网运行另授予断网组 SID(受限令牌的第二次访问检查靠它通过);其他窗口站(服务、CI)沿用 seclogon 授予的桌面。

令牌与按工作区隔离

ACL

DACL 以原始 ACE 字节读写(未知类型的 ACE 原样保留,保留 PROTECTED/AUTO_INHERITED 控制位),按规范顺序插入:

  • 可写根与专用临时目录:顶层对组和工作区 SID 授予不含 DELETE 的修改权限(根不能被删、改名),另加 (OI)(CI)(IO) 修改权限,已有子项写入对应的继承 ACE。不继承 DACL 的子项(例如 Go 或某些工具创建的目录)写入显式的同等 ACE。授予遍历跳过有多个硬链接的文件和已有我们授权的子树(以顶层那条不含 DELETE、非继承的允许 ACE 为边界)。
  • 受保护路径(ProtectedPaths:.git/hooks、.git/config、core.hooksPath 等,feat(sandbox): 加固保护路径并隐藏运行时套接字 #149)与 readOnly:对组拒绝写、追加、写属性、删、改名、改权限、改所有者;不继承的子项也给显式 ACE,任何失败都拒绝运行。
  • 屏蔽目录:对组拒绝全部访问,可继承;与 feat(sandbox): Windows 原生按次沙箱(低完整性级别令牌 + 完整性标签,含读屏蔽) #145 相同,每 maskRecheck 重新遍历一次。屏蔽目录内的工作区保留自己的授权。
  • 读取:K-brain 自身、位于用户目录内的程序和 PATH 中位于用户目录内的目录授予组与读取 SID 读取/执行(跳过屏蔽目录、数据目录、WindowsApps 和用户目录本身)。
  • 用户目录的其余部分本来就不授予沙箱账户,因此不可读(low-integrity 下可读)。

自检

probeWindows 先尝试 sandbox-user:在临时目录里分别以两个账户运行内部自检(写工作区、写外部、读屏蔽文件、连本地回环监听),要求断网账户连不上、联网账户连得上。通过才报告 sandbox-user;状态缺失或自检失败时退回 low-integrity,setup_available: true,setup_hint 为安装提示或带具体问题的修复提示。

卸载

按顺序:对每个记录过的路径删除 SID 属于本 setup(合成 SID 前缀或两个组)的 ACE → 提权执行:删除 WFP 对象、撤销账户权限、删除用户配置文件与账户、删除登录界面隐藏值、删除组 → 删除状态文件和路径记录。"不存在"一律跳过,所以重复执行或部分安装后执行都安全。

共享文件改动

尽量小,Windows 逻辑都在 windows_*.go 和新文件里:

  • run.go:Capability 增加 SetupAvailable、SetupHint;Reprobe() 让 setup 之后重新探测(run_test.go 随之调整)。
  • windows.go / windows_launch.go(feat(sandbox): Windows 原生按次沙箱(低完整性级别令牌 + 完整性标签,含读屏蔽) #145 的文件):探测与分发;confinedProcess 增加 token、inheritDesktop。
  • config.go:接受 sandbox-user。
  • 后端:health.go、run_sandbox.go、server.go 各几行,新接口在 sandbox_setup.go;capabilityRevision 由常量改为函数。
  • cmd/kn/main.go:分发 sandbox 子命令(实现在 cmd/kn/sandbox.go)。

测试

  • 所有平台:setup_test.go(名字长度/确定性/命名空间、合成 SID 格式、GUID 版本位、提示文本、非 Windows 返回 ErrSetupUnsupported);sandbox_setup_test.go(令牌、405、200 + revision、403/501/500、409);cmd/kn/sandbox_test.go(参数解析、状态输出)。
  • Windows(无需管理员):WFP 结构体大小(FWPM_FILTER0 200 字节等)、gitSafeDirectoryEnv、DACL 插入顺序、在测试自有目录树上授予并移除合成 SID;TestWindowsCapability 隔离数据目录并检查 setup_available/setup_hint。
  • Windows 管理员完整流程 TestWindowsSandboxUserAdminCycle,只在工作流设置 KBRAIN_WINDOWS_ADMIN_TESTS=1 时运行(账户用 KBrainTst- 前缀,数据目录为临时目录,不触碰真实数据目录):卸载残留 → setup(两次,验证幂等,过滤器 8 个)→ 命令确实以两个沙箱账户的 SID 运行 → 联网:写工作区、不能写外部、curl.exe 与 PowerShell 7 Invoke-WebRequest 访问 HTTPS(Windows PowerShell 5.1 的结果只记录)、能连本地监听 → 命令运行在私有桌面、打不开用户的桌面 → 两种命令都打不开用户的中完整性进程(该进程使用标准用户进程的默认 DACL,授予登录 SID 读取/执行)→ 断网:外部 IP、127.0.0.1 与 [::1] 本地监听都连不上 → 第二个工作区对断网运行不可写、不可删,低完整性命令也写不了 → .git/hooks、.git/config 不可写、.git 不可改名 → 屏蔽文件与状态文件不可读 → 凭据类环境变量被过滤 → cmd、PowerShell、Git 可用 → 卸载(两次)后账户、组、过滤器、登录界面隐藏值、状态文件、所有记录路径上的 ACE 都不存在,断网运行重新 fail-closed。该步骤在测试被跳过时失败,之后另有一步用 Get-LocalUser/Get-LocalGroup/netsh wfp 检查残留。

CI(head 0641fb0):全部通过。Windows 管理员流程在 Sandbox (windows-amd64) 的 "Windows sandbox-user administrator cycle" 步骤中实际运行(--- PASS: TestWindowsSandboxUserAdminCycle,11 个子测试全部 PASS),"Windows leftovers after uninstall" 步骤输出 users: 0 groups: 0 filters: 0。

局限(已写入文档)

  • 联网运行之间不隔离工作区(Schannel 限制,见上)。
  • 不能在会话命名空间(Local\)里创建命名对象:会话的 BaseNamedObjects 只给登录 SID(及 SYSTEM、管理员)创建权限,而命令令牌里登录 SID 是仅拒绝的。保留它会让命令能读取用户进程的内存(标准用户进程的默认 DACL 授予登录 SID 读取),所以选择安全。已知受影响:Windows PowerShell 5.1 的 Invoke-WebRequest 创建默认代理失败;curl.exe、PowerShell 7、设置 DefaultWebProxy = $null 后的 5.1 都正常(CI 实测)。
  • DNS Client、BITS 等系统服务代为发出的网络请求不受过滤。
  • C:\ProgramData、C:\Users\Public 等 Users 组可写的位置对沙箱账户也可写。
  • ACE 在运行后保留以便复用,卸载时统一移除;首次在大工作区上运行需要逐个文件添加。

kn sandbox setup 经 UAC 为当前用户创建两个隐藏的本地沙箱账户(联网/断网)、
两个本地组和阻止断网账户全部连接(含回环)的持久 WFP 过滤器,密码以 DPAPI
加密保存在数据目录。setup 存在且自检通过时命令以沙箱账户运行:可写根与专用
临时目录授予沙箱组和每工作区合成 SID,受保护路径拒绝写入,屏蔽目录拒绝访问;
断网运行使用受限令牌,只能写本次运行的根。环境来自过滤后的 cmd.Env。
未 setup 时保留低完整性路径,断网仍 fail-closed 并提示 kn sandbox setup。

Refs #164
run_sandbox 增加 setup_available、setup_hint;capabilities.sandbox_setup: 1。
接口需要后端令牌,拒绝 UAC 返回 403 declined,并发返回 409 busy;
每次 setup 让 capabilityRevision 加一。

Refs #164
Go 等工具创建的目录带受保护的 DACL(不继承),授予遍历原先跳过它们,
使工作区的这些子目录对沙箱账户不可写。现在给它们完整掩码的显式条目,
并且只把根顶部"不含 DELETE 的修改"条目视为另一目标的边界,
受保护路径与屏蔽目录的拒绝遍历不会在这些子项处停下。

Refs #164
…s and document the session namespace limitation
@xiaozhou26
xiaozhou26 force-pushed the feat/windows-sandbox-elevated branch from edc7c56 to 0641fb0 Compare November 5, 2026 19:28
@xiaozhou26
xiaozhou26 merged commit 7db2a37 into main Nov 5, 2026
8 checks passed
@xiaozhou26
xiaozhou26 deleted the feat/windows-sandbox-elevated branch November 5, 2026 19:35
xiaozhou26 added a commit to Stack-Cairn/LiveAgent that referenced this pull request Nov 5, 2026
…1239)

K-brain can install its elevated Windows sandbox (two hidden local accounts
plus firewall rules for the offline one) behind a UAC prompt via
POST /v1/sandbox/setup (Stack-Cairn/K-brain#165). When health reports
sandbox_setup 1, run_sandbox.setup_available and no network control yet, the
desktop selector offers "Set up offline sandbox…" under the disabled entry.

A confirmation dialog lists what setup changes and that kn sandbox uninstall
reverts it; the dialog then waits on the administrator prompt and reports
each status (ready, self-test still failing, declined, busy, failed with
K-brain's message, unsupported, error). Every attempt the backend may have
acted on re-reads /v1/health, and the refreshed capability reaches every
mounted selector so Offline sandbox becomes selectable.

Desktop only and only for a K-brain on this machine; the WebUI adapter never
offers it. A sandbox without path protection stays unavailable.

Closes #1238
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows 沙箱提升模式:kn sandbox setup(专用沙箱账户 + WFP 断网 + 按工作区隔离)

1 participant