當前位置:
首頁 > 最新 > HDFS BlockToken機制解析

HDFS BlockToken機制解析

一、背景

敏感信息和隱私數據的安全保護是互聯網公司非常關心的問題,尤其進入大數據時代,稍有不慎就會出現重大安全事故,所以數據安全問題就變得越來越重要。

Hadoop作為數據平台的基礎設施,需要優先關注和解決好安全問題。雖然安全特性對Hadoop非常重要,不過社區直到2011年末隨Hadoop-1.0.0才第一次正式發布Hadoop Security,在這之前Hadoop社區版存在較大的安全隱患,需要用戶自行解決。

當然數據安全本身是一個複雜的系統工程,想要描述清楚和完美解決幾乎不可能。儘管如此,合理有效的安全保障是必要的。本文就Hadoop中數據塊安全問題,從設計權衡和實現原理進行簡單分析和梳理,簡要闡述當前方案在實踐中可能遇到的問題,同時提供可借鑒的解決思路。

二、Hadoop安全概述

Hadoop安全主要解決兩個問題:

(1)認證:解決用戶身份合法性驗證問題;

(2)授權:解決認證用戶的操作範圍問題;

其中認證問題利用Kerberos可以很好地解決,通過HADOOP-4487引入Kerberos並結合內部設計的Token機制完美解決Hadoop安全認證問題,同時在性能上得到很好地保證,圖1為Hadoop安全認證體系概要圖示。關於Hadoop Security特性的細節參考HADOOP-4487,這裡不再展開。

圖1 Hadoop安全認證體系

雖然通過Kerberos和Token機制很好的解決了認證的問題,但是仍然存在安全隱患,尤其在DataNode端。比如認證通過的客戶端只要獲取到數據塊信息,可以直接訪問DataNode,對數據塊進行任意讀寫甚至刪除操作。換句話說,如果通過特殊方法獲取到Hadoop集群內所有數據塊集合,就存在單一認證用戶清空整集群數據的可能。

針對這個問題社區在2008.10與Hadoop Security特性同步開始設計BlockToken方案HADOOP-4359,經過半年左右時間在2009.05完成並發布,BlockToken特性可以非常好地保護數據塊安全。可以說HADOOP-4487和HADOOP-4359構建起整個Hadoop安全體系,本文重點關注HADOOP-4359。

三、安全基礎簡介

BlockToken方案使用HMAC(Hash Message Authentication Code)[1]技術實現對合法請求的訪問認證檢查。

HMAC是一種基於HASH函數和共享密鑰的消息安全認證協議,它可以有效地防止數據在傳輸的過程中被截取和篡改,維護數據的安全性、完整性和可靠性。HMAC可以與任何迭代HASH函數結合使用,MD5和SHA-1就是這種HASH函數。實現原理是用公開函數和共享密鑰對原始數據產生一個固定長度的值作為認證標識,用這個標識鑒別消息的完整性。使用密鑰生成一個固定大小的消息摘要小數據塊即HMAC,並加入到消息中一起傳輸。接收方利用與發送方共享的密鑰對接收到的消息進行認證和合法性檢查。這種演算法不可逆,無法通過消息摘要反向推導出消息,因此又稱為單向HASH函數。通過這種技術可以有效保證數據的安全性、完整性和可靠性。

HMAC演算法流程:

(1)消息傳遞前,Alice和Bob約定共享密鑰和HASH函數;

(2)Alice把要發送的消息使用共享密鑰計算出HMAC值,然後將消息和HMAC發送給Bob;

(3)Bob接收到消息和HMAC值後,使用共享密鑰獨立計算消息本身的HMAC值,與接收到的HMAC值對比;

(4)如果二者的HMAC值相同,說明接收到的消息是完整的,且是Alice發送;

