# 安全描述符(属主 / 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` 一起搬**。