Files
BakNRet/docs/security-descriptor.md
Shuery 60ecbc2933 docs: README 拆出四篇主题文档,本文件留总览与操作
拆出软件名录(67 行)、清单语法(114 行)、归档布局与迁移(47 行)、安全描述符(69 行),共 297 行;README 从 653 降到 360 行,每节原位留一句摘要加链接。

为什么拆:这四块是「要查的资料」,README 剩下的是「要读的流程」。混在一起时,想查清单语法的人得先滚过一百多行总览;拆开后每篇也能被单独引用与单独评审。

搬运按标题抽取原文、不重打,并在写之前断言正文长度、写之后再读回断言一次(第一次尝试就是栽在没有断言上:@(a, b, $arr) 不会展开数组,而是把 $arr 拼成一行,结果四篇各只有 5 行、正文却已从 README 删除。那次已回滚)。
2026-09-27 10:20:08 +08:00

74 lines
4.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 安全描述符(属主 / ACL)
> 本文从 [README.md](../README.md) 拆出,单独成篇是为了让它能被单独引用与单独评审。
**问题**:归档格式装不下 NTFS 安全描述符 —— 7-Zip 的 `-sni`(Store NT security information)
官方文档写明「当前版本只能写进 WIM 归档」,`.7z` 里一个字节的 ACL 都没有。
于是"备份 → 恢复"之后,每个对象的安全描述符都是**新建对象的默认值**:属主是跑恢复脚本的
那个进程,DACL 是从目标父目录继承来的那一套。
**为什么这对 `C:\ProgramData` 是致命的**:那里的目录 ACL 里有
```text
(A;OICIIO;GA;;;CO) CREATOR OWNER + inherit-only + GENERIC_ALL
```
`CREATOR OWNER`(`S-1-3-0`)不是账户,是**访问检查时才替换的占位符** —— 替换成
"被检查对象的属主"。所以这句话的真实含义是「谁创建的东西谁有全权」。只回放 ACE 文本、
不恢复属主,等于把里面的"谁"换成了跑恢复脚本的账户,**原程序(服务账户 / 专用用户)
反而没了读写权限**。真机实测(`tools\lab\Lab.ps1 acl-test`):
```text
原属主 = S-1-5-18 (NT AUTHORITY\SYSTEM)
恢复后属主 = S-1-5-18 ← 正确恢复(要靠显式启用的 SeRestorePrivilege)
负对照属主 = S-1-5-32-544 ← 只搬文件、不回放安全描述符时,属主落到"跑脚本的账户"
```
**怎么做**:
- 备份时把每个对象的 SDDL(`Get-Acl` 的原文,含 `O:` / `G:` / `D:`)写进旁挂文件
`Backups/<归档名>.acl.json`,键是**归档内相对路径**(目标机器上 `%UserProfile%` 和
名录的前缀补全都会变,只有归档内路径两端同源)。
- SDDL 里的 SID 是**数值形式**,`CO` / `OW` 这类占位符原样保留。全程**不做账户名解析**
—— 名字解析会把占位符映射成当前用户,或者直接抛 `IdentityNotMappedException`,
那正是"权限落到脚本头上"的另一种成因。
- 恢复时在**解压之后**、对真实目标路径**自顶向下**回放:父目录先写,子对象的继承才收敛。
原本不 `protected` 的 DACL 只写显式 ACE,其余交给父目录重新继承(保住活继承语义);
`protected` 的原样写。
- 写属主要 `SeRestorePrivilege`,而且**必须显式启用**:管理员的过滤令牌里它默认是 disabled,
`Set-Acl` / `SetAccessControl` 都不会替你打开。所以**恢复要在管理员(或 SYSTEM)下跑**,
脚本启动时会明确告警"属主将无法恢复,只能恢复 DACL"。
- 写失败有三级回退:`属主+属组+DACL` → `属主+DACL` → `仅 DACL`(属组常常是最先失败的那个,
而它对访问判定几乎没影响,不能因为它把属主一起丢掉)。
- 对象的安全描述符读不到(系统目录里很常见)时**带错误记账**、写进 sidecar 并计入 manifest
的 `security.errors`,恢复时跳过它并告警 —— 而不是当成"这个对象没有特殊权限"。
配置在 `BackupConfig.psd1`:
```powershell
Security = @{
Mode = 'Full' # Off | Full | Smart | Roots
IncludeSacl = $false # 连审计规则(SACL)一起存取,需要 SeSecurityPrivilege
SidMap = @{} # 跨机恢复的 SID 映射:@{ 'S-1-5-21-旧' = 'S-1-5-21-新' }
FailOnError = $false # sidecar 写不出来时,是否把该条目算作失败
}
```
- `Full`(默认):每个对象都存。**正确性优先**,几万文件的树 sidecar 几 MB。
- `Smart`:只存"继承复现不出来"的对象(protected / 有显式 ACE / 属主属组与父目录不同 /
继承链已脱节)。判据偏保守,但终究是启发式,所以不是默认。
- `Roots`:只存每个归档项的根,最省。
- `Off`:完全不采集,恢复出来的就是新建对象的默认值。
`Restore.ps1` 另有 `-SkipSecurity` 可以只恢复文件内容。
**已知取舍(有意为之)**:
- 归档旁边没有 `acl.json` 的旧归档照常恢复,只是打一行告警说明"属主/ACL 是默认值"。
- **陈旧继承 ACE 会被"冻结"**:如果某个对象的 DACL 里留着已经没有任何出处的继承 ACE
(父目录改过权限、Windows 自己也不会再传播它),那它靠继承复现不出来,只能整套冻结成
显式 ACE **并置 protected** —— 这是唯一"既不丢 ACE、也不产生重复 ACE"的做法(实测:
目标上原本就留着那条陈旧 ACE,再补一条显式 ACE 会让同一条 ACE 出现两次)。
代价是这个对象从此不跟随父目录,而它本来就已经跟父目录脱节了。
- ACL 只跟着归档旁边的 `acl.json` 走:**搬归档时要把同名的 `.acl.json` 一起搬**。