Block Token
Block Token解决的问题
Hadoop 安全需要解决的两个问题:
- 认证:解决用户身份合法性验证问题
- 授权:解决认证用户的操作范围问题
认证问题通过Kerberos得到很好的解决,而且Hadoop内部设计了一套Token机制完美实现安全认证问题,同时在性能上得到保证
虽然解决了认证问题,但是存在安全隐患,尤其在DataNode端,比如客户端只要获取数据块信息之后就可以直接访问DataNode,对Block进行随意读写甚至是删除操作。换句话说,如果通过特殊方法获取集群的所有数据块集合,就存在单一认证用户清空整集群数据的可能。
Block Token 就是为了解决如上问题
Block Token 知识
BlockToken方案采用HMAC(Hash Message Authentication Code)技术实现对合法请求的访问认证检查。
HMAC是一种基于加密HASH函数和共享密钥的消息安全认证协议,它可以有效地防止数据在传输的过程中被截取和篡改,维护数据的安全性、完整性和可靠性。HMAC可以与任何迭代散列函数捆绑使用,MD5和SHA-1就是这种散列函数。实现原理是用公开函数和密钥对原始数据产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。使用密钥生成一个固定大小的小数据块即HMAC,并加入到消息中传输。接收方利用与发送方共享的密钥对接收到的消息进行认证和合法性检查。这种算法是不可逆的,无法通过消息摘要反向推导出消息,因此又称为单向散列函数。HMAC能有效保证数据的安全性、完整性和可靠性
HMAC算法流程
- 消息传递前,A和B约定共享密钥;
- A把要发送的消息使用共享密钥计算出HMAC值,然后将消息和HMAC发送给B;
- B接收到消息和HMAC值后,使用共享密钥计算消息本身的HMAC值,与接收到的HMAC值对比;
如果HMAC值相同,说明接收到的消息是完整的,而且是A发送的;
BlockToken方案里默认使用了经典的HMAC-SHA1算法,对照前面的流程,在HDFS里A可以代表NameNode,B代表DataNode,客户端在整个过程中仅作为数据流转的节点。简单说,只要NameNode给客户端发放了BlockToken,即可认证该客户端是可信赖的,DataNode检查BlockToken通过后就必须接受客户端表述的所有权限。
Block Token机制实现
Block Token机制是整个Hadoop生态里安全协议的重要组成部分,对HDFS来说,主要包括两个部分:
- 客户端经过初始认证(Kerberos),从NameNode获取DelegationToken,作为后续访问HDFS的凭证;
- 客户端真正读写数据前,请求NameNode获取对应Block的信息和BlockToken,根据结果向对应DataNode请求读写数据。请求到达DataNode端后,根据提供的BlockToken进行安全验证,通过验证后才能继续后续步骤,否则请求失败;
其中LocatedBlock是衔接整个读写流程的重要数据结构:
public class LocatedBlock {
private final ExtendedBlock b;
private long offset; // offset of the first byte of the block in the file
private final DatanodeInfoWithStorage[] locs;
/** Cached storage ID for each replica */
private final String[] storageIDs;
/** Cached storage type for each replica, if reported. */
private final StorageType[] storageTypes;
// corrupt flag is true if all of the replicas of a block are corrupt.
// else false. If block has few corrupt replicas, they are filtered and
// their locations are not part of this object
private boolean corrupt;
private Token<BlockTokenIdentifier> blockToken = new Token<>();
}
public class BlockTokenIdentifier extends TokenIdentifier {
// 用来表示Token的类型
static final Text KIND_NAME = new Text("HDFS_BLOCK_TOKEN");
private long expiryDate;
private int keyId;
private String userId;
private String blockPoolId;
private long blockId;
private final EnumSet<AccessMode> modes;
private StorageType[] storageTypes;
private String[] storageIds;
private boolean useProto;
private byte[] handshakeMsg;
}
public enum AccessMode {
READ, WRITE, COPY, REPLACE
}
从NameNode的getBlockLocations中查看,获取的LocatedBlocks需要加入BlockToken
LocatedBlocks getBlockLocations(String clientMachine, String srcArg,
long offset, long length) throws IOException {
// 最后一个参true就是代表needBlockToken
res = FSDirStatAndListingOp.getBlockLocations(
dir, pc, srcArg, offset, length, true);
}
private LocatedBlock createLocatedBlock(LocatedBlockBuilder locatedBlocks,
final BlockInfo blk, final long pos, final AccessMode mode)
throws IOException {
final LocatedBlock lb = createLocatedBlock(locatedBlocks, blk, pos);
// 设置BlockToken
if (mode != null) {
setBlockToken(lb, mode);
}
}
public void setBlockToken(final LocatedBlock b,
final AccessMode mode) throws IOException {
// dfs.block.access.token.enable来控制是否生成BlockToken,默认不开启
if (isBlockTokenEnabled()) {
blockTokenSecretManager.generateToken();
}
}
public Token<BlockTokenIdentifier> generateToken(String userId,
ExtendedBlock block, EnumSet<BlockTokenIdentifier.AccessMode> modes,
StorageType[] storageTypes, String[] storageIds) {
BlockTokenIdentifier id = new BlockTokenIdentifier(userId, block
.getBlockPoolId(), block.getBlockId(), modes, storageTypes,
storageIds, useProto);
return new Token<BlockTokenIdentifier>(id, this);
}
public Token(T id, SecretManager<T> mgr) {
password = mgr.createPassword(id);
identifier = id.getBytes();
kind = id.getKind();
service = new Text();
}
namenode返回的BlockToken客户端是怎么传递给datanode
currentNode = blockSeekTo(pos);
private synchronized DatanodeInfo blockSeekTo(long target) {
// 上面获取目标Block targetBlock
// 生成 blockReader
blockReader = getBlockReader(targetBlock, offsetIntoBlock,
targetBlock.getBlockSize() - offsetIntoBlock, targetAddr,
storageType, chosenNode);
}
protected BlockReader getBlockReader(LocatedBlock targetBlock,
long offsetInBlock, long length, InetSocketAddress targetAddr,
StorageType storageType, DatanodeInfo datanode) throws IOException {
Token<BlockTokenIdentifier> accessToken = targetBlock.getBlockToken();
// 生成BlockReader
return new BlockReaderFactory(dfsClient.getConf()).setBlockToken(accessToken).build();
}
public BlockReader build() throws IOException {
return getRemoteBlockReaderFromTcp();
}
private BlockReader getRemoteBlockReaderFromTcp() throws IOException {
blockReader = getRemoteBlockReader(peer);
}
private BlockReader getRemoteBlockReader(Peer peer) throws IOException {
return BlockReaderRemote.newBlockReader(token);
}
public static BlockReader newBlockReader(token) {
// 创建Porto的时候将token发送过去
new Sender(out).readBlock(block, blockToken, clientName, startOffset, len,
verifyChecksum, cachingStrategy);
}
// DataXceiver
public void readBlock(final ExtendedBlock block,
final Token<BlockTokenIdentifier> blockToken,
final String clientName,
final long blockOffset,
final long length,
final boolean sendChecksum,
final CachingStrategy cachingStrategy) {
checkAccess(out, true, block, blockToken, Op.READ_BLOCK,
BlockTokenIdentifier.AccessMode.READ);
}
private void checkAccess() {
checkAccess(out, reply, blk, t, op, mode, null, null);
}
private void checkAccess() {
datanode.blockPoolTokenSecretManager.checkAccess(t, null, blk, mode,
storageTypes, storageIds);
}
get(block.getBlockPoolId()).checkAccess(token, userId, block, mode,
storageTypes, storageIds);
public void checkAccess() {
BlockTokenIdentifier id = new BlockTokenIdentifier();
id.readFields(new DataInputStream(new ByteArrayInputStream(token
.getIdentifier())));
// DataNode检查第一阶段
checkAccess(id, userId, block, mode, storageTypes, storageIds);
// DataNode检查第二阶段
if (!Arrays.equals(retrievePassword(id), token.getPassword())) {
}
}
public byte[] retrievePassword(BlockTokenIdentifier identifier) {
BlockKey key = null;
synchronized (this) {
key = allKeys.get(identifier.getKeyId());
}
return createPassword(identifier.getBytes(), key.getKey());
}
BlockToken加密结构(使用块信息与用户信息生成BlockTokenIdentifier,然后生成一个BlockToken
Token之中的信息)
public BlockTokenIdentifier(String userId, String bpid, long blockId,
EnumSet<AccessMode> modes, StorageType[] storageTypes,
String[] storageIds, boolean useProto) {
this.cache = null;
this.userId = userId;
this.blockPoolId = bpid;
this.blockId = blockId;
this.modes = modes == null ? EnumSet.noneOf(AccessMode.class) : modes;
this.storageTypes = Optional.ofNullable(storageTypes)
.orElse(StorageType.EMPTY_ARRAY);
this.storageIds = Optional.ofNullable(storageIds)
.orElse(new String[0]);
this.useProto = useProto;
this.handshakeMsg = new byte[0];
}
public Token(T id, SecretManager<T> mgr) {
password = mgr.createPassword(id);
identifier = id.getBytes();
kind = id.getKind();
service = new Text();
}
protected byte[] createPassword(BlockTokenIdentifier identifier) {
// 重点(NameNode与DataNode需要共享的一个密钥)
BlockKey key = null;
synchronized (this) {
key = currentKey;
}
identifier.setExpiryDate(timer.now() + tokenLifetime);
identifier.setKeyId(key.getKeyId());
return createPassword(identifier.getBytes(), key.getKey());
}
一次完整的加密解密请求
- 客户端向NameNode发送Block请求
- NameNode经过权限检查,查找到对应数据块信息,生成对应的BlockToken放到LocateBlock返回给客户端
- 客户端收到LocateBlock之后,会先使用对应的Block信息创建通道的时候会将Blocks信息与Token信息全部发送给DataNode
- DataNode接收到读写信息之后,首先进行BlockToke校验,目的是对客户端的真实性及权限检查。根据从客户端提交过来的BlockTokenIdentifier分两步完成:
(1) 将BlockToken里的BlockTokenIdentifier反序列化,检查客户端请求的数据块、访问权限及用户名是否与BlockToken里表达一致,如果检查通过进入下一步,否则直接失败;
Block Token 加密key更新逻辑
private class Monitor implements Runnable {
// 定时更新SecretKey
blockManager.shouldUpdateBlockKey(now - lastBlockKeyUpdate);
}
synchronized boolean updateKeys() throws IOException {
// 移除过期Key
removeExpiredKeys();
// 设置当前Key最终过期时间
allKeys.put(currentKey.getKeyId(), new BlockKey(currentKey.getKeyId(),
timer.now() + keyUpdateInterval + tokenLifetime,
currentKey.getKey()));
// 生成新的当前Key
currentKey = new BlockKey(nextKey.getKeyId(), timer.now()
+ 2 * keyUpdateInterval + tokenLifetime, nextKey.getKey());
allKeys.put(currentKey.getKeyId(), currentKey);
setSerialNo(serialNo + 1);
nextKey = new BlockKey(serialNo, timer.now() + 3
* keyUpdateInterval + tokenLifetime, generateSecret());
allKeys.put(nextKey.getKeyId(), nextKey);
}
每次DataNodeManager处理DataNode心跳都会进行SecretKey的更新,NameNodee给DataNode回复心跳的时候会发送回去
对于NameNode与DataNode之前不会出现NameNode已经使用最新Key进行吧加密了Block,DataNode还没收到最新的这个Key,hdfs中是通过保存三份数据来处理一致性