返回日报条目

资料节译

Meta以PrivacyLib在数据访问边界执行隐私检查

阅读原文

在 Meta,许多隐私要求归根结底都指向同一个问题:我们能否证明一条数据只会流向获准的地方?鉴于 Meta 的规模,以及手动隐私审计可能因系统变化而与实际情况脱节,我们开发了隐私感知基础设施(PAI),让基础设施本身能够回答这个问题。PAI 在 Meta 的基础设施中嵌入高效、可靠的一流隐私构件,让产品得以快速持续演进。

在之前的文章中,我们介绍了数据理解、数据沿袭和策略区域(我们的信息流控制技术)如何在 PAI 中发挥作用。现在,我们将深入探讨 PrivacyLib:这是一个通用库,运行在数百个数据与处理系统的客户端内部,在数据访问发生时执行检查。

PrivacyLib 在 PAI 整体工作流的最后阶段发挥重要作用:持续执行和监控数据流。

在 PrivacyLib 出现之前,我们针对每项要求,分别将隐私执行逻辑集成到各个数据与处理系统的客户端中。采用这种方式,在各系统中实现一项新要求可能需要数月。每项新要求,无论是用途限制还是数据流发现,都需要同一组能力:覆盖广泛的系统;以周而非月为单位部署;在每次数据访问时运行而不造成延迟问题;以及在不使整个服务集群停机的情况下部署执行逻辑变更。随着要求和系统不断演进,这种方式越来越难以扩展。

PrivacyLib 将系统集成与要求实现分开。每个系统只需集成一次。当某项要求可以在数据访问时满足,我们就将其实现为一个 PrivacyLib 模块,并在已完成集成的系统之间共享,因此新要求不再需要在每个系统中单独实现。

模块分为两类:

内部 AI 智能体成为一种新的数据访问方式后,我们还添加了智能体可观测性模块,用于观察与智能体相关的数据访问事件,并帮助保护智能体能够触及的数据。已集成 PrivacyLib 的系统无需额外集成工作即可启用该模块,这正是一次集成所带来的好处:每项新的隐私能力都以模块形式提供,而不是变成一个单独项目。

PrivacyLib 在 Meta 的多种数据访问路径中运行:

在这种规模下运行会带来三项工程挑战:

挑战解决方法
覆盖范围: 系统可能有多个客户端库和旧版集成,接受直接网络调用,并允许直接访问后端存储中的数据库。必须处理所有这些访问方式,才能确保无论通过哪条路径访问,要求都能得到满足。我们在两个层面进行集成:类型化客户端库,PrivacyLib 可在其中获取完整请求上下文;以及其余 RPC 路径的底层网络边界,以实现全面覆盖。每个已集成系统都会将其原生 API 映射到 PrivacyLib 的通用操作,并提供其已配置模块所需的输入。完成这些工作后,同一模块即可在多个系统中启用,无需针对每个系统单独集成。
规模: 每秒进行数十亿次检查时,即便每次检查只有少量计算成本,累积到整个服务集群也会造成显著影响。热路径上的每个服务都要承担这项成本。我们在构建时将要求编译成最简单的可评估形式,并通过进程内缓存处理大多数检查,因此常见路径无需网络调用。
可靠性: 隐私逻辑运行在关键系统的热路径中,而这些系统由按各自节奏发布的团队负责。变更必须能够独立测试并单独撤销,因为全服务集群回滚不能成为恢复手段。执行逻辑作为其所属服务的一部分进行部署,因此变更通过该服务自己的流水线进入生产环境,而不是通过面向整个服务集群的配置推送。

演示:保护消息元数据

为了说明这些机制如何在 Meta 的规模下协同工作,我们来看看处理消息元数据时必须满足的一项隐私要求。

用户通过 Messenger 等应用交换消息,该应用支持端到端加密。在路由和传送消息时,Messenger 的后端系统会在遵守适用法律的前提下处理非内容类消息数据(即消息元数据),例如消息发送或送达时间等信息,以提升产品性能、检测滥用行为,并保护用户免受有害行为侵害。

其中许多后端服务使用 Logger 这个客户端库,在服务进程内运行,并将这些元数据写入数据仓库表。写入记录之前,Logger 内部的 PrivacyLib 集成会调用强制执行模块策略区域,检查这些元数据是否可以流向目标表。这项检查分为两部分:作出判断需要哪些输入,以及运行时如何实际评估检查。接下来我们会分别考察这两部分,并介绍 PrivacyLib 如何跨系统扩展,以及如何在不中断相关服务的情况下部署变更。

