拆出软件名录(67 行)、清单语法(114 行)、归档布局与迁移(47 行)、安全描述符(69 行),共 297 行;README 从 653 降到 360 行,每节原位留一句摘要加链接。 为什么拆:这四块是「要查的资料」,README 剩下的是「要读的流程」。混在一起时,想查清单语法的人得先滚过一百多行总览;拆开后每篇也能被单独引用与单独评审。 搬运按标题抽取原文、不重打,并在写之前断言正文长度、写之后再读回断言一次(第一次尝试就是栽在没有断言上:@(a, b, $arr) 不会展开数组,而是把 $arr 拼成一行,结果四篇各只有 5 行、正文却已从 README 删除。那次已回滚)。
74 lines
4.7 KiB
Markdown
74 lines
4.7 KiB
Markdown
# 安全描述符(属主 / 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` 一起搬**。
|