BlockToken方案默認使用了經典的HMAC-SHA1演算法,對照前面的流程,Alice代表的是NameNode,Bob代表DataNode,客戶端在整個過程中僅作為數據流轉的節點。因為HMAC能夠保證數據傳輸過程中不被截取和篡改,只要NameNode給客戶端發放了BlockToken,即可認為該客戶端申請對單個數據塊的訪問許可權是可信賴的,DataNode只要對BlockToken檢查通過就必須接受客戶端表述的所有許可權。

四、HDFS BlockToken機制

Token機制是整個Hadoop生態里安全協議的重要組成部分,在HDFS內部包括兩個部分:

(1)客戶端經過初始認證(Kerberos),從NameNode獲取DelegationToken,作為後續訪問HDFS的憑證;

(2)客戶端讀寫數據前,請求NameNode獲取對應數據塊Block信息和BlockToken,根據結果向對應DataNode真正請求讀寫數據。請求到達DataNode端,根據客戶端提供的BlockToken進行安全認證檢查,通過後繼續後續步驟,否則請求失敗;

這裡的第二部分就是HADOOP-4359關注的內容。


開始詳細梳理BlockToken原理之前,首先簡單梳理下如圖2所示的HDFS讀寫流程:

圖2 HDFS讀寫流程示意圖

(1)客戶端讀寫操作(open/create)需首先獲取數據塊Block分布,根據文件路徑請求NameNode獲取LocatedBlock;

(2)如果是讀操作,根據返回LocatedBlock集合,從中選擇合適的DataNode進行讀數據請求,若需要讀取的數據分布在多個Block,按順序逐個切換到對應DataNode讀取;

(3)如果是寫操作,首先將返回的LocatedBlock中所有DataNode建立數據管道(Pipeline),然後開始向數據管道里寫數據,若寫出的數據不能在一個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 blockToken = new Token();

......

}


前一節提到的LocatedBlock除了標識數據塊Block信息外,還包含了認證流程中的核心數據結構blockToken:

public class Token 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 modes;

}

包含了當前請求來源userId,數據塊標識blockId,數據塊所在的BlockPool(用於HDFS Federation架構),本次請求的許可權標識modes(READ, WRITE, COPY, REPLACE),Token的過期時間及keyId;

(5)password即是使用共享密鑰SecretKey應用HMAC演算法對identifier計算得到的密碼。

需要說明的是,keyId和SecretKey存在對應關係,通過keyId可以索引到SecretKey,後續詳細介紹。


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後,帶著BlockToken請求對應DataNode執行數據讀寫操作;

4、DataNode端接收到讀寫請求,首先進行BlockToken檢查,目的是檢查客戶端的真實性和許可權。主要有兩個步驟:

(1)將BlockToken里的identifier反序列化,檢查客戶端請求的數據塊、訪問許可權及用戶名是否與BlockToken的表達一致,如果檢查通過進入下一步,否則直接失敗;

(2)從identifier反序列化結果里取出keyId,在本地索引對應的共享密鑰SecretKey,使用與NameNode端相同的HMAC演算法計算password,之後與BlockToken中的password進行比較,如果相等開始真正的數據讀寫流程,否則請求失敗。

上述流程中,NameNode和DataNode計算密碼時使用的密鑰SecretKey均是以BlockTokenIdentifier.keyid作為索引在本地內存中獲取。要想對相同的BlockTokenIdentifier使用同樣的加密演算法計算得到相同的結果,密鑰SecretKey必須完全一致。所以核心問題是,NameNode和DataNode如何保證密鑰SecretKey同步,使符合預期的請求通過驗證。

最簡單的辦法就是NameNode和DataNode初始化固定的密鑰,到期後NameNode重新生成並同步給DataNode問題解決。

但是事實並沒有這麼簡單,我們知道DataNode與NameNode之間信息交互最頻繁的渠道是Heartbeat(默認3s一次),如果NameNode更新了SecretKey,但是DataNode心跳3s後才上報,在這3s時間內,兩端存在密鑰不一致的問題,也就是在這個時段內即使合法請求也會檢查失敗,所以「最簡單的辦法」顯然還不能完全解決問題。