Logger 的每个目标表都有一个 LoggerConfig,用来描述写入其中的记录。MetadataTraceLoggerConfig 定义了消息传送追踪的一项配置,并标记其中包含消息元数据:

final class MetadataTraceLoggerConfig extends LoggerConfigBase {
  use MessagingTrafficMetadataPolicyTrait;
  public function configure(
    LoggerFieldDefiner $fd,
  ): LoggerFieldDefiner {
    $this->annotateWithTableLevelPolicy();
    // Field and destination definitions
  }
}

一个日志目标可能有许多写入方,因此描述其输出数据的注解应属于目标本身,而不是各个写入服务。如果每个服务都单独声明该注解,各处声明可能不一致,也可能有新写入方完全漏掉注解。在 LoggerConfig 中只声明一次,就能让所有写入方使用相同的数据描述。

对于这个目标,MessagingTrafficMetadataPolicyTrait 提供 MESSAGING_TRAFFIC_METADATA 表级注解,而 annotateWithTableLevelPolicy() 会将其附加到该目标。

LoggerConfig 是 Logger 自己的模式定义,而每个系统都用自己的格式描述数据。为了让 PrivacyLib 使用统一格式,Logger 的代码生成流水线会将配置转换为兼容 PrivacyLib 的产物。生成的 MetadataTraceLoggerConfigPrivacyLibArtifact 源自 MetadataTraceLoggerConfig,并包含目标身份和消息注解:

/**
 * @codegen-source MetadataTraceLoggerConfig
 */
class MetadataTraceLoggerConfigPrivacyLibArtifact
  extends LoggerInputPrivacyLibArtifact {
  
  final public static function getAssetIDImpl(
  ): LoggerInputAssetID {
    return LoggerInputAssetID::get(
      'MetadataTraceLoggerConfig',
    );
  }
  protected static function getPurposePolicyVars(
  ): TPurposePolicyVars {
    return PurposePolicy::makeVars(
      TPurposePolicyName::MESSAGING_TRAFFIC_METADATA,
    );
  }
  // Other generated fields omitted.
}

上面的生成类由 Logger 在 Hack 客户端中使用;Hack 是 Meta 的类型化 PHP 语言。对于 C++ 和 Python 客户端,Logger 会将目标身份和注解发布到中央目录。PrivacyLib 使用这些信息生成特定语言的产物。在下一节的 C++ 示例中,PrivacyLib 会使用 LoggerConfig 名称加载该目标对应的 C++ 产物。

上一节的产物描述了一个 Logger 目标。PrivacyLib 还需要知道 Logger 写入操作的含义,以及应运行哪些模块。Logger 的 Hack 集成会为所有目标统一定义这些内容:

$config->libraryConfig()->enablePrivacyLib(
  PrivacyLibOperation::write()
    ->input(
      PrivacyLibInput::instance(
        nameof ILoggerInputPrivacyLibPayloadProvider,
      ),
    )
    ->enforcements(
      PrivacyLibWriteEnforcement::zones(
        ZoneDataType::LOGGER,
      )
        ->withPropagation()
        ->enabled(),
      PrivacyLibWriteEnforcement::telemetryLogging()->enabled(),
      // Other modules
    ),
);

PrivacyLibOperation::write() 为 Logger 声明了 PrivacyLib 的写入操作,enforcements() 则列出在该操作上运行的模块。策略区域模块可以影响检查结果,而遥测日志模块会记录评估情况但不影响结果。在这里添加模块,无需修改各个目标或 Logger 调用点。ZoneDataType::LOGGER 告知策略区域当前检查的是 Logger 目标。

在 C++ 服务中,Logger 会按如下方式构造 PrivacyLib 请求:

const auto assetList =
      std::vector<std::string>{configName};
PrivacyLibBuilder builder(
      "LOGGER_WRITE_CLIENT",
      assetList,
      LoggerWriteAPIEnumHelper::apiToHashedString(
          PolicyEnforcedLoggerWriteAPI::log),
      Caller::LOGGER,
      getLoggerWriteImplementation());
auto result =
      PrivacyLibRequest(std::move(builder)).dispatch();

