This work is licensed under a Creative Commons Attribution 3.0
Unported License.
http://creativecommons.org/licenses/by/3.0/legalcode
为了更好地保护集群中的数据,Swift 操作员可能希望以加密形式存储对象。本规范描述了一个计划,为 Swift 添加由操作员管理的加密功能,同时对客户端保持完全透明。
Swift 对象通常以标准 POSIX 文件系统的文件形式存储在磁盘上;在典型的 3 副本情况下,一个对象表示为集群中 3 个不同文件系统上的 3 个文件。
攻击者可以通过多种方式访问磁盘。当磁盘发生故障时,它可能会在保修期内退回给制造商;由于它已发生故障,擦除数据可能不可行,但数据可能仍然存在于盘片上。当磁盘达到使用寿命时,它们会被丢弃,如果未正确擦除,可能仍然包含数据。内部人员可能会从数据中心窃取或克隆磁盘。
目标 1:攻击者获得对 Swift 对象服务器文件系统的读取访问权限后,应尽可能获得较少有用的数据。这为用户的提供数据保密性。
目标 2:当密钥管理实现允许安全删除密钥时,删除对象的密钥应使对象无法恢复。这提供了一种安全删除对象的方法。
还有其他攻击 Swift 集群的方法,但本规范不涉及这些方法。特别是,本规范不涉及以下威胁
- 攻击者访问 Swift 的内部网络
- 攻击者破坏密钥数据库
- 攻击者修改 Swift 的代码(在 Swift 节点上)以进行恶意行为
如果减轻了这些威胁,那将是幸运的副作用,但本规范的意图不是解决这些问题。
At-Rest 加密有两个逻辑部分。第一部分是加密引擎;它执行数据的实际加密和解密以及元数据。
第二部分是密钥管理。这是存储、检索和提供密钥材料给加密引擎的过程。该过程可以拆分,其中一个代理负责安全地存储密钥材料(有时是硬件安全模块),另一个代理负责为加密引擎检索密钥材料。Swift 将通过 Python 的 entry-points 机制支持各种密钥材料检索器,称为“keymasters”。通常,Swift 集群将只使用一个 keymaster。
加密引擎和 keymaster 将实现为三个独立的中间件。加密引擎将具有“decrypter”和“encrypter”过滤器工厂函数,并且 keymaster 过滤器将位于它们之间。示例
[pipeline:main]
pipeline = catch_errors gatekeeper ... decrypter keymaster encrypter proxy-logging proxy-server
encrypter 中间件负责在 PUT 或 POST 请求上加密对象的数据和元数据。
decrypter 中间件负责三件事。首先,它在对象 GET 或 HEAD 响应上解密对象的数据和元数据。其次,它在容器 GET 或 HEAD 响应上解密容器列表条目和容器元数据。第三,它在帐户 GET 或 HEAD 响应上解密帐户元数据。
DELETE 请求不受加密影响,因此 encrypter 或 decrypter 不需要执行任何操作。keymaster 可能会删除与已删除实体关联的任何密钥或密钥。
OPTIONS 请求应完全被加密引擎忽略,因为 OPTIONS 请求和响应不包含用户数据或用户元数据。
在 Swift 中,大型对象由段组成,这些段是普通的旧对象,以及一个清单,它是一个将段连接在一起的特殊对象。这里,“特殊”意味着“具有特定的头部值”。
大型对象支持在中间件(“dlo”和“slo”)中实现。encrypter/keymaster/decrypter 三元组必须放置在代理的中间件管道中 dlo 和 slo 中间件的右侧。这样,encrypter 和 decrypter 不需要对大型对象执行任何特殊处理;相反,每个请求都是针对普通对象、容器或帐户的。
对于未加密的对象,对象服务器负责验证客户端在 PUT 请求上发送的任何 Etag 头部;Etag 头部的值是上传对象数据的 MD5 哈希值。
对于加密对象,明文不可用,因此 encrypter 必须通过计算对象数据的 MD5 哈希值并将其与客户端发送的任何 Etag 头部进行验证来执行验证 - 如果两者不匹配,则 encrypter 应立即返回状态为 422 的响应。
假设计算出的明文 MD5 哈希值已通过验证,encrypter 将加密该值并传递给对象服务器以将其存储为系统元数据。由于验证值在完全读取明文流之前不可用,因此将使用“请求页脚”发送此元数据,如第 7.2 节所述。
如果客户端请求包含 Etag 头部,则 encrypter 还应计算密文的 MD5 哈希值,并将此值包含在 Etag 请求页脚中。这将允许对象服务器验证收到的密文的哈希值,从而完成客户端发送 Etag 所暗示的端到端验证要求:encrypter 验证客户端到代理的通信,对象服务器验证代理到对象服务器的通信。
keymaster 负责确定是否应加密任何特定资源。此决定取决于实现,但可能基于容器策略或帐户名称。当资源不应加密时,keymaster 将在请求 environ 中设置密钥 swift.crypto.override,以指示 encrypter 中间件不需要加密。
如果需要加密,keymaster 通过将零参数可调用对象放置在 WSGI 环境字典中的键“swift.crypto.fetch_crypto_keys”处来将加密密钥传达给 encrypter 和 decrypter 中间件。调用时,它将返回处理当前请求所需的密钥。它必须存在于包含任何加密数据或元数据的帐户、容器或对象的任何 GET 或 HEAD 请求上。如果在处理 GET 或 HEAD 请求时遇到加密数据或元数据,但 fetch_crypto_keys 不存在 _或_ 调用时未返回密钥,则这是一个错误,客户端将收到 500 系列响应。
在 PUT 或 POST 请求上,keymaster 必须在请求处理期间将“swift.crypto.fetch_crypto_keys”放置在 WSGI 环境中;也就是说,在将请求传递给其余中间件管道之前。这样,encrypter 才能以流式方式加密对象的数据,而无需缓冲整个对象。
在 GET 或 HEAD 请求上,keymaster 必须在返回控制给 decrypter 之前将“swift.crypto.fetch_crypto_keys”放置在 WSGI 环境中。不必在请求处理时完成。这允许将密钥的属性存储在 sysmeta 中,例如外部数据库中的密钥 ID,或 keymaster 想要的任何其他内容。
Swift 将使用 CTR 模式下的 AES,密钥长度为 256 位。
为了允许范围 GET 请求,密码应以计数器 (CTR) 模式使用。
整个对象体应加密为单个字节流。用于加密对象体的初始化向量 (IV) 将随机生成并存储在系统元数据中。
CTR 模式基本上将分组密码转换为流密码,因此处理范围 GET 请求变得更加容易。无需修改客户端请求的字节范围。在解密时,为了将请求的数据与 AES 的 16 字节块大小对齐,需要进行一些填充,但所有这些都可以在代理级别完成。
请记住,当发出 GET 请求时,decrypter 不了解对象。对象可能已加密,也可能未加密;对象可能存在,也可能不存在。如果 Swift 允许可配置的密码模式,则必须扩展请求的字节范围才能获得任何受支持的密码模式所需的足够字节数,这意味着要考虑每个受支持的密码/块大小/模式的块大小和运行特性。除了网络开销(尤其是对于小的字节范围)之外,由此产生的代码的复杂性将使其成为错误的理想场所。
密码和模式将存储在每个加密对象的系统元数据中。这样,当 Swift 支持其他密码或模式时,仍然可以解密现有对象。
通常,我们必须假设 Swift 集群中的任何资源(帐户/容器/对象元数据或对象数据)都可能使用不同的密码加密,或者未加密。因此,必须将密码选择存储为每个加密资源的元数据,以及 IV。由于用户元数据可以独立于对象进行更新,这意味着存储与元数据相关的加密元数据。
如果 keymaster 未将“swift.crypto.fetch_crypto_keys”添加到 GET 请求的 WSGI 环境中,则客户端将收到对象的密文而不是明文,这看起来像是垃圾。但是,我们可以通过系统元数据头部是否存在来判断对象是否已加密,因此 decrypter 可以通过在未提供用于解密加密对象的密钥时引发错误来防止这种情况。
就像 Swift 支持多个并发身份验证系统一样,它可以支持多个并发 keymaster。对于身份验证,每个身份验证系统通过查看以其经销商前缀开头的帐户来声明 Swift 命名空间的子集。类似地,多个 keymaster 可以以某种方式划分 Swift 命名空间,从而和平共处。
Swift 需要一个琐碎的 keymaster 来进行加密引擎的功能测试。琐碎的 keymaster 绝对不适合生产使用。为此,应故意保持尽可能小,而无需考虑密钥的实际安全性。
也许琐碎的 keymaster 可以使用可配置前缀与对象的完整路径的 SHA-256 作为加密密钥。也就是说,
key = SHA256(prefix_from_conf + request.path)
这将允许测试 PUT 和 GET 路径、COPY 路径(目标对象的密钥与源对象不同),以及无效密钥路径(在对象 PUT 之后更改前缀)。
Swift 可能会希望在某个时候将内容存储在 Barbican 中的 keymaster。
如上所述,Swift 将支持各种 keymaster 实现,并且任何 keymaster 的实现细节超出了本规范的范围(除了提供一个用于测试的琐碎的 keymaster)。但是,我们在此包含一个关于 keymaster 可能如何表现的 _参考_ 讨论,特别是关于管理何时加密资源(或不加密)的方面。
keymaster 最终负责指定是否应加密资源。做出此决定的方法取决于实现,但可能基于容器策略或帐户名称。当资源不应加密时,keymaster 将在请求 environ 中设置密钥 swift.crypto.override,如上所述,以指示 encrypter 中间件不需要加密。(唯一的例外可能是 decrypter 在头部中找不到加密元数据,并假定对象从未加密的情况。)
如果考虑对象加密(而不是帐户或容器元数据),keymaster 可能会选择基于每个帐户、每个容器或每个对象对对象进行加密。如果加密是按帐户或容器指定的,keymaster 可能会根据其自身(或某些其他代理)先前设置在帐户或容器上的元数据做出决策。例如
- 管理员或用户可能在创建帐户时将 keymaster 特定的系统元数据添加到帐户中;
- keymaster 可能会检查容器元数据以获取存储策略索引,然后将其映射到加密/不加密决策;
- keymaster 可能会接受客户端提供的启用/禁用加密的头部,并将其转换为随后检查该资源的系统元数据。
如果加密是按对象指定的,则决策可能基于对象名称或基于客户端提供的头部。
keymaster 还负责指定在要加密/解密资源时使用的 _哪个密钥_。同样,如果我们专注于对象加密,keymaster 可以选择为每个对象使用唯一的密钥,或者为同一容器中的所有对象使用唯一的密钥,或者为同一帐户中的所有对象使用唯一的密钥(不禁止对整个集群使用单个密钥,但不建议这样做)。下面加密元数据存储的规范足够灵活,可以支持所有这些选择。
如果 keymaster 选择为每个对象指定一个唯一的密钥,那么它将清楚地需要能够管理集群中与对象一样多的密钥。为了提高性能,它还应能够在需要时及时检索任何对象的密钥。keymaster _可能_ 选择将加密的密钥存储在 Swift 本身中:例如,对象的唯一密钥可以使用其容器密钥进行加密,然后可能存储为对象元数据。但是,虽然可扩展,但这种解决方案可能无法提供“密钥安全删除”所需的属性,因为在 Swift 中删除对象并不能保证立即删除磁盘上的内容。
为了说明,考虑一个代号为 Vinz 的 _假设_ keymaster 实现。Vinz 基于容器对对象进行加密
- 对于每个对象 PUT,Vinz 检查目标容器的元数据以发现容器的存储策略。
- 然后,Vinz 使用存储策略作为其自身加密策略配置中的一个键。
- 使用存储策略“gold”或“silver”的容器已加密,使用存储策略“bronze”的容器未加密。
- 重要的是,存储策略到加密策略的映射是 keymaster 独有的属性,可以根据需要更改。
- Vinz 还检查帐户元数据,查找系统管理员可能设置的元数据项 ‘X-Account-Sysmeta-Vinz-Encrypt: always’。如果存在,Vinz 将指定对象加密,而不管容器策略如何。
- 对于要加密/解密的的对象,Vinz 添加变量swift.crypto.fetch_crypto_keys=vinz_fetch_crypto_keys到请求环境。Vinz 还与 Barbican 交互以获取对象的容器的密钥,并在调用时提供该密钥vinz_fetch_crypto_keys.
- 对于不需要加密/解密的对象,Vinz 添加变量swift.crypto.override=True到请求环境。
每个对象都使用密钥管理器的密钥进行加密。加密器为每个对象体随机生成一个新的 IV。
IV 和密码的选择使用 sysmeta 存储。在以下讨论中,我们将密码的选择和 IV 统称为“加密元数据”。
对象体的加密元数据可以作为加密器添加到对象 PUT 请求头部的 sysmeta 项存储,例如:
X-Object-Sysmeta-Crypto-Meta: "{'iv': 'xxx', 'cipher': 'AES_CTR_256'}"
注意
这里,以及以下示例中,可以省略'cipher'键控的元数据项,直到未来的更改引入替代密码。加密元数据的存在足以推断使用 ‘AES_CTR_256’,除非另有说明。
Swift 实体(帐户、容器和对象)有三种类型的元数据。
首先,有基本的对象元数据,如 Content-Length、Content-Type 和 Etag。这些始终存在且用户可见。
其次,有用户元数据。这些是对象、容器和帐户上的以 X-Object-Meta-、X-Container-Meta- 或 X-Account-Meta- 开头的头部。每个实体都有用户元数据的数量、单个大小和总大小的限制。用户元数据是可选的;如果存在,则用户可见。
第三,也是最后,有系统元数据,通常缩写为“sysmeta”。这些是对象、容器和帐户上的以 X-Object-Sysmeta-、X-Container-Sysmeta- 和 X-Account-Sysmeta- 开头的头部。系统元数据的数量或总大小没有限制,但由于 HTTP 头部长度限制,单个数据项的大小可能有限制。系统元数据对用户不可见或可设置;它旨在由 Swift 中间件安全地存储数据,远离用户的窥探和操作。
对象的明文 etag 和 content type 是敏感信息,将在容器列表和对象的元数据中存储加密。为了实现这一点,加密器中间件实际上会加密 etag 和 content type _两次_:一次使用对象的密钥,另一次使用容器的密钥。
必须对每个不同的加密头部使用不同的 IV。因此,将为 etag 和 content_type 存储加密元数据
X-Object-Sysmeta-Crypto-Meta-ct: "{'iv': 'xxx', 'cipher': 'AES_CTR_256'}"
X-Object-Sysmeta-Crypto-Meta-Etag: "{'iv': 'xxx', 'cipher': 'AES_CTR_256'}"
对象密钥加密的值将使用X-Object-Sysmeta-Crypto-Etag和Content-Type头部发送到对象服务器,这些头部将存储在对象的元数据中。
容器密钥加密的 etag 和 content-type 值将使用头部名称发送到对象服务器X-Backend-Container-Update-Override-Etag和X-Backend-Container-Update-Override-Content-Type分别。现有的对象服务器行为是然后使用这些值在X-Etag和X-Content-Type头部中包含在发送到容器服务器的容器更新中。
在处理容器 GET 请求时,解密器必须处理容器列表并使用容器密钥解密每个 Etag 或 Content-Type 的出现。在处理对象 GET 或 HEAD 时,解密器必须解密X-Object-Sysmeta-Crypto-Etag和X-Object-Sysmeta-Crypto-Content-Type使用对象密钥的值,并将这些值复制到Etag和Content-Type返回给客户端的头部。
这样,客户端在容器列表和对象 GET 或 HEAD 响应中看到明文 etag 和 content type,就像没有启用加密一样,但这些明文值没有存储在任何地方。
注意
加密器在处理所有对象内容之前无法知道明文 etag 的值。因此,除非加密器缓冲整个对象密文(!),否则它无法在请求主体之前将加密的 etag 头部发送到对象服务器。相反,加密器将发出一个 multipart MIME 文档作为请求主体,并将加密的 etag 作为“请求页脚”附加。此机制将基于 Erasure Coding 功能 [1] 中引入的对象服务器请求中 multipart MIME 主体的用法。
对于加密的基本对象元数据(即 etag 和 content-type),对象数据加密元数据将适用,因为此基本元数据仅由对象 PUT 设置。但是,转发到容器服务器的容器更新中的基本对象元数据的加密副本也需要将加密元数据存储在容器服务器 DB 对象表中。为了避免容器服务器中的大量代码更改,我们建议将加密元数据附加到基本元数据值字符串。
例如,包含在容器更新中的 Etag 头部值将具有以下形式
Etag: E(CEK, <etag>); meta={'iv': 'xxx', 'cipher': 'AES_CTR_256'}
其中E(CEK, <etag>)是使用容器密钥 (CEK).
加密的对象 etag 的密文。
在处理容器 GET 列表时,解密器需要解析列表返回自容器服务器的每个 etag 值,并将其值转换为响应客户端的预期明文 etag。由于“常规”明文 etag 是一个固定长度的字符串,不能包含 ‘;’ 字符,因此解密器能够轻松区分未加密的 etag 值和始终比明文 etag 更长的带有附加加密元数据的 etag 值。附加到容器更新 etag 的加密元数据也适用于加密的 content-typeE(CEK, <content-type>)
因为两者是同时设置的。但是,其他提议的工作 [2] 使得可以使用 POST 更新对象 content-type,这意味着与 etag 关联的加密元数据可能与 content-type 关联的加密元数据不同。因此,我们建议类似地附加 content-type 值中将要发送到容器服务器的加密元数据
Content-Type: E(CEK, <content-type>); meta=”{‘iv’: ‘yyy’, ‘cipher’: ‘AES_CTR_256’}”
7.2.1 关于 Etag 的说明¶
在存储的对象的元数据中,名为“Etag”的基本元数据字段将包含密文的 MD5 散列。这是必需的,以便对象服务器不会在对象 PUT 上出错,并且对象审计器也不会由于散列不匹配而隔离对象(除非发生位腐烂)。
7.3 用户元数据¶
不仅对象的內容敏感;元数据也很敏感。由于元数据值必须是有效的 UTF-8 字符串,因此加密的值将以适当的方式编码(可能为 base64)进行存储。由于这种编码可能会增加用户元数据值的大小,超过允许的限制,因此元数据限制检查需要由加密器中间件实现。这样,用户在启用加密时看不到较低的元数据大小限制。加密器中间件将设置请求环境键 swift.constraints.override,以指示代理服务器已应用限制检查。
用户元数据名称将不会加密。由于每次通过 POST 请求更新元数据时都可能使用不同的 IV(或者实际上是不同的密码),因此加密元数据名称将使 Swift 无法删除过时的元数据项。同样,如果在现有的 Swift 集群上启用加密,加密元数据名称将阻止在更新时删除先前未加密的元数据。对于对象上的每个用户元数据,我们需要存储加密元数据,因为所有用户元数据项都使用不同的 IV 进行加密。这不能存储为 sysmeta 的项,因为 sysmeta 不能通过对象 POST 更新。因此,我们建议修改对象服务器以持久化头部X-Object-Massmeta-Crypto-Meta-*与X-Object-Meta-*对于对象上的每个用户元数据,我们需要存储加密元数据,因为所有用户元数据项都使用不同的 IV 进行加密。这不能存储为 sysmeta 的项,因为 sysmeta 不能通过对象 POST 更新。因此,我们建议修改对象服务器以持久化头部头部具有相同的语义,即对于对象上的每个用户元数据,我们需要存储加密元数据,因为所有用户元数据项都使用不同的 IV 进行加密。这不能存储为 sysmeta 的项,因为 sysmeta 不能通过对象 POST 更新。因此,我们建议修改对象服务器以持久化头部将在每次 POST 上更新,并在 POST 中不存在时删除。守门员中间件将防止
头部包含在客户端请求或响应中。加密器将添加一个X-Object-Massmeta-Crypto-Meta-<key>
X-Object-Massmeta-Crypto-Meta-<key>: "{'iv': 'zzz', 'cipher': 'AES_CTR_256'}"
注意
头部到对象 PUT 和 POST 请求头部,用于每个用户元数据,例如:可能值得添加一个通用的机制来持久化任何头部在X-Object-Massmeta-
命名空间中,并将该前缀添加到守门员禁止的头部。这将支持其他中间件(例如密钥管理器)以类似的方式使用中间件生成的元数据注释用户元数据。对于容器和帐户上的用户元数据,我们需要为每个用户元数据项存储加密元数据,因为这些可以通过 POST 请求独立更新。在这里,我们可以使用 sysmeta 来存储加密元数据项,例如,对于具有键的用户元数据项X-Container-Meta-Color
X-Container-Sysmeta-Crypto-Meta-Color: "{'iv': 'ccc', 'cipher': 'AES_CTR_256'}"
7.4 系统元数据¶
系统元数据(“sysmeta”)将不会被加密。
考虑一个使用 sysmeta 进行存储的中间件。如果出于某种原因,该中间件从加密前移动到加密后,那么所有先前存储的 sysmeta 将从其角度来看变成无法读取的垃圾。
7.5 摘要¶
Etag = MD5(ciphertext) (IFF client request included an etag header)
X-Object-Sysmeta-Crypto-Meta-Etag = {'iv': <iv>, 'cipher': <C_req>}
Content-Type = E(OEK, content-type)
X-Object-Sysmeta-Crypto-Meta-ct = {'iv': <iv>, 'cipher': <C_req>}
X-Object-Sysmeta-Crypto-Meta = {'iv': <iv>, 'cipher': <C_req>}
X-Object-Sysmeta-Crypto-Etag = E(OEK, MD5(plaintext))
X-Backend-Container-Update-Override-Etag = \
E(CEK, MD5(plaintext); meta={'iv': <iv>, 'cipher': <C_req>}
X-Backend-Container-Update-Override-Content-Type = \
E(CEK, content-type); meta={'iv': <iv>, 'cipher': <C_req>}
其中加密器将在发送到对象服务器的 PUT 请求上设置以下头部OEK是对象加密密钥,iv是随机选择的初始化向量,C_req
是在处理此请求时使用的密码。
X-Object-Meta-<user_key> = E(OEK, <user_value>} for every <user-key>
X-Object-Massmeta-Crypto-Meta-<user_key> = {'iv': <iv>, 'cipher': <C_req>}
此外,在包含用户定义元数据头部的对象 PUT 或 POST 请求上,加密器将设置
X-Container-Meta-<user_key> = E(CEK, <user_value>}
X-Container-Sysmeta-Crypto-Meta-<user_key> = {'iv': <iv>, 'cipher': <C_req>}
在发送到容器服务器的 PUT 或 POST 请求上,加密器将为每个用户定义的元数据头部设置以下头部
X-Account-Meta-<user_key> = E(AEK, <user_value>}
X-Account-Sysmeta-Crypto-Meta-<user_key> = {'iv': <iv>, 'cipher': <C_req>}
其中类似地,在发送到帐户服务器的 PUT 或 POST 请求上,加密器将为每个用户定义的元数据头部设置以下头部AEK
8. 客户端可见的更改¶
9.1 内部网络保护¶
Swift 的安全模型是基于周界的:代理服务器处理身份验证和授权,然后在私有内部网络上向存储服务器发出未经身份验证的请求。如果攻击者获得对内部网络的访问权限,他们可以读取和修改 Swift 集群中的任何对象,以及创建新的对象。可以使用经过身份验证的加密(例如 HMAC、GCM)来检测对象篡改。
大致上,这将涉及计算对象的强哈希值(例如 SHA-384 或 SHA-3),然后对该哈希值进行身份验证。对象审计器需要参与其中,以便我们对检测修改对象的时长有一个上限。
9.2 其他密码¶
9.3 客户端管理的密钥¶
9.4 重新加密支持¶
不要使用对象密钥 K-obj 并计算密文为 E(k-obj, 明文),而是将对象密钥视为密钥加密密钥 (KEK),并为每个对象创建一个随机数据加密密钥 (DEK)。
将用户元数据存储在 sysmeta 中¶
为了避免需要在加密器中间件中检查元数据头部限制,加密的元数据值可以存储在 sysmeta 中,sysmeta 不受相同的限制。在处理 GET 或 HEAD 响应时,解密器需要解密元数据值并将它们复制回用户元数据头部。
强制每个容器的单个不可变的密码选择¶
如果密码的选择对于容器甚至帐户是不可变的,我们可以避免将密码选择作为每个资源(包括单个元数据项)的元数据存储。不幸的是,在允许对分布式资源的相同副本进行多个并发操作的最终一致性系统中,很难实现不可变属性。
还应注意的是,仍然需要为每个资源存储 IV,因此这种替代方案并不能减轻存储加密元数据的需要。
此外,将密码选择绑定到容器策略并不能保证账户元数据的密码选择的不可变性。