雖然「最簡單的辦法」存在問題,但是提供了一種簡單高效解決問題的思路,既然只維護一份共享密鑰SecretKey會出現「黑障區」問題,那麼同一時刻始終保持兩份在線,這樣就可以完全避免3s的黑障時間段。

事實上,HDFS更進一步同時維護三份共享密鑰,NameNode一旦發現有SecretKey過期,馬上生成新SecretKey補充進來並向前滾動當前激活SecretKey,DataNode心跳過來後及時下發更新後的SecretKey集合,如圖3所示。維護三份密鑰的代價是NameNode需要同時檢查三份數據有效期,但是通常情況過期時間較大(默認是10h)且數據量極小,所以完全不會給NameNode或者DataNode帶來負擔。

圖3 HDFS BlockToken流程圖示


前面提到了NameNode和DataNode同步密鑰的流程,在HDFS HA架構里通常還存在Active NameNode和Standby NameNode同步數據的問題。

事實上,Active與Standby之間不對SecretKey通過EditLog或其他方式同步。這樣帶來的新問題是:如何保證操作主從切換後,當前正常讀寫請求的Token驗證通過。如前面提到,NameNode定期更新SecretKey後及時將更新後的SecretKey集合同步給DataNode,DataNode更新以保證正常讀寫請求通過驗證,這種方式對Active和Standby同樣適用。所以單從DataNode來看,同一個BlockPool實際上同一時間本地緩存至少6份共享密鑰,其中3份來自Active NameNode,另外3份來自Standby NameNode。這樣的話,不管客戶端請求攜帶的keyId來自Active NameNode或者Standby NameNode,只要是正常請求均能驗證通過,與是否操作主從切換或者從Standby NameNode請求無關。

接下來的問題,DataNode維護了多份數據,如何避免來自Active和Standby之間的keyId衝突,以及HDFS Federation架構下,來自多個Namespace的keyId衝突。先來看HDFS Federation架構,與BlockPool類似,共享密鑰相關信息也按照這個維度組織就不會相互干擾。來自同Namespace下Active和Standby的keyId確實存在衝突的可能,為了避免出現這種情況,實現時結合Active和Standby的nnId分配獨立的keyId序號段即可解決。

除了以上問題,服務重啟時還存在其他問題:

(1)NameNode重啟:當NameNode重啟會重置,由於NameNode重啟後所有DataNode需要重新註冊,註冊完成後返回的CMD指令中包含了NameNode的集合,保證了DataNode與NameNode之間完成同步;

(2)DataNode重啟:DataNode重啟比較簡單,向Active NameNode和Standby NameNode分別註冊,成功後會收到Active和Standby的所有集合,更新內存狀態即可。

為什麼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沒有及時同步造成後續讀寫失敗;

dst: /ip:port

at java.lang.Thread.run(Thread.java:745)

這兩個問題的主要原因是社區實現中DataNode同步SecretKey採用的是從NameNode Push的方案,但是對是否Push成功沒有感知,比如:

(1)NameNode重啟後,會重新生成新SecretKey集合,DataNode註冊時NameNode將所有新生成的SecretKey集合Push給DataNode。我們知道NameNode重啟階段負載非常高,尤其是大規模集群,存在一種情況是NameNode端成功處理了DataNode的註冊請求,並將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後該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不一致造成客戶端讀寫請求失敗的問題。

六、總結

本文以Hadoop Security特性背景入手,對HDFS BlockToken方案設計的考慮,社區實現原理,存在的問題和解決思路進行了簡單分析和梳理。

通過對BlockToken機制原理和實現細節解析,期望對Hadoop安全窺一斑見全局,對其中可能存在的問題及優化思路提供參考價值。

七、參考

[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

[6] https://issues.apache.org/jira/browse/HDFS-13473

[7] http://hadoop.apache.org/releases.html

利益相關:無。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 賀小橋 的精彩文章:

TAG:賀小橋 |