内容简介:敏感信息的安全和隐私保护是互联网企业非常关心的问题,尤其进入大数据时代,稍有不慎就会出现重大的安全事故,数据安全问题就变得越来越重要。Hadoop作为数据平台事实上的标准基础设施,需要优先关注和解决好安全问题。虽然安全特性对Hadoop非常重要性,不过社区直到2011年末随Hadoop-1.0.0才第一次正式发布Hadoop Security,在这之前Hadoop社区版一直存在较大的安全隐患,需要用户自行解决。当然数据安全本身是一个复杂的系统化工程,想要描述清楚和完美解决几乎不可能。尽管如此,合理有效的安
一、背景
敏感信息的安全和隐私保护是互联网企业非常关心的问题,尤其进入大数据时代,稍有不慎就会出现重大的安全事故,数据安全问题就变得越来越重要。
Hadoop作为数据平台事实上的标准基础设施,需要优先关注和解决好安全问题。虽然安全特性对Hadoop非常重要性,不过社区直到2011年末随Hadoop-1.0.0才第一次正式发布Hadoop Security,在这之前Hadoop社区版一直存在较大的安全隐患,需要用户自行解决。
当然数据安全本身是一个复杂的系统化工程,想要描述清楚和完美解决几乎不可能。尽管如此,合理有效的安全保障还是非常必要。本文就Hadoop中数据块安全的问题,从设计权衡、实现原理、其中存在的问题及解决思路进行简单分析和梳理。
二、Hadoop安全概述
Hadoop安全需要解决两个问题:
(1)认证:解决用户身份合法性验证问题;
(2)授权:解决认证用户的操作范围问题;
其中认证问题通过Kerberos能够很好地解决,并通过 HADOOP-4487 在Hadoop内部设计了一套Token机制完美实现了安全认证问题,同时在性能上得到保证,图1为Hadoop安全认证体系概要图示。关于Hadoop Security特性的细节参考 HADOOP-4487 ,这里不再展开。
虽然通过Kerberos和Token机制很好的解决了认证的问题,但是仍然存在安全隐患,尤其在DataNode端。比如客户端只要获取到数据块BlockID后,可以直接访问DataNode,对Block进行随意读写甚至删除操作。换句话说,如果通过特殊方法获取集群的所有数据块集合,就存在单一认证用户清空整集群数据的可能。
社区针对这个问题在2008.10与Hadoop Security特性同步开始设计BlockToken方案 HADOOP-4359 ,经过半年左右时间在2009.05完成并发布,通过BlockToken数据块安全问题也得到了很好的解决。可以说 HADOOP-4487 和 HADOOP-4359 构建起了整个Hadoop安全体系。
三、安全基础简介
BlockToken方案采用HMAC(Hash Message Authentication Code)[1]技术实现对合法请求的访问认证检查。
HMAC是一种基于加密HASH函数和共享密钥的消息安全认证协议,它可以有效地防止数据在传输的过程中被截取和篡改,维护数据的安全性、完整性和可靠性。HMAC可以与任何迭代散列函数捆绑使用,MD5和SHA-1就是这种散列函数。实现原理是用公开函数和密钥对原始数据产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。使用密钥生成一个固定大小的小数据块即HMAC,并加入到消息中传输。接收方利用与发送方共享的密钥对接收到的消息进行认证和合法性检查。这种算法是不可逆的,无法通过消息摘要反向推导出消息,因此又称为单向散列函数。HMAC能有效保证数据的安全性、完整性和可靠性。
HMAC算法流程:
(1)消息传递前,A和B约定共享密钥;
(2)A把要发送的消息使用共享密钥计算出HMAC值,然后将消息和HMAC发送给B;
(3)B接收到消息和HMAC值后,使用共享密钥计算消息本身的HMAC值,与接收到的HMAC值对比;
(4)如果HMAC值相同,说明接收到的消息是完整的,而且是A发送的;
BlockToken方案里默认使用了经典的HMAC-SHA1算法,对照前面的流程,在HDFS里A可以代表NameNode,B代表DataNode,客户端在整个过程中仅作为数据流转的节点。简单说,只要NameNode给客户端发放了BlockToken,即可认证该客户端是可信赖的,DataNode检查BlockToken通过后就必须接受客户端表述的所有权限。
四、HDFS BlockToken机制
Token机制是整个Hadoop生态里安全协议的重要组成部分,对HDFS来说,主要包括两个部分:
(1)客户端经过初始认证(Kerberos),从NameNode获取DelegationToken,作为后续访问HDFS的凭证;
(2)客户端真正读写数据前,请求NameNode获取对应Block的信息和BlockToken,根据结果向对应DataNode请求读写数据。请求到达DataNode端后,根据提供的BlockToken进行安全验证,通过验证后才能继续后续步骤,否则请求失败;
本文主要关注第二个部分。其他部分的细节参考 HADOOP-4487 。
4.1 HDFS读写流程
首先简单梳理下如图2所示HDFS的读写流程:
(1)客户端读写操作(open/create)须首先获取数据块Block的分布,根据文件路径请求NameNode获取LocatedBlock;
(2)如果是读操作,根据返回LocatedBlock集合,从中选择合适的DataNode进行读数据请求,若需要读取的数据分布在多个Block,按顺序逐个切换DataNode读取;
(3)如果是写操作,根据返回LocatedBlock,首先将其中所有DataNode建立数据管道,完成后开始向数据管道里写数据,若需要写出的数据不能在一个Block内完成,再次向NameNode申请LocatedBlock,直到所有数据完成写出;
(4)读写操作完成,关闭数据流;
其中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 String[] storageIDs; /** Cached storage type for each replica, if reported. */ private 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<BlockTokenIdentifier>(); ...... }
4.2 BlockToken数据结构
前面提到的LocatedBlock中除了标识数据块Block信息外,还包含了安全验证中用到的关键数据结构blockToken:
public class Token<T extends TokenIdentifier> implements Writable { private byte[] identifier; private byte[] password; private Text kind; private Text service; private TokenRenewer renewer; }
blockToken的主要属性如下:
(1)kind标识的是Token的类型,这里为常量“HDFS_BLOCK_TOKEN”;
(2)service用来描述请求的服务,一般由服务端的"host:port"组成,对blockToken置为空;
(3)TokenRenewer在客户端的整个生命周期内周期性进行Renew,避免因为Token过期造成请求失败,但未见BlockToken周期Renew的显性实现,所以BlockToken一般是一次性生效;
(4)identifier是BlockTokenIdentifier的序列化结果:
public class BlockTokenIdentifier extends TokenIdentifier { private long expiryDate; private int keyId; private String userId; private String blockPoolId; private long blockId; private final EnumSet<AccessMode> modes; }
这里包含了当前请求的userId,BlockId,Block所在的BlockPool(用于HDFS Federation架构),Token的权限标识modes(READ, WRITE, COPY, REPLACE),Token的过期时间及keyId。
(5)password即使是使用HMAC算法以identifier和共享密钥SecretKey计算得到的密码。
需要说明的是,keyId和SecretKey存在对应关系,也就是通过keyId可以索引到SecretKey。
4.3 BlockToken流程
BlockToken在HDFS整个读写流程的以下几个步骤里:
1、客户端使用文件路径向NameNode发送读写请求,其中请求接口如下:
public LocatedBlocks getBlockLocations(String clientName, String src, long offset, long length); public LocatedBlock addBlock(String src, String clientName, ExtendedBlock previous, DatanodeInfo[] excludedNodes, long fileId, String[] favoredNodes);
2、NameNode经过权限检查后,搜索到文件对应的数据块信息,结合当前的keyId可以组织出来完整的BlockTokenIdentifier,用keyId对应的SecretKey加密BlockTokenIdentifier得到密码,BlockToken数据就绪,加上已经获取到的数据块信息即是LocatedBlock返回给客户端;
3、客户端从NameNode获取到LocatedBlock后,请求对应的DataNode执行数据读写操作;
4、DataNode端接收到读写请求,首先进行BlockToken校验,目的是对客户端的真实性及权限检查。根据从客户端提交过来的BlockTokenIdentifier分两步完成:
(1)将BlockToken里的BlockTokenIdentifier反序列化,检查客户端请求的数据块、访问权限及用户名是否与BlockToken里表达一致,如果检查通过进入下一步,否则直接失败;
(2)BlockTokenIdentifier反序列化结果里取出keyId,从本地索引出对应的SecretKey,使用与NameNode端相同的HMAC算法计算password,比较计算结果和BlockToken中的password,如果检查通过进入真正的数据读写,否则失败。
上述流程中,NameNode和DataNode计算密码时使用的密钥SecretKey均是以BlockTokenIdentifier.keyid作为索引在本地内存中获取。如果对相同的BlockTokenIdentifier使用同样的加密算法计算得到相同的结果,密钥必须完成一致。所以核心问题是,NameNode和DataNode如何保证密钥SecretKey同步,使得符合预期的请求能够正常验证通过。
最简单的办法就是给NameNode和DataNode初始化固定的<keyId,SecretKey>,到期后NameNode重新生成<keyId,SecretKey>并同步给DataNode问题解决。
但是事实并没有这么简单,我们知道DataNode与NameNode之间进行信息交互的唯一渠道是Heartbeat(默认3s一次),如果NameNode更新了SecretKey,但是DataNode的心跳3s后才上报,在这个默认的3s时间内,两端一定存在<keyId,SecretKey>不一致的可能,这个时段内即使有符合预期的请求也会校验失败,所以“最简单的办法”显然还不能完全解决问题。
虽然“最简单的办法”存在问题,但是提供了一种可以简单高效解决问题的思路,既然只维护一份<keyId,SecretKey>会出现黑障区问题,那么同一时刻始终保持两份<keyId,SecretKey>在线,这样就可以完全避免3s的黑障时间段。
实际上,HDFS更进一步,同时维护三份密钥<keyId,SecretKey>,NameNode如果发现有过期SecretKey,马上生成新的SecretKey补充进来并进行当前有效的SecretKey滚动,DataNode心跳过来后及时将更新后的SecretKey集合下发,如图3所示。
唯一的代价是NameNode过期检查需要同时检查三份数据过期时间,但是通常情况过期时间都较大(默认是10h)且数据量极小,所以完全不会对NameNode或者DataNode带来任何负担。
4.4 BlockToken密钥HA
前面提到了NameNode和DataNode同步密钥的流程,在HDFS HA架构里通常还存在ActiveNameNode和Standby NameNode之间同步数据的问题。
事实上,Active与Standby之间不对SecretKey通过EditLog或其他方式进行同步。这样带来的新问题是:如何保证操作主从切换后,当前的读写请求Token验证正常。如前面提到,DataNode定期请求Active NameNode更新本地<keyId,SecretKey>以保证正常读写请求能通过验证,同样DataNode也请求Standby NameNode并更新<keyId,SecretKey>。所以单从DataNode上看,同一个BlockPool实际上同一时间本地缓存至少6份<keyId,SecretKey>,其中3份来自Active NameNode,另外3份来自Standby NameNode。这样的话,不管客户端请求携带的keyId来自Active NameNode或者Standby NameNode,只要是正常请求均能验证通过,与是否操作主从切换或者从Standby NameNode请求无关。
除了主从切换的问题,服务重启时也存在类似问题需要解决:
(1)NameNode重启:当NameNode重启会重置<keyId,SecretKey>,由于NameNode重启后所有DataNode需要重新注册,注册完成后返回的CMD指令中包含了NameNode的<keyId,SecretKey>集合,保证了DataNode与NameNode之间完成同步;
(2)DataNode重启:DataNode重启比较简单,向Active NameNode和Standby NameNode分别注册,成功后会收到Active和Standby的所有<keyId,SecretKey>集合,更新内存状态即可。
为什么NameNode之间不像其他的WRITE操作,通过EditLog在Active与Standby之间保持同步?原因有两个:
1、SecretKey更新频率很低(10h);
2、数据量非常小(可忽略);
根据这两条不管是NameNode端还是DataNode端都完全可以承载,另外如果通过EditLog进行同步会增加复杂度,同时持久化SecretKey数据后,安全性上大打折扣,与Token设计的初衷相悖。
至此,BlockToken的整个流程简单梳理完成,可以看到,BlockToken与Kerberos体系的架构和核心流程有很多相似的地方。
五、BlockToken的问题及解决思路
从前面整个BlockToken的流程分析可以看出,设计思路和实现方案都比较优雅,但是实践过程中还是可能会遇到一些问题:
(1)NameNode重启完成后DataNode没有成功更新SecretKey造成客户端读写失败;
(2)NameNode滚动SecretKey后DataNode没有及时同步更新成功造成后续读写失败;
2017-05-17 23:41:58,952 ERROR org.apache.hadoop.hdfs.server.datanode.DataNode: rz-data-hdp-dn2925.rz.sankuai.com:50010:DataXceiver error processing WRITE_BLOCK operation src: /ip:port dst: /ip:port org.apache.hadoop.security.token.SecretManager$InvalidToken: Can't re-compute password for block_token_identifier (expiryDate=*, keyId=*, userId=*, blockPoolId=*, blockId=*, access modes=[WRITE]), since the required block key (keyID=*) doesn't exist. at org.apache.hadoop.hdfs.security.token.block.BlockTokenSecretManager.retrievePassword(BlockTokenSecretManager.java:384) at org.apache.hadoop.hdfs.security.token.block.BlockTokenSecretManager.checkAccess(BlockTokenSecretManager.java:302) at org.apache.hadoop.hdfs.security.token.block.BlockPoolTokenSecretManager.checkAccess(BlockPoolTokenSecretManager.java:97) at org.apache.hadoop.hdfs.server.datanode.DataXceiver.checkAccess(DataXceiver.java:1271) at org.apache.hadoop.hdfs.server.datanode.DataXceiver.writeBlock(DataXceiver.java:663) at org.apache.hadoop.hdfs.protocol.datatransfer.Receiver.opWriteBlock(Receiver.java:137) at org.apache.hadoop.hdfs.protocol.datatransfer.Receiver.processOp(Receiver.java:74) at org.apache.hadoop.hdfs.server.datanode.DataXceiver.run(DataXceiver.java:251) at java.lang.Thread.run(Thread.java:745)
出现这两个问题的主要原因是社区实现中DataNode同步SecretKey采用的是从NameNode Push的方案,但是对是否Push成功没有感知,比如:
(1)NameNode重启后,会重新生成新SecretKey集合,DataNode注册时NameNode将所有新生成的SecretKey集合Push给DataNode。我们知道NameNode重启阶段负载非常高,尤其是大规模集群,存在一种情况是NameNode端成功处理了DataNode的register请求,并将SecretKey集合返回给DataNode,但是DataNode端已经超时,或者因为网络异常等情况没有接收到NameNode的返回结果,这个时候NameNode和DataNode两端出现了不一致的问题:NameNode认为DataNode已经成功更新了SecretKey,之后不再下发更新SecretKey命令,但是DataNode端没有接收到新SecretKey集合,依然维护着一批无效SecretKey。此后当客户端读写请求过来后,BlockToken的验证永远失败;
(2)NameNode滚动SecretKey后,通过Heartbeat的返回值将新SecretKey集合Push给DataNode,同前一场景类似,返回值超时或者DataNode没有接收到心跳的返回值,同样造成NameNode和DataNode两端密钥不一致的问题,默认10h后Token滚动到该keyId时,客户端的请求因为BlockToken验证失败造成读写失败;
前面的两类场景可以看出,问题实际上发生在NameNode向DataNode同步SecretKey时,由于采用了Push的方案,但是对结果是否正常并没有感知,两端的数据不一致造成。问题清楚后解决方案其实也比较清晰,将NameNode向DataNode同步SecretKey的实现从Push升级到Pull(详见: HDFS-13473 ):
(1)DataNode注册时通过NameNode发下命令更新SecretKey的处理流程保持现状;
(2)在DataNode的心跳中增加当前SecretKey的版本号,NameNode端如果发现与本地SecretKey版本号不匹配通过心跳返回最新SecretKey集合;
将SecretKey同步机制从Push更新到Pull之后,因为心跳间隔默认3s,即使存在单次甚至连续数次心跳处理失败的情况,也可以在接下来成功的请求里及时更新,而不再是之前必须等默认10h之后才再次发起同步请求,而且还存在更新不成功的可能。可以有效避免NameNode和DataNode两端因为SecretKey不一致造成的数据请求数据的问题。
六、总结
本文从HDFS BlockToken引入的背景,方案设计的考虑,以及社区实现方案原理,并就其中存在的问题及解决思路进行了简单分析。
通过对BlockToken机制原理和实现细节分析,可以对HDFS安全方案窥一斑见全局,对其中可能出现的问题及优化改进思路提供支持。
七、参考
[1] https://en.wikipedia.org/wiki/HMAC
[2] https://issues.apache.org/jira/secure/attachment/12428537/security-design.pdf
[3] https://issues.apache.org/jira/secure/attachment/12409284/AccessTokenDesign1.pdf
[4] https://issues.apache.org/jira/browse/HADOOP-4487
[5] https://issues.apache.org/jira/browse/HADOOP-4359
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- HBase - 并发控制机制解析
- JStorm 源码解析:ACK 机制
- npm的lock机制解析
- Antd Form 实现机制解析
- Android 8.1 源码_机制篇 -- 全面解析 Handler 机制(原理篇)
- 浏览器解析机制与渲染过程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Python灰帽子
[美] Justin Seitz / 丁赟卿 译、崔孝晨 审校 / 电子工业出版社 / 2011-3 / 39.00元
《Python灰帽子》是由知名安全机构Immunity Inc的资深黑帽Justin Seitz主笔撰写的一本关于编程语言Python如何被广泛应用于黑客与逆向工程领域的书籍。老牌黑客,同时也是Immunity Inc的创始人兼首席技术执行官(CTO)Dave Aitel为这本书担任了技术编辑一职。书中绝大部分篇幅着眼于黑客技术领域中的两大经久不衰的话题:逆向工程与漏洞挖掘,并向读者呈现了几乎每个......一起来看看 《Python灰帽子》 这本书的介绍吧!