Day 43 Hard Privacy GDPR 删除权 数据脱敏 审计追踪 同意管理

隐私与合规架构 — 当「删除」不再是 DELETEPrivacy & Compliance: Right to Erasure, Anonymization, Audit Trail, Consent

问题场景与约束

设计一个 5000 万用户的多租户 SaaS 数据平台,用户覆盖欧盟。用户点「删除我的账号」,你必须在 30 天内(GDPR Article 17)把他的 PII 从所有地方抹掉——但数据散落在 40 个微服务的 OLTP 库、Snowflake 数仓、Redis 缓存、Kafka 事件流、ES 索引,以及过去 90 天的每一份 DB 备份里。

为什么这是最反直觉的 System Design 题:分布式系统所有的设计习惯都在跟合规作对。append-only、不可变日志、多副本、事件溯源——全为「数据永不丢」;而 GDPR 要的恰是「让特定数据彻底消失并能证明」。约束还包括:删除须可验证、不能误删法定保留数据(税务/反欺诈有最短保留期)、脱敏数据不能被重识别、每次 PII 访问都要留痕。

高层架构

graph TD U[用户 DSAR 请求
删除/导出/撤回同意] --> ORCH[隐私编排器
DSAR Orchestrator] CAT[(PII 数据目录
Data Catalog / Registry)] -.定位数据.-> ORCH ORCH -->|fan-out 删除| SVC1[微服务 A] ORCH -->|fan-out 删除| SVC2[微服务 B ...N] ORCH -->|销毁密钥| KV[[Key Vault
KMS / crypto-shred]] KV -.持有 per-user DEK.-> DB[(加密数据存储)] ORCH -->|每步落证据| AUD[[审计日志
append-only 哈希链]] CG[同意网关
Consent Gate] -.拦截每次处理.-> SVC1 CG -.拦截每次处理.-> SVC2 classDef ctrl fill:#1a2530,stroke:#64c8ff,color:#e8eef5 classDef store fill:#2a1530,stroke:#ff7ab6,color:#e8eef5 classDef sec fill:#1a1a30,stroke:#ffb450,color:#e8eef5 class U,ORCH,CG ctrl class DB,CAT store class KV,AUD sec

编排器是「删除的事务协调者」;数据目录回答「PII 在哪」;密钥库让删除退化成删一把密钥;审计链证明「确实做了」

四个核心能力:①编排器把删除扇出成几十个子任务并追踪完成(本质是长事务 / Saga);②数据目录解决「PII 散在哪」这个最致命的问题;③密钥库 + crypto-shredding 把「在备份里物理删除」的不可能任务变成「删一把密钥」;④同意网关在每次处理前检查用户是否授权了该用途。

关键技术点

1. 删除权:Crypto-shredding vs 物理删除 vs Tombstone

原理:GDPR 的删除不要求物理擦除,只要求数据永久不可恢复。这打开了一条捷径——crypto-shredding(加密粉碎):给每个用户一把独立的数据加密密钥 DEK,用它加密该用户所有 PII;删除时只销毁这把 DEK,散落在 40 个库和 90 份备份里的密文瞬间全部变成无法解密的乱码,无需追杀每一份副本。欧盟数据保护委员会(前 Article 29 工作组)承认加密擦除是有效删除方式。

Trade-off:
# Crypto-shredding:删除退化成删一把密钥 (伪代码)
def write_pii(user_id, field, value):
    dek = kms.get_or_create_key(f"user:{user_id}")   # per-user DEK
    db.put(user_id, field, aes_encrypt(dek, value))   # 存的是密文

def read_pii(user_id, field):
    dek = kms.get_key(f"user:{user_id}")              # 密钥没了 → 抛异常 → 视为已删
    return aes_decrypt(dek, db.get(user_id, field))

def erase_user(user_id):
    kms.destroy_key(f"user:{user_id}")   # 唯一动作:备份/数仓/Kafka 里的密文全部报废
    audit.append("ERASE", user_id, proof=kms.key_destroyed_receipt(user_id))
现实案例:

2. 数据脱敏:伪匿名化 vs 匿名化(重识别是死穴)