PrivacyLib 使用 configName 加载目标的产物。经过哈希处理的 PolicyEnforcedLoggerWriteAPI::log 值将 Logger 的 API 映射到 PrivacyLib 的写入操作。其余构造器参数用于标识 Logger 是调用方,并提供执行写入的实现。dispatch() 会调用已配置的模块并记录其结果。对于策略区域模块,PrivacyLib 会提供生成产物中的目标注解以及当前处理上下文。在这个例子中,策略区域会评估两个条件:

  1. 当前上下文表示对消息元数据的使用已获准。
  2. 目标产物带有 MESSAGING_TRAFFIC_METADATA 注解。

启用强制执行后,只要任一条件不满足,策略区域模块就会拒绝写入。PrivacyLib 会将各强制执行模块的结果合并成一个响应。拒绝会阻止 Logger 将记录发送到下游。观察模块会发出遥测数据,但不影响响应。

PrivacyLib 通过通用执行模型、轻量级评估与遥测,以及对绕过已集成客户端的请求提供覆盖,让隐私检查能够跨异构系统扩展。

PrivacyLib 集成覆盖了 API 差异很大的系统:键值存储、图数据库、机器学习流水线、文件系统,以及 Logger 这样的日志系统。每项集成都将访问点映射到五种通用操作之一,PrivacyLib 会在每次操作时调用相应模块:

  1. 读取: 在获取数据时运行。
  2. 写入: 在存储数据时运行。
  3. RPC: 在调用另一项服务时运行。
  4. 请求入口: 在服务开始处理请求时运行一次。
  5. 内联执行: 在较大的请求中封装一项具体计算,例如评估某个特征。该计算期间发生的数据访问会使用其内联隐私上下文进行评估。

模块面向这五种操作编写,而不是面向每个系统的原生 API。读取、写入和 RPC 也有操作后变体,因此观察模块可以在访问决策作出后运行。系统提供映射后,所有兼容模块都可以复用,因此在已集成系统中启用额外模块只需几天。

PrivacyLib 运行在数据与处理系统的客户端内部,因为客户端已经拥有模块所需的目标身份和请求上下文。让评估在同一进程内进行,可以避免每次访问都跨进程传递这些输入。这种设计还有两项优势:

  1. 更丰富的上下文:PrivacyLib 可以在调用方身份、端点和用途信息仍可用时使用这些信息。如果在数据存储上进行检查,这些信息往往会丢失。
  2. 故障隔离:有问题的变更会被限制在运行该代码版本的服务中,使共享数据存储不受故障影响。

由于进程内评估会在每次数据访问时运行,因此必须具备较高的计算效率。每请求缓存可以避免在同一请求中重复检查,每主机缓存则会保留常用配置和结果。这样就无需像早期设计那样远程获取配置(仅这项操作就比进程内路径慢数千倍)。因此,PrivacyLib 能以较低的单次检查延迟,在整个服务集群中每秒执行数十亿次检查,同时只消耗 Meta 总计算能力的一小部分。

在 PrivacyLib 这样的规模下,遥测也必须保持轻量。PrivacyLib 会记录每个模块的状态、抽样记录延迟并记录错误,让我们能够全面了解生产环境中的执行情况。若完整记录每次调用,会让接收这些事件的系统不堪重负,因此目标是在让罕见事件保持可见的同时,对重复事件进行去重。

两种机制协同实现这一点:每台主机上的布隆过滤器以固定比例对高频模式进行降采样,细粒度抽样率则会随流量变化进行调整。

并非所有调用方都通过集成 PrivacyLib 的客户端库访问数据;有些会直接发出原始 RPC。RPC 层仍可检查这些请求,但它能看到的上下文远少于客户端。PrivacyLib 使用经过加密签名的令牌弥补这一差距:客户端侧检查成功后,PrivacyLib 会将令牌附加到请求上;ServiceRouter 会在受支持的 RPC 流量的底层边界验证令牌,确认请求已经通过信息更完整的客户端侧检查。

如果请求到达 RPC 层时没有令牌,ServiceRouter 就会退回到一种较粗略的检查,只依据网络负载中可见的上下文。由于该上下文有限,底层会采用最严格的解释,宁可产生误报,也不留下覆盖缺口。两层机制共同覆盖通过 Meta 管理的客户端和受支持 RPC 路径进行的访问。这些边界之外的路径不受该机制覆盖,需要单独集成 PrivacyLib。

综合来看,只要可行,评估就会在本地以较低计算成本完成;无法本地评估时,则由较粗略的后备检查兜底。模块也有统一的运行位置,可以跨越原本互不相通的系统。接下来要解决的是更棘手的运维问题:如何改变执行行为,同时不破坏线上流量或作出错误的访问决策。

