前言
之前寫了許多關於數據遷移的文章,也衍生的介紹了很多HDFS中相關的工具和特性,比如DistCp,ViewFileSystem等等.但是今天本文所要講的主題轉移到了另外一個領域數據安全.數據安全一直是用戶非常重視的一點,所以對於數據管理者,務必要做到以下原則:
數據不丟失,不損壞,數據內容不能被非法查閱.
本文所主要描述的方面就是上面原則中最后一點,保證數據不被非法查閱.在HDFS中,就有專門的功能來做這樣的事情,Encryption zone,數據加密空間,
Encryption zone綜述
HDFS Encryption zone加密空間是一種end-to-end(端到端)的加密模式.其中的加/解密過程對於客戶端來說是完全透明的.數據在客戶端讀操作的時候被解密,當數據被客戶端寫的時候被加密,所以HDFS本身並不是一個主要的參與者,形象的說,在HDFS中,你看到的只是一堆加密的數據流.
Encryption zone原理介紹
了解HDFS數據加密空間的原理對我們使用Encryption zone有很大的幫助.Encryption zone是HDFS中的一個抽象概念,它表示在此空間的內容在寫的時候會被透明的加密,同時在讀的時候,被透明的解密.這就是核心所在.具體到細小的細節.
- 1.每個encryption zone 會與每個encryption zone key相關聯,而這個key就是會在創建encryption zone的時候同時被指定.
- 2.每個encryption zone中的文件會有其唯一的data encryption key數據加密key,簡稱就是DEK.
- 3.DEK不會被HDFS直接處理,取而代之的是,HDFS只處理經過加密的DEK, 就是encrypted data encryption key,縮寫就是EDEK.
- 4.客戶端詢問KMS服務去解密EDEK,然后利用解密后得到的DEK去讀/寫數據.
在第四步驟有一個很重要的過程:
在客戶端向KMS服務請求時候,會有相關權限驗證,不符合要求的客戶端將不會得到解密好的DEK.而且KMS的權限驗證是獨立於HDFS的,是自身的一套權限驗證.
下面是對應的原理展示圖:
Key Provider可以理解為是一個key store的保存庫,其中KMS是其中的一個實現.
Encryption zone源碼實現
這個小節,我們將從源碼的層面對上述原理做跟蹤分析.這里可以分為2大方向,1個是創建文件,並且寫文件數據;2.讀文件數據內容.
Encryption zone下的寫文件
首先客戶端發起createFile請求,到了NameNode這邊,會調用startFile方法,里面就會有DEK,EDEK的生成
private HdfsFileStatus startFileInt(final String src,
PermissionStatus permissions, String holder, String clientMachine,
EnumSet<CreateFlag> flag, boolean createParent, short replication,
long blockSize, CryptoProtocolVersion[] supportedVersions,
boolean logRetryCache)
throws IOException {
...
FSDirWriteFileOp.EncryptionKeyInfo ezInfo = null;
// 判斷key provider是否為空
if (provider != null) {
readLock();
try {
checkOperation(OperationCategory.READ);
// 不為空,就生成EncryptionKey info.
ezInfo = FSDirWriteFileOp
.getEncryptionKeyInfo(this, pc, src, supportedVersions);
} finally {
readUnlock();
}
// Generate EDEK if necessary while not holding the lock
if (ezInfo != null) {
// 然后根據ezInfo的key名稱生成EDEK信息在ezInfo中
ezInfo.edek = FSDirEncryptionZoneOp
.generateEncryptedDataEncryptionKey(dir, ezInfo.ezKeyName);
}
EncryptionFaultInjector.getInstance().startFileAfterGenerateKey();
}
...
try {
// 繼續調用startFile方法
stat = FSDirWriteFileOp.startFile(this, pc, src, permissions, holder,
clientMachine, flag, createParent,
replication, blockSize, ezInfo,
toRemoveBlocks, logRetryCache);
...
繼續方法的調用
static HdfsFileStatus startFile(
FSNamesystem fsn, FSPermissionChecker pc, String src,
PermissionStatus permissions, String holder, String clientMachine,
EnumSet<CreateFlag> flag, boolean createParent,
short replication, long blockSize,
EncryptionKeyInfo ezInfo, INode.BlocksMapUpdateInfo toRemoveBlocks,
boolean logRetryEntry)
throws IOException {
assert fsn.hasWriteLock();
...
CipherSuite suite = null;
CryptoProtocolVersion version = null;
KeyProviderCryptoExtension.EncryptedKeyVersion edek = null;
// 取出ezInfo中的關鍵信息
if (ezInfo != null) {
edek = ezInfo.edek;
suite = ezInfo.suite;
version = ezInfo.protocolVersion;
}
...
FileEncryptionInfo feInfo = null;
final EncryptionZone zone = FSDirEncryptionZoneOp.getEZForPath(fsd, iip);
if (zone != null) {
// The path is now within an EZ, but we're missing encryption parameters
if (suite == null || edek == null) {
throw new RetryStartFileException();
}
// Path is within an EZ and we have provided encryption parameters.
// Make sure that the generated EDEK matches the settings of the EZ.
final String ezKeyName = zone.getKeyName();
if (!ezKeyName.equals(edek.getEncryptionKeyName())) {
throw new RetryStartFileException();
}
// 傳入到FileEncryptionInfo中,feInfo將會被設置到INode文件中
feInfo = new FileEncryptionInfo(suite, version,
edek.getEncryptedKeyVersion().getMaterial(),
edek.getEncryptedKeyIv(),
ezKeyName, edek.getEncryptionKeyVersionName());
}
...
OK,完成了這些操作之后,將會返回一個HDFSFileStatus對象,此對象將會被DFSOutputstream利用.下面就是客戶端的解密DEDK,並加密數據的過程了.
public HdfsDataOutputStream createWrappedOutputStream(DFSOutputStream dfsos,
FileSystem.Statistics statistics, long startPos) throws IOException {
// 取出文件中的加密信息
final FileEncryptionInfo feInfo = dfsos.getFileEncryptionInfo();
if (feInfo != null) {
// 文件是被加密的,需要包裝數據流為加密流
// File is encrypted, wrap the stream in a crypto stream.
// Currently only one version, so no special logic based on the version #
getCryptoProtocolVersion(feInfo);
final CryptoCodec codec = getCryptoCodec(conf, feInfo);
// 解密feInfo中的EDEK的信息,其中會向KerProvider進行請求
KeyVersion decrypted = decryptEncryptedDataEncryptionKey(feInfo);
// 然后解密后的信息作為參數,構造出加密輸出流
final CryptoOutputStream cryptoOut =
new CryptoOutputStream(dfsos, codec,
decrypted.getMaterial(), feInfo.getIV(), startPos);
return new HdfsDataOutputStream(cryptoOut, statistics, startPos);
} else {
// No FileEncryptionInfo present so no encryption.
return new HdfsDataOutputStream(dfsos, statistics, startPos);
}
}
我們可以繼續完decryptEncryptedDataEncryptionKey方法,驗證是否有向KeyProvider方法請求服務.
private KeyVersion decryptEncryptedDataEncryptionKey(FileEncryptionInfo
feInfo) throws IOException {
try (TraceScope ignored = tracer.newScope("decryptEDEK")) {
// 獲取keyProvider服務實例
KeyProvider provider = getKeyProvider();
if (provider == null) {
throw new IOException("No KeyProvider is configured, cannot access" +
" an encrypted file");
}
// 獲取加密的key version
EncryptedKeyVersion ekv = EncryptedKeyVersion.createForDecryption(
feInfo.getKeyName(), feInfo.getEzKeyVersionName(), feInfo.getIV(),
feInfo.getEncryptedDataEncryptionKey());
try {
KeyProviderCryptoExtension cryptoProvider = KeyProviderCryptoExtension
.createKeyProviderCryptoExtension(provider);
// 進行解密操作
return cryptoProvider.decryptEncryptedKey(ekv);
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
}
}
構造完加密輸出流對象之后CryptoOutputStream之后,在隨后的寫操作中,數據都會額外經過一步加密算法的操作.此部分的過程調用圖如下:
Encryption zone下的讀文件
讀文件部分的操作與寫文件非常類似.
首先是構造出目標文件的HDFSFileStatus對象,然后取出其中的FileEncryptionInfo,在此過程中FileEncryptionInfo會被設置到LocatedBlocks中.
private static HdfsLocatedFileStatus createLocatedFileStatus(
FSDirectory fsd, byte[] path, INodeAttributes nodeAttrs,
byte storagePolicy, int snapshot,
boolean isRawPath, INodesInPath iip) throws IOException {
...
// 然后設置到LocatedBlocks中
loc = fsd.getBlockManager().createLocatedBlocks(
fileNode.getBlocks(snapshot), fileSize, isUc, 0L, size, false,
inSnapshot, feInfo, ecPolicy);
...
然后這些Blocks信息會以參數的信息傳入到DFSInputStream,並在方法fetchLocatedBlocksAndGetLastBlockLength被設置到變量中.
private long fetchLocatedBlocksAndGetLastBlockLength(boolean refresh)
throws IOException {
LocatedBlocks newInfo = locatedBlocks;
...
// 將locatedBlocks中的EncryptionInfo信息設置到變量中
fileEncryptionInfo = locatedBlocks.getFileEncryptionInfo();
return lastBlockBeingWrittenLength;
}
然后此信息同樣會被取出用到加密輸入流中
public HdfsDataInputStream createWrappedInputStream(DFSInputStream dfsis)
throws IOException {
// 獲取文件加密信息
final FileEncryptionInfo feInfo = dfsis.getFileEncryptionInfo();
if (feInfo != null) {
// File is encrypted, wrap the stream in a crypto stream.
// Currently only one version, so no special logic based on the version #
getCryptoProtocolVersion(feInfo);
final CryptoCodec codec = getCryptoCodec(conf, feInfo);
// 解密DEDK
final KeyVersion decrypted = decryptEncryptedDataEncryptionKey(feInfo);
// 構造加密輸入流
final CryptoInputStream cryptoIn =
new CryptoInputStream(dfsis, codec, decrypted.getMaterial(),
feInfo.getIV());
return new HdfsDataInputStream(cryptoIn);
} else {
// No FileEncryptionInfo so no encryption.
return new HdfsDataInputStream(dfsis);
}
}
與之前的過程非常的類似,在加密輸入流中,就會對讀取的數據進行解密,使得用戶能看到正常的數據.同樣給出過程圖:
Encryption zone的管理
在源碼分析的最后部分,這里再簡單描述一下HDFS的Encryption zone的中心管理.同樣是一個叫做EncryptionZoneManager的類來專門做這個事情的,但是有一點不同,他保存的對象不是EncryptionZone,而是EncryptionZoneInt.
public class EncryptionZoneManager {
public static Logger LOG = LoggerFactory.getLogger(EncryptionZoneManager
.class);
...
// 用TreeMap保存的Encryption zone列表
private final TreeMap<Long, EncryptionZoneInt> encryptionZones;
private final FSDirectory dir;
private final int maxListEncryptionZonesResponses;
...
這里的TreeMap的key位置保存的encryption zone的對應目錄的indeed.
EncryptionZoneInt與EncryptionZone有什么微妙的關系呢?
在具體使用的時候,EncryptionZoneInt會被用來構造EncryptionZone
如下代碼
EncryptionZone getEZINodeForPath(INodesInPath iip) {
final EncryptionZoneInt ezi = getEncryptionZoneForPath(iip);
if (ezi == null) {
return null;
} else {
return new EncryptionZone(ezi.getINodeId(), getFullPathName(ezi),
ezi.getSuite(), ezi.getVersion(), ezi.getKeyName());
}
}
通過判斷目標路徑是否在encryption zone列表中,來判斷此文件是否為加密文件,以為inodeId作為key去map中取出.
下面給出Encryption zone管理的結構圖:
Encryption zone的使用
最后再介紹以下Encryption zone功能的具體配置使用.總的來說,住需要完成幾個相關的配置項即可.
第一步: 完成keyProveider的配置
將已存在的keyProvider的URL地址配置到下面的配置中
dfs.encryption.key.provider.uri
第二步: 加密算法相關的配置
主要有以下的一些配置
hadoop.security.crypto.codec.classes.EXAMPLECIPHERSUITE
hadoop.security.crypto.codec.classes.aes.ctr.nopadding
hadoop.security.crypto.cipher.suite
hadoop.security.crypto.jce.provider
hadoop.security.crypto.buffer.size
當然這些配置並不需要額外配置,采用默認配置也是可以的.
第三步: 配置listZone響應回復的個數
此配置會在listZones的命令中起到作用.
dfs.namenode.list.encryption.zones.num.responses
第四步: 創建Encryption zone加密空間
這里的加密空間是針對目錄級別的,並且還需要設置一個key名稱,使用的命令如下
hdfs crypto -createZone -keyName <keyName> -path <path>
這里的path是要已經建好的目錄,此命令的作用相當於將目標目錄作為一個加密空間,在此目錄下的文件在讀寫的過程中,被加/解密.
以上操作完成之后,加密空間就基本創建好了,可以用listZones的命令查看當前已創建的加密空間
hdfs crypto -listZones
然后此目錄文件數據的加解密過程對於客戶端來說完全是透明的了.
Encryption zone使用范例
下面舉出官方的使用例子
# 以普通用戶的身份創建一個加密key
hadoop key create myKey
# 以超級用戶的身份創建一個空目錄,並使之成為加密空間
hadoop fs -mkdir /zone
hdfs crypto -createZone -keyName myKey -path /zone
# 修改此目錄權限為普通用戶的
hadoop fs -chown myuser:myuser /zone
# 以普通用戶的身份進行put上傳文件和cat查看文件操作
hadoop fs -put helloWorld /zone
hadoop fs -cat /zone/helloWorld
參考鏈接
1.http://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/TransparentEncryption.html