原理:数仓要做分析、测试环境要仿真数据,但不能直接用真 PII。两条路径法律后果完全不同伪匿名化(pseudonymization)——把姓名换成 token、但保留一张可逆映射表——数据仍属于 PII,仍受 GDPR 全约束(映射表泄露即前功尽弃);匿名化(anonymization)——彻底切断与个人的关联、不可逆——才脱离 GDPR 管辖。难点是「彻底」几乎做不到:去掉姓名远远不够,准标识符(quasi-identifier)的组合能重识别——经典结论:邮编 + 生日 + 性别三者组合就能唯一定位美国绝大多数人。

Trade-off(隐私 vs 数据效用,此消彼长):
现实案例:

3. 审计追踪:不可篡改地证明「谁碰了 PII」

原理:合规不仅要「做对」,还要能举证做对了。审计日志记录每一次 PII 的访问 / 修改 / 删除 / 导出——谁、何时、为何、动了哪条。要求是 append-only + tamper-evident(防篡改可检测):即便 DBA 有 root,也不能悄悄改历史。核心技术是哈希链:每条日志带上「前一条的哈希」,形成链条,改中间任一条会让后续所有哈希对不上——和区块链同源思想(见 Day 36),但无需共识,单机 Merkle/哈希链即可。

# Tamper-evident 审计链 (伪代码)
def append_audit(actor, action, subject_id, purpose):
    prev = store.last().hash if store.last() else GENESIS
    entry = {actor, action, subject_id, purpose, ts, prev_hash: prev}
    entry.hash = sha256(canonical(entry))   # 含 prev_hash → 链式绑定
    store.append(entry)                      # WORM / 只追加介质
# 校验:从头重算每条 hash,任一处对不上 → 有人篡改过
Trade-off:哈希链抗篡改但只能追加(改不了=也删不了,与「删除权」天然冲突,见陷阱);WORM 存储(S3 Object Lock)靠介质保证不可改,简单但贵、保留期一到即失保护;外部锚定(周期性把链头哈希公证/上链)能防「内部人重建整条链」,但引入外部依赖。多数团队:哈希链 + WORM 冷存 + 定期离线快照,够用且可负担。
现实案例:

4. 同意管理:把「用户授权了什么用途」当作真相源

原理:GDPR 的核心是目的限制(purpose limitation)——为「发货」收集的地址不能拿去「精准广告」。所以同意不是注册时勾一次的 checkbox,而是一个 (用户 × 用途 × 状态 × 时间) 的真相源,且必须能实时传播到每个下游处理点:营销服务处理前先问「这个用户同意 marketing 了吗?」——像 policy / feature flag 一样在数据流里当门禁。撤回同意(withdraw)要像删除一样级联:不仅停止未来处理,已流入下游管道的数据也要按用途清理。

Trade-off:
现实案例:

扩展与优化

常见陷阱 + 面试问题

1. 备份里的数据怎么删? 这是压垮物理删除方案的问题——你无法 UPDATE 一份冷备份。标准答案:要么 crypto-shred(删密钥,备份里的密文报废);要么接受「备份按保留期自然滚动过期,期间用访问控制 + 记录『此用户已请求删除,恢复备份时须重新执行删除』」。面试官最爱追这个。
2. 审计日志不可篡改,那审计日志里的 PII 怎么删? 悖论:审计要 append-only,删除权要能抹除。解法是审计里只存引用不存 PII 明文(存 user_id 的假名 / 哈希 + 用途,不存姓名邮箱原文),或对审计中的 PII 同样 crypto-shred。别把 PII 明文写进不可删的链。
3. 伪匿名 ≠ 匿名。 大量团队把 token 化数据当「已脱敏」随意用于分析——只要映射表还在,法律上它仍是 PII,删除请求照样要覆盖它。这是审计中最常见的失分点。

深入资源

深入思考