隐私要求会随时间变化。修改目标注解或强制执行模块,都可能改变某项写入是否获准。PrivacyLib 为这两类变更都提供了安全的发布流程。

添加或移除注解只需修改 LoggerConfig 中的两行:导入 MessagingTrafficMetadataPolicyTrait 的声明,以及调用 annotateWithTableLevelPolicy() 的代码。由于一个目标可能有许多写入方,这两行改动可能同时开始阻止许多服务的写入。

因此,审核流程会向作者和审查者展示这项修改的实际影响。在代码审查期间,Logger 的代码生成流水线会更新目标的 PrivacyLib 产物。随后,模拟会将观测到的数据流分别对照新旧产物进行回放,并报告结果发生变化的流。源代码修改、生成产物和模拟结果会一同审查,让决策依据变更影响的证据,而不只是阅读代码改动。如果模拟报告了意料之外的变化,审查者会在发布前暂缓该产物。

批准后,产物会通过服务各自惯用的构建与部署流水线到达各项服务。这样,在变更部署之前就有两种可见信息:模拟报告的决策变化,以及准确展示待部署内容的生成代码;同时也有一项控制措施:由每项服务自己的部署决定变更何时进入生产环境。这也限定了影响范围:部署后发现的问题只会影响运行该构建版本的服务。

缺点是变更不会同时到达整个服务集群,因此传播速度比全局配置更新慢。实际上,要求负责人会通过明确的跟踪机制,在规定期限内更新范围内的服务;需要更快更新的服务则可以使用紧急部署流程。

注解变更只影响一个 Logger 目标。添加到 Logger 写入路径的模块则会对每个 Logger 目标运行。因此,我们会先部署模块,再将其启用到全部流量。

以策略区域在这条路径上的引入为例。我们首先在 Logger 的 PrivacyLib 写入操作配置中,将策略区域模块设为 rollingOut():

PrivacyLibWriteEnforcement::zones(ZoneDataType::LOGGER)
    ->rollingOut()

发布键 logger.input.write.zones 标识了 Logger 输入、写入操作和策略区域模块的这一组合。rollingOut() 会让模块执行受运行时控制。随后,我们分阶段将该控制从 0% 调高到 100%,同时监控拒绝次数、错误和延迟。如果有效写入开始失败,我们可以降低百分比,无需重新构建或重新部署服务。遥测日志模块因独立配置而继续运行。

策略区域模块在 Logger 流量中稳定运行后,我们将配置从 rollingOut() 改为 enabled()。此时,Logger 集成会直接运行该模块,不再查询发布百分比。

上文描述的两条变更路径在不同阶段分别采用防护措施。如果模拟显示数据流发生意外变化,我们会在发布前暂缓注解产物。如果模块在发布开始后运行异常,我们会通过运行时控制降低其发布百分比。

这些安全机制改变了在 Meta 满足隐私要求所需的方式。如今,PrivacyLib 运行在数百个数据与处理系统的客户端内部,每秒评估数十亿次检查,同时仅消耗 Meta 计算能力的一小部分。基于它构建的要求,例如通过策略区域实施用途限制,以及通过隐私探针发现数据流,都能复用这些集成,无需重新构建;因此,在已覆盖的系统中启用模块只需几天,而定制集成过去需要数月。如果变更出现问题,故障会限制在运行已部署变更的服务中。如今,我们可以基于这一基础支持一类更新的工作负载:AI 智能体。

AI 智能体比传统服务更难推理,因为其行为是动态的,访问哪些数据要到运行时才确定。因此,我们必须从一开始就为智能体安全做好设计:不仅要对构建智能体时预期它会访问的数据实施控制(例如策略区域),还要对它们运行时尝试访问的数据实施控制。在启用这些控制的路径上,它们可以在返回任何数据之前阻止未获准的访问。

智能体通过调用工具来执行任务。一次工具调用可能以嵌套方式经过多个服务,最终才到达数据。为应对这一新挑战,我们基于 PrivacyLib 的设计原则构建了 AgentLib,这是一个用于观察和管理内部智能体工具调用的通用运行时。在受支持的路径上,AgentLib 会将每次工具调用关联到智能体会话,下游客户端和服务会持续传递该上下文,直到调用到达集成了 PrivacyLib 的系统。在数据访问边界上,PrivacyLib 会结合该上下文、请求的操作和目标注解,再将它们传递给各个模块作出最终决策。

