HDFS 实施透明、端到端加密。配置后,从特殊 HDFS 目录中读取和写入的数据会透明地加密和解密,而无需更改用户应用程序代码。此加密也是端到端的,这意味着数据只能由客户端加密和解密。HDFS 绝不会存储或访问未加密的数据或未加密的数据加密密钥。这满足了加密的两个典型要求:静态加密(指存储在持久性介质(如磁盘)上的数据)以及传输中加密(例如,当数据通过网络传输时)。
加密可以在传统数据管理软件/硬件堆栈的不同层中进行。选择在给定层进行加密具有不同的优点和缺点。
应用程序级加密。这是最安全、最灵活的方法。应用程序对加密内容拥有最终控制权,并且可以准确反映用户的需求。但是,编写应用程序来执行此操作很困难。对于不支持加密的现有应用程序的客户来说,这不是一个选择。
数据库级加密。在属性方面类似于应用程序级加密。大多数数据库供应商提供某种形式的加密。但是,可能会出现性能问题。一个示例是索引无法加密。
文件系统级加密。此选项提供高性能、应用程序透明性,并且通常易于部署。但是,它无法对某些应用程序级策略进行建模。例如,多租户应用程序可能希望根据最终用户进行加密。数据库可能希望对存储在单个文件中的每一列使用不同的加密设置。
磁盘级加密。易于部署且性能高,但灵活性也较差。实际上只能防止物理盗窃。
HDFS 级加密介于此堆栈中的数据库级加密和文件系统级加密之间。这有许多积极影响。HDFS 加密能够提供良好的性能,并且现有的 Hadoop 应用程序能够在加密数据上透明地运行。在制定策略决策时,HDFS 也比传统文件系统具有更多上下文。
HDFS 级加密还可以防止文件系统级及以下的攻击(所谓的“操作系统级攻击”)。由于数据已由 HDFS 加密,因此操作系统和磁盘只与加密字节进行交互。
许多不同的政府、金融和监管实体要求数据加密。例如,医疗保健行业有 HIPAA 法规,卡支付行业有 PCI DSS 法规,美国政府有 FISMA 法规。将透明加密内置到 HDFS 中使组织更容易遵守这些法规。
加密也可以在应用程序级执行,但通过将其集成到 HDFS 中,现有应用程序可以在加密数据上运行而无需更改。这种集成架构意味着更强的加密文件语义以及与其他 HDFS 功能的更好协调。
对于透明加密,我们为 HDFS 引入了一个新抽象:加密区。加密区是一个特殊目录,其内容将在写入时被透明加密,在读取时被透明解密。每个加密区都与一个在创建该区时指定的加密区密钥相关联。加密区中的每个文件都有其自己的唯一数据加密密钥 (DEK)。DEK 永远不会由 HDFS 直接处理。相反,HDFS 永远只会处理加密数据加密密钥 (EDEK)。客户端解密 EDEK,然后使用后续的 DEK 来读写数据。HDFS 数据节点只看到一个加密字节流。
加密的一个非常重要的用例是“打开它”,并确保整个文件系统中的所有文件都被加密。为了支持这一强有力的保证,同时不丧失在文件系统不同部分使用不同加密区密钥的灵活性,HDFS 允许嵌套加密区。在创建加密区后(例如在根目录 /
上),用户可以在其后代目录(例如 /home/alice
)上使用不同的密钥创建更多加密区。文件的 EDEK 将使用来自最近祖先加密区的加密区密钥生成。
需要一个新的集群服务来管理加密密钥:Hadoop 密钥管理服务器 (KMS)。在 HDFS 加密的情况下,KMS 执行三项基本职责
提供对存储的加密区密钥的访问
为 NameNode 上的存储生成新的加密数据加密密钥
为 HDFS 客户端使用解密加密数据加密密钥
将在下面更详细地描述 KMS。
在加密区中创建新文件时,NameNode 要求 KMS 使用加密区的密钥生成新的 EDEK。然后,EDEK 将作为文件元数据的一部分永久存储在 NameNode 上。
在加密区中读取文件时,NameNode 向客户端提供文件的 EDEK 和用于加密 EDEK 的加密区密钥版本。然后,客户端要求 KMS 解密 EDEK,其中包括检查客户端是否有权访问加密区密钥版本。假设成功,客户端使用 DEK 解密文件内容。
读取和写入路径的所有上述步骤都通过 DFSClient、NameNode 和 KMS 之间的交互自动发生。
对加密的文件数据和元数据的访问受正常的 HDFS 文件系统权限控制。这意味着,如果 HDFS 遭到破坏(例如,通过未经授权访问 HDFS 超级用户帐户),恶意用户只能访问密文和加密密钥。但是,由于对加密区域密钥的访问受 KMS 和密钥存储上的单独权限集控制,因此这不会构成安全威胁。
KMS 是一个代理,代表 HDFS 守护程序和客户端与后端密钥存储进行交互。后端密钥存储和 KMS 都实现了 Hadoop KeyProvider API。有关更多信息,请参阅KMS 文档。
在 KeyProvider API 中,每个加密密钥都有一个唯一的密钥名称。由于密钥可以轮换,因此一个密钥可以有多个密钥版本,其中每个密钥版本都有自己的密钥材料(加密和解密期间使用的实际秘密字节)。可以通过密钥名称获取加密密钥(返回密钥的最新版本)或通过特定密钥版本获取密钥。
KMS 实现了其他功能,可以创建和解密加密加密密钥 (EEK)。EEK 的创建和解密完全在 KMS 上进行。重要的是,请求创建或解密 EEK 的客户端永远不会处理 EEK 的加密密钥。为了创建新的 EEK,KMS 会生成一个新的随机密钥,使用指定的密钥对其进行加密,并将 EEK 返回给客户端。为了解密 EEK,KMS 会检查用户是否具有对加密密钥的访问权限,使用该密钥解密 EEK,并返回解密后的加密密钥。
在 HDFS 加密中,EEK 是加密数据加密密钥 (EDEK),其中数据加密密钥 (DEK)用于对文件数据进行加密和解密。通常,密钥存储被配置为仅允许最终用户访问用于加密 DEK 的密钥。这意味着 EDEK 可以由 HDFS 安全地存储和处理,因为 HDFS 用户无法访问未加密的加密密钥。
一个必要的先决条件是 KMS 的实例,以及 KMS 的后端密钥存储。有关更多信息,请参阅KMS 文档。
一旦设置了 KMS,并且 NameNode 和 HDFS 客户端已正确配置,管理员就可以使用hadoop key
和hdfs crypto
命令行工具创建加密密钥并设置新的加密区域。可以使用 distcp 等工具将现有数据复制到新的加密区域中,从而对其进行加密。
与在读写加密区域时使用的加密密钥交互时要使用的 KeyProvider。HDFS 客户端将使用通过 getServerDefaults 从 Namenode 返回的提供程序路径。如果 namenode 不支持返回密钥提供程序 URI,则将使用客户端的 conf。
给定加密编解码器的前缀,包含给定加密编解码器(例如 EXAMPLECIPHERSUITE)的实现类的逗号分隔列表。如果可用,将使用第一个实现,其他实现是后备实现。
默认值:org.apache.hadoop.crypto.OpensslAesCtrCryptoCodec, org.apache.hadoop.crypto.JceAesCtrCryptoCodec
AES/CTR/NoPadding 的加密编解码器实现的逗号分隔列表。如果可用,将使用第一个实现,其他实现是后备实现。
默认值:AES/CTR/NoPadding
加密编解码器的密码套件。
默认值:无
CryptoCodec 中使用的 JCE 提供程序名称。
默认值:8192
CryptoInputStream 和 CryptoOutputStream 使用的缓冲区大小。
默认值:100
列出加密区域时,一批中将返回的最大区域数。分批增量获取列表可提高 namenode 性能。
crypto
命令行界面用法:[-createZone -keyName <keyName> -path <path>]
创建新的加密区域。
路径 | 要创建的加密区域的路径。它必须是一个空目录。该路径下会预置一个回收站目录。 |
密钥名称 | 用于加密区域的密钥的名称。不支持大写密钥名称。 |
用法:[-listZones]
列出所有加密区域。需要超级用户权限。
用法:[-provisionTrash -path <path>]
为加密区域预置一个回收站目录。
路径 | 加密区域根目录的路径。 |
用法:[-getFileEncryptionInfo -path <path>]
从文件中获取加密信息。这可用于找出文件是否正在加密,以及用于加密文件的密钥名称/密钥版本。
路径 | 要获取加密信息的文件的路径。 |
用法:[-reencryptZone <action> -path <zone>]
通过遍历加密区域并调用 KeyProvider 的 reencryptEncryptedKeys 接口,对加密区域进行重新加密,以批量重新加密密钥提供程序中密钥区域密钥的最新版本的所有文件的 EDEK。需要超级用户权限。
请注意,由于快照的不可变特性,重新加密不适用于快照。
操作 | 要执行的重新加密操作。必须为 -start 或 -cancel 。 |
路径 | 加密区域根目录的路径。 |
重新加密是 HDFS 中仅限于 NameNode 的操作,因此可能会给 NameNode 带来很大的负载。可以更改以下配置来控制 NameNode 上的压力,具体取决于集群可接受的吞吐量影响。
dfs.namenode.reencrypt.batch.size | 要发送到 KMS 以进行重新加密的一批 EDEK 的数量。在持有名称系统读/写锁时处理每一批,并在批次之间进行节流。请参阅以下配置。 |
dfs.namenode.reencrypt.throttle.limit.handler.ratio | 重新加密期间要持有的读锁的比率。1.0 表示不进行节流。0.5 表示重新加密最多可以持有其总处理时间的 50% 的读锁。负值或 0 无效。 |
dfs.namenode.reencrypt.throttle.limit.updater.ratio | 重新加密期间要持有的写锁的比率。1.0 表示不进行节流。0.5 表示重新加密最多可以持有其总处理时间的 50% 的写锁。负值或 0 无效。 |
用法:[-listReencryptionStatus]
列出所有加密区域的重新加密信息。需要超级用户权限。
这些说明假定您正在以普通用户或 HDFS 超级用户身份运行(视情况而定)。根据您的环境需要,使用 sudo
。
# As the normal user, create a new encryption key hadoop key create mykey # As the super user, create a new empty directory and make it an encryption zone hadoop fs -mkdir /zone hdfs crypto -createZone -keyName mykey -path /zone # chown it to the normal user hadoop fs -chown myuser:myuser /zone # As the normal user, put a file in, read it out hadoop fs -put helloWorld /zone hadoop fs -cat /zone/helloWorld # As the normal user, get encryption information from the file hdfs crypto -getFileEncryptionInfo -path /zone/helloWorld # console output: {cipherSuite: {name: AES/CTR/NoPadding, algorithmBlockSize: 16}, cryptoProtocolVersion: CryptoProtocolVersion{description='Encryption zones', version=1, unknownValue=null}, edek: 2010d301afbd43b58f10737ce4e93b39, iv: ade2293db2bab1a2e337f91361304cb3, keyName: mykey, ezKeyVersionName: mykey@0}
distcp 的一种常见用例是出于备份和灾难恢复目的而在集群之间复制数据。这通常由 HDFS 超级用户(即集群管理员)执行。
为了在使用 HDFS 加密时启用此相同的工作流,我们引入了一个新的虚拟路径前缀 /.reserved/raw/
,该前缀使超级用户可以直接访问文件系统中的底层块数据。这允许超级用户在无需访问加密密钥的情况下执行 distcp 数据,并且还避免了解密和重新加密数据的开销。这也意味着源数据和目标数据将逐字节相同,如果使用新的 EDEK 重新加密数据,则不会出现这种情况。
在使用 /.reserved/raw
对加密数据执行 distcp 时,务必使用 -px 标志保留扩展属性。这是因为加密文件属性(例如 EDEK)通过 /.reserved/raw
中的扩展属性公开,并且必须保留这些属性才能解密文件。这意味着,如果在加密区域根目录或其上层目录启动 distcp,它将自动在目标位置(如果尚未存在)创建一个加密区域。但是,仍然建议管理员首先在目标集群上创建相同的加密区域,以避免任何潜在的意外情况。
默认情况下,distcp 会比较文件系统提供的校验和,以验证数据是否已成功复制到目标位置。在从未加密或已加密的位置复制到已加密的位置时,文件系统校验和将不匹配,因为底层块数据不同,因为将使用新的 EDEK 在目标位置进行加密。在这种情况下,请指定 -skipcrccheck 和 -update distcp 标志以避免验证校验和。
HDFS 限制跨加密区域边界的文件和目录重命名。这包括将加密文件/目录重命名为未加密目录(例如,hdfs dfs mv /zone/encryptedFile /home/bob
),将未加密文件或目录重命名为加密区域(例如,hdfs dfs mv /home/bob/unEncryptedFile /zone
),以及在两个不同的加密区域之间重命名(例如,hdfs dfs mv /home/alice/zone1/foo /home/alice/zone2
)。在这些示例中,/zone
、/home/alice/zone1
和 /home/alice/zone2
是加密区域,而 /home/bob
不是。仅当源路径和目标路径位于同一加密区域中,或两个路径都未加密(不在任何加密区域中)时,才允许重命名。
此限制增强了安全性,并显著简化了系统管理。加密区域下的所有文件 EDEK 都使用加密区域密钥进行加密。因此,如果加密区域密钥遭到破坏,则识别所有易受攻击的文件并重新加密它们非常重要。如果最初在加密区域中创建的文件可以重命名为文件系统中的任意位置,则从根本上来说这是困难的。
为了遵守上述规则,每个加密区域在“区域目录”下都有自己的 .Trash
目录。例如,在 hdfs dfs rm /zone/encryptedFile
之后,encryptedFile
将移至 /zone/.Trash
,而不是用户主目录下的 .Trash
目录。当删除整个加密区域时,“区域目录”将移至用户主目录下的 .Trash
目录。
如果加密区域是根目录(例如 /
目录),则根目录的回收站路径是 /.Trash
,而不是用户主目录下的 .Trash
目录,并且在根目录中重命名子目录或子文件的行为将与一般加密区域中的行为保持一致,例如本节开头提到的 /zone
。
Hadoop 2.8.0 之前的 crypto
命令不会自动配置 .Trash
目录。如果在 Hadoop 2.8.0 之前创建了加密区域,然后将集群升级到 Hadoop 2.8.0 或更高版本,则可以使用 -provisionTrash
选项(例如 hdfs crypto -provisionTrash -path /zone
)配置回收站目录。
这些漏洞假设攻击者已获得对集群计算机(即数据节点和名称节点)硬盘的物理访问权限。
访问包含数据加密密钥的进程的交换文件。
这本身并不会泄露明文,因为它还需要访问加密块文件。
可以通过禁用交换、使用加密交换或使用 mlock 来防止密钥被换出,从而缓解此问题。
访问加密块文件。
这些漏洞假设攻击者已获得对集群计算机(即数据节点和名称节点)的 root shell 访问权限。由于恶意 root 用户可以访问保存加密密钥和明文的进程的内存状态,因此 HDFS 中无法解决其中的许多漏洞。对于这些漏洞,唯一的缓解技术是仔细限制和监控 root shell 访问。
访问加密块文件。
转储客户端进程的内存以获取 DEK、委派令牌、明文。
记录网络流量以嗅探传输中的加密密钥和加密数据。
转储数据节点进程的内存以获取加密块数据。
转储名称节点进程的内存以获取加密数据加密密钥。
这些漏洞利用假设攻击者已入侵 HDFS,但没有 root 或 hdfs
用户 shell 访问权限。
访问加密块文件。
通过 -fetchImage 访问加密区域和加密文件元数据(包括加密数据加密密钥)。
恶意用户可以收集他们有权访问的文件的密钥,并稍后使用它们来解密这些文件的加密数据。由于用户有权访问这些文件,因此他们已经可以访问文件内容。这可以通过定期密钥轮换策略来缓解。通常需要在密钥轮换后执行 reencryptZone 命令,以确保现有文件上的 EDEK 使用新版本密钥。
下面列出了完成密钥轮换和重新加密的手动步骤。这些说明假设您以密钥管理员或 HDFS 超级用户身份运行(视情况而定)。
# As the key admin, roll the key to a new version hadoop key roll exposedKey # As the super user, re-encrypt the encryption zone. Possibly list zones first. hdfs crypto -listZones hdfs crypto -reencryptZone -start -path /zone # As the super user, periodically check the status of re-encryption hdfs crypto -listReencryptionStatus # As the super user, get encryption information from the file and double check it's encryption key version hdfs crypto -getFileEncryptionInfo -path /zone/helloWorld # console output: {cipherSuite: {name: AES/CTR/NoPadding, algorithmBlockSize: 16}, cryptoProtocolVersion: CryptoProtocolVersion{description='Encryption zones', version=2, unknownValue=null}, edek: 2010d301afbd43b58f10737ce4e93b39, iv: ade2293db2bab1a2e337f91361304cb3, keyName: exposedKey, ezKeyVersionName: exposedKey@1}