Crypto-shredding 把「删数据」变成「删密钥」,但如果攻击者在你删密钥之前就拷走了密文,事后你还能声称「已删除」吗?
不能完全声称。crypto-shred 的保证是「此后无人(含你自己)能再解密」,防的是持续持有密文者(备份、数仓、离职员工的旧快照)未来解密。但它不能撤销已发生的泄露——若攻击者在密钥有效期内已解密导出明文,删密钥无济于事。这是 crypto-shredding 的边界:它是删除机制,不是泄露补救机制。所以它满足 Article 17(此后不可恢复),但不改变泄露仍需按 Article 33/34 上报的义务。两件事正交:删除保证「未来的不可访问性」,泄露关注「过去的已访问性」。
你为每个用户生成一把 DEK 做 crypto-shredding。5000 万用户就是 5000 万把密钥——KMS 撑得住吗?瓶颈在哪,怎么绕?
每次读写都调 KMS 解 5000 万把 DEK 会打爆 KMS。工程上用两层信封 + 缓存:KMS 里只存少量 KEK(主密钥),每用户 DEK 用 KEK 加密后和密文存在一起(wrapped DEK),读时本地缓存解开的 DEK(几秒 TTL),避免每次打 KMS。删除销毁的是「解开该用户 DEK 的能力」:若共享 KEK,则要删掉散在备份里的 wrapped DEK(又回到老问题);真正干净的是per-user KEK——删除=在 KMS 删这一把 KEK,O(1) 且不依赖追备份。代价是 KMS 要承载千万级密钥的生命周期,这正是云 KMS「密钥即资源」要解决的规模问题。题眼:把「密钥数量」(靠分层)和「KMS 调用频率」(靠缓存)两个维度分开优化。
审计日志既要「不可篡改」又要「能删其中的 PII」,看似矛盾。除了「日志里不存明文」,还有别的架构解法吗?
有,分层解耦:把审计拆成不可变的「事件骨架」+ 可删的「PII 载荷」。骨架(谁、何时、什么类型操作、指向载荷的引用哈希)进哈希链、永不可改,证明「发生过一次对 user X 的访问」;PII 载荷(具体值)单独存在可 crypto-shred 的存储里。删除时销毁载荷密钥,骨架完好、链不断、篡改仍可检测,但载荷已不可读——「审计完整性」由骨架保证,「删除权」由密钥销毁保证,各归其位。更进一步可用可编辑区块链(redactable,chameleon hash 允许授权方不破坏链验证地改某条),但复杂度陡增,多数场景「骨架+载荷分离」已足够。核心心法:两个约束冲突时,往往不是二选一,而是把对象拆成受不同约束的两部分
「删除即 Saga」跨 40 个服务,若第 37 个服务永久删不掉(比如一个已废弃、无人维护的系统还存着该用户数据),整个删除请求算成功还是失败?合规上怎么办?
合规上不能算成功——Article 17 要求删除「所有」副本,漏一处即未履行。但「僵尸系统」确实存在。正确处理分两层:技术上,编排器必须把该子任务标记 failed 并持续重试 + 告警,绝不能静默吞掉(最危险的是「39/40 成功就报完成」,第 40 处 PII 成了合规地雷);治理上,这暴露的是数据目录不完整 / 存在影子副本——根因是当初复制 PII 时没登记进目录。这也是 crypto-shredding 的战略价值:若这 40 个服务存的都是同一 per-user 密钥加密的密文,你根本不需每个服务配合——删中央那把密钥,僵尸系统里的密文自动报废。它把「N 个分布式删除必须全部成功」的 AND 难题,压成「删一把密钥」的单点操作。
差分隐私给了数学保证,为什么业界大量「匿名化」数据集仍被重识别?DP 没被普遍采用的真实障碍是什么?
因为大多数「匿名化」根本不是 DP,而是去标识 + k-匿名这类语法匿名,对「攻击者拥有外部辅助数据」毫无保证。经典翻车:Netflix Prize 数据集被 IMDb 公开评分交叉重识别。这些数据「删了姓名、做了 k-匿名」,但准标识符 + 外部知识组合仍能定位;DP 的强大正在于它对任意背景知识都成立。那为什么 DP 没普及?三个真实障碍:①效用损失——噪声让细粒度 / 小样本查询几乎不可用;②隐私预算 ε 会耗尽——同一数据反复查询每次泄一点,多团队共享时预算管理极难;③ε 取多少无普适答案,大了没保护、小了没数据。所以 DP 在聚合统计发布(普查、遥测)落地好,在行级细节分析里仍难替代。理解这个断层,比记住「DP 很安全」重要。