在 PrivacyLib 内部,智能体可观测性模块会记录智能体会话和工具调用标识符,以及目标、客户端 API、集成点和任何错误。工程师可以按会话查询这些事件,以调查工具调用涉及的服务和目标。引入智能体可观测性模块时,PrivacyLib 原有的广泛覆盖使我们可以为已集成该库的系统添加监测,而无需手动将模块集成到数百个系统中。即使借助 AI 辅助开发,这项任务也会十分艰巨。

为了执行某些策略,我们直接使用现有的策略区域模块,因为它依赖的执行输入相同。在启用了这项控制的路径上,该模块会在允许操作前检查目标注解和智能体上下文。策略区域阻止操作时,PrivacyLib 会沿调用链将结果传回智能体环境。即使智能体选择了开发者未预料到的工具,策略区域也会执行这项检查。访问决策不依赖智能体是否按预期行事。

在 Meta 的各个系统中构建和运行 PrivacyLib,让我们总结出几条重要经验。

为不断演进的系统和要求而设计: 在 PrivacyLib 出现之前,我们会在满足每项新要求所需的各个调用点添加隐私控制。随着时间推移,Meta 构建了新的数据与处理系统,服务获得了新的数据处理方式,隐私要求也不断扩展。每次变化都会增加集成工作,有时新的访问路径出现得比我们的覆盖速度还快。经验是,应在稳定的数据访问边界进行集成,定义系统共用的一小组操作,并让注解与数据保持关联。模块可以复用相同的集成,从而逐步减少工程投入。

可靠性赢得采用: 早期,一个无效配置曾导致整个服务集群中的进程崩溃,违背了我们自己的安全发布原则。另一些事故则源于注解修改虽然编译成功,却改变了访问决策并导致产品故障。这些事故表明,只有当服务负责人能够在发布前看到变更对其流量的影响,并且无需全服务集群回滚就能从问题变更中恢复时,他们才会信任 PrivacyLib 进入关键的数据访问路径。

覆盖范围也包括部署: 新要求需要修改模块、受支持的策略或注解时,旧版服务也需要部署具备更新后 PrivacyLib 能力的版本。我们已建立明确的清单、跟踪和发布机制来协调这些部署,使要求负责人能够安全地只更新范围内的服务,按时满足要求。

这些经验指引着 PrivacyLib 下一步的发展方向。随着 AI 将数据访问带入越来越新颖的环境和调用路径,我们会继续增强 PrivacyLib 在两个方面的能力:

  1. AI 训练: 用于训练的数据会从多种环境进入我们的系统,其中许多数据会在不同地点生成或转换。我们正将 PrivacyLib 式的方法扩展到这一领域,让相同的保障能够随之扩展。关于 Meta 如何处理 AI 训练数据,详见Meta 的 AI:训练数据透明度。
  2. 内部 AI 智能体: 对 Meta 内部用于工程和提高效率的智能体而言,观察其尝试访问的数据并阻止不被允许的访问,只是起点,远非完整方案。更难的问题还在后面:在内部智能体运行时评估其行为,并推理这些智能体之间的通信;在这种情况下,单次数据访问的发起者和智能体之间可能隔着多个智能体。

作者谨此感谢多位现任和前任 Meta 员工,他们多年来为 PrivacyLib 的开发作出了关键贡献。我们特别感谢(按字母顺序排列)Adwait Sathe、Andrew Vaslas、Darrien Glasser、Diana Marsala、Hanzhi Zhang、Kousik Nandy、Matthew Hillman、Matthieu Martin、Michael Dietz、Nathan Maue、Ramy Wassef、Sen Luo、Spencer Baumgardner、Vince Yang 和 Zhong Hu。我们还要感谢本文的所有审阅者,包括(按字母顺序排列)Aleksandar Ilic、Alex Kagan、Alex Ponomarenko、Arash Nikkar、Avtar Brar、Chloe Lu、Chris Wiltz、Emil Vazquez、Emily Van Deuren、Rituraj Kirti、Sushaant Mujoo 和 Wenlong Dong。我们尤其感谢 Jonathan Bergeron 负责统筹这项工作并提供指导和宝贵反馈,也感谢 Chloe Lu 协调所需支持,让这篇博文得以发布。

文章《PrivacyLib:数据访问边界处的隐私控制》最初发表于 Meta 工程。