HDFS实现了透明、端到端的加密功能。一旦配置完成,对特定HDFS目录的读写操作将会被透明地加密和解密,无需修改用户应用程序代码。这种加密同时也是端到端的,意味着数据只能由客户端进行加密和解密。HDFS永远不会存储或访问未加密的数据或未加密的数据加密密钥。这满足了加密的两个典型需求:静态数据加密(指持久化存储介质如磁盘上的数据)以及传输中加密(例如数据在网络中传输时)。
在传统的数据管理软件/硬件堆栈中,可以在不同层级进行加密。选择在特定层级加密会带来不同的优缺点。
应用层加密。这是最安全且最灵活的方法。应用程序对加密内容拥有最终控制权,能够精确反映用户需求。然而,编写实现此功能的应用程序较为困难。对于现有不支持加密的应用程序用户来说,这也无法作为可选方案。
数据库级加密。在特性方面与应用级加密类似。大多数数据库供应商都提供某种形式的加密。然而,可能存在性能问题。一个例子是索引无法被加密。
文件系统级加密。该选项提供高性能、应用透明性,通常易于部署。然而,它无法模拟某些应用级策略。例如,多租户应用可能希望根据最终用户进行加密。数据库可能希望对存储在单个文件中的每一列采用不同的加密设置。
磁盘级加密。易于部署且性能高,但灵活性较差。仅能有效防范物理盗窃。
HDFS级加密在这个技术栈中介于数据库级和文件系统级加密之间。这带来了许多积极影响。HDFS加密能够提供良好的性能,现有的Hadoop应用程序能够透明地在加密数据上运行。在制定策略决策时,HDFS也比传统文件系统拥有更多上下文信息。
HDFS级别的加密还能防止文件系统级别及以下的攻击(所谓的“操作系统级别攻击”)。由于数据已被HDFS加密,操作系统和磁盘仅能接触到加密后的字节流。
许多不同的政府、金融和监管机构都要求进行数据加密。例如,医疗行业有HIPAA法规,银行卡支付行业有PCI DSS法规,美国政府有FISMA法规。在HDFS中内置透明加密功能,使组织更容易遵守这些法规。
加密也可以在应用层执行,但通过将其集成到HDFS中,现有应用程序无需修改即可操作加密数据。这种集成架构意味着更强的加密文件语义,并与其他HDFS功能实现更好的协调。
为了实现透明加密,我们在HDFS中引入了一个新的抽象概念:加密区。加密区是一个特殊目录,其内容在写入时会自动加密,在读取时会自动解密。每个加密区都与一个唯一的加密区密钥相关联,该密钥在创建加密区时指定。加密区内的每个文件都有自己唯一的数据加密密钥(DEK)。HDFS从不直接处理DEK,而是仅处理加密后的数据加密密钥(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实现了额外功能,支持创建和解密加密密钥(EEKs)。EEK的创建和解密完全在KMS上完成。重要的是,请求创建或解密EEK的客户端永远不会处理EEK的加密密钥。要创建新的EEK,KMS会生成一个新的随机密钥,用指定密钥对其进行加密,然后将EEK返回给客户端。要解密EEK,KMS会检查用户是否有权访问加密密钥,使用该密钥解密EEK,并返回解密后的加密密钥。
在HDFS加密的上下文中,EEKs是加密的数据加密密钥(EDEKs),其中数据加密密钥(DEK)用于加密和解密文件数据。通常,密钥库被配置为仅允许终端用户访问用于加密DEKs的密钥。这意味着EDEKs可以被HDFS安全地存储和处理,因为HDFS用户将无法访问未加密的加密密钥。
必要的先决条件是拥有一个KMS实例,以及KMS的后备密钥存储。更多信息请参阅KMS文档。
一旦KMS设置完成且NameNode和HDFS客户端正确配置后,管理员可以使用hadoop key
和hdfs crypto
命令行工具来创建加密密钥并设置新的加密区域。可以通过使用distcp等工具将现有数据复制到新的加密区域来实现数据加密。
用于与读写加密区域时使用的加密密钥交互的KeyProvider。HDFS客户端将使用通过getServerDefaults从Namenode返回的提供者路径。如果namenode不支持返回密钥提供者URI,则将使用客户端的配置。
给定加密编解码器的前缀,包含该加密编解码器(例如EXAMPLECIPHERSUITE)的实现类的逗号分隔列表。如果可用,将使用第一个实现,其他实现作为备选方案。
默认值: org.apache.hadoop.crypto.OpensslAesCtrCryptoCodec, org.apache.hadoop.crypto.JceAesCtrCryptoCodec
用于AES/CTR/NoPadding的加密编解码器实现的逗号分隔列表。如果可用,将使用第一个实现,其他实现作为备选方案。
默认值: org.apache.hadoop.crypto.OpensslSm4CtrCryptoCodec, org.apache.hadoop.crypto.JceSm4CtrCryptoCodec
SM4/CTR/NoPadding加密编解码器实现的逗号分隔列表。如果可用,将使用第一个实现,其他实现作为备选方案。
默认值: AES/CTR/NoPadding
加密编解码器的密码套件,目前支持AES/CTR/NoPadding和SM4/CTR/NoPadding。
默认值:无
CryptoCodec中使用的JCE提供程序名称。
默认值: 8192
CryptoInputStream和CryptoOutputStream使用的缓冲区大小。
默认值: 100
列出加密区域时,单次批量返回的最大区域数量。通过分批增量获取列表可提升namenode性能。
crypto
命令行界面用法:[-createZone -keyName
创建一个新的加密区域。
path | 要创建的加密区路径。必须是一个空目录。系统会在此路径下预置一个回收站目录。 |
keyName | 用于加密区域的密钥名称。不支持大写字母的密钥名称。 |
用法:[-listZones]
列出所有加密区域。需要超级用户权限。
用法:[-provisionTrash -path
为加密区域配置一个回收站目录。
path | 加密区域根目录的路径。 |
用法:[-getFileEncryptionInfo -path
从文件中获取加密信息。这可用于查明文件是否正在加密,以及用于加密的密钥名称/密钥版本。
path | 要获取加密信息的文件路径。 |
用法:[-reencryptZone
通过遍历加密区域并调用KeyProvider的reencryptEncryptedKeys接口,使用密钥提供器中最新的加密区域密钥批量重新加密所有文件的EDEK,从而对加密区域进行重新加密。需要超级用户权限。
请注意,由于快照的不可变性,重新加密不适用于快照。
action | The re-encrypt action to perform. Must be either -start or -cancel . |
path | 加密区根目录的路径。 |
重新加密是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(数据加密密钥)、委托令牌和明文数据。
记录网络流量以嗅探传输中的加密密钥和加密数据。
转储数据节点进程的内存以获取加密的块数据。
转存namenode进程的内存以获取加密的数据加密密钥。
这些攻击手段假设攻击者已经入侵了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}