##前言
KMS
是Hadoop下的一個密鑰管理服務,它實際是與Hadoop結合,提供HDFS文件做AES加密用的。所以它是用來存儲AES祕鑰的,AES提供三種位數的祕鑰,分別是128
, 192
, 256
,所以KMS只能存儲這三種位數的byte
數組。
如果你是爲了給HDFS文件加密,那麼直接通過配置就可以完成,它與Hadoop能夠完美契合,由Hadoop調用自動產生祕鑰並管理祕鑰。但是HDFS文件加密粒度太粗,我們的數據並非要全部加密,而當前針對Hive表列的加密並沒有集成方案,官方提供了AES的encrypt函數,但是祕鑰key還需明文傳入。這樣對集羣來說其實很不安全。
如果我們自己實現加密UDF,然後借用KMS來做密鑰管理,在KMS上加上Kerberos認證,祕鑰的處理就可以都封裝在UDF內部,不對外暴露,而且可以實現身份認證。
KMS本身提供了一系列API來創建,獲取和維護密鑰,官網介紹中主要以RESTFUL的形式提供,但如果集羣上了Kerberos,請求的認證在RESTFULL裏就不好做(具體沒操作過)。在Hadoop源碼裏,提供了KMSClientProvider
用於Hadoop的加密,所以我們可以利用這個接口來獲取KMS服務,實現創建管理密鑰。
##配置
-
KMS是一個Web服務,只需要在一臺機器上配置即可,其主要配置文件是
kms-site.xml
,主要配置項是hadoop.kms.key.provider.uri
,配置值是KMS的key以文件形式存在哪個keystore
文件裏,配置格式是jceks://file@/path/to/kms.keystore
,如jceks://file@/home/kms/kms.keystore
,當然,服務最好以kms
用戶來起。這個文件會在KMS起來後生成。之後在kms-env.sh
裏配置export KMS_LOG=/path/to/log
和export KMS_TEMP=/path/to/log
。kms.keystore
文件本身和裏面的存儲密鑰都有密碼保護,默認配置項爲hadoop.security.keystore.java-keystore-provider.password-file
,密碼存儲在文件裏,不可換行,由於KMS是通過ClassLoader.getResource
來加載該文件,所以該配置必須配在KMS Web服務啓動對應的conf目錄下。此外也可通過環境變量設置,爲HADOOP_KEYSTORE_PASSWORD
,可將其配置在kms-env.sh
裏,環境變量的設置優先級最高! -
然後在hadoop的
core-site.xml
裏配上hadoop.security.key.provider.path
,未啓用https,其值爲kms://http@${hostname}:16000/kms
,如果啓用了https,則應爲kms://https@${hostname}:16000/kms
。 -
以上兩步配完後,重啓HDFS,然後以
kms
身份,啓動KMS(/path/to/hadoop/sbin/kms.sh start
),啓動完後,就可以用/path/to/hadoop/bin/hadoop key list -metadata
來查看KMS裏存儲的Key了,當然,還沒有創建key,所以沒有key信息,但是可以驗證KMS服務是否配置正確。其次,這個命令雖然可以創建key,但是隻能創建隨機key,不能創建指定key。 -
配置SSL(https),確保傳輸過程加密。SSL需要用到證書,可以去CA官網下載一個證書作爲網站根證書和信任證書,也可以用Java生成一個自簽名證書並添加它爲受信任證書。詳細介紹可以參考CDH官網,我們這裏採用自簽名證書。
- 以
kms
用戶生成tomcat根證書(此根證書只能爲當前機器上的Web服務所用,其他機器上的web服務如果需要SSL,也需要像這個一樣單獨生成該服務器的根證書。其次,該證書只是做SSL通信安全加密所用,並不具備可信任性,因爲不是權威機構頒發),執行/usr/java/default/bin/keytool -genkey -alias tomcat -keyalg RSA
,過程中問到"What is your first and last name?"
時,必須填寫運行KMS Service那臺機器的hostname,然後會提示輸入keystore
的密碼,這個密碼假定爲xxx.c0m
,需要記住,後面配置時需要用到它。這一步執行完後,會在kms
用戶的home目錄下生成.keystore
文件(可用/path/to/java/bin/keytool -list -v -keystore .keystore -storepass xxx.c0m
來顯示當前keystore裏可用的證書)。 - 配置
kms-env.sh
,添加證書的位置和密碼,即export KMS_SSL_KEYSTORE_FILE=/home/kms/.keystore
和export KMS_SSL_KEYSTORE_PASS=xxx.c0m
,然後更改core-site.xml
裏的hadoop.security.key.provider.path
爲https
。到這裏KMS的SSL算是配完了,但是重啓HDFS和KMS後,發現 list 祕鑰會報錯: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target),這是因爲我們沒有添加證書爲受信任根證書,訪問並不認同當前根證書。 - 以
kms
用戶導出根證書爲crt
文件:/usr/java/default/bin/keytool -export -alias tomcat -keystore /home/kms/.keystore -file /home/kms/tomcat.crt -storepass xxx.c0m
,這裏就要用到上面的密碼。這一步是爲了添加受信任證書做準備,當前證書被稱作keystore,受信任證書是truststore,java truststore有幾個不同的判斷維度,可參考這裏 - 因爲我們並沒有配置
javax.net.ssl.trustStore
(也可以採用配置這個文件),也沒有<jre-home>/lib/security/jssecacerts
文件,所以它會使用<jre-home>/lib/security/cacerts
作爲受信任證書文件,而這裏面並沒有我們的KMS根證書。以root
用戶操作,執行cp $JAVA_HOME/jre/lib/security/cacerts $JAVA_HOME/jre/lib/security/jssecacerts
,然後將剛導出的根證書添加到受信任證書jssecacerts裏,即/usr/java/default/bin/keytool -import -alias tomcat -keystore /usr/java/default/jre/lib/security/jssecacerts -file /home/kms/tomcat.crt -storepass changeit
,這裏的密碼是jssecacerts
的密碼,默認是changeit
- 上面一步做完後,本機上任何賬戶都可以使用KMS服務,至此KMS的SSL就配完了。這一步的過程實際是把
/home/kms/.keystore
的公鑰導入到了jssecacerts
文件裏,私鑰還在原文件裏。 - 要想其他機器也正常訪問KMS,我們需要把
jssecacerts
拷貝到其他機器<jre-home>/lib/security/
目錄下。
- 配置KMS Kerberos
KMS需要
HTTP
的憑據,在KMS服務機器上生成憑據,配置kms-site.xml
文件,設置hadoop.kms.authentication.type
爲kerberos
,然後添加hadoop.kms.authentication.kerberos.keytab
和hadoop.kms.authentication.kerberos.principal
,設置hadoop.kms.authentication.kerberos.name.rules
爲DEFAULT
- 在CDH裏配置KMS:CDH裏配置很簡單,在Cluster界面,Actions -> Add a Service,然後添加Java KeyStore Service,然後一步步走配置流程即可,SSL的配置與上面的一樣。然後在Set Up HDFS Dependency這一步裏,點擊關閉,不配置HDFS文件加密。對於Kerberos配置,選擇Administration -> Security -> Kerberos Credentials,查看是否有當前主機的HTTP憑據,沒有就生成一個
- 配置完後,我們可以使用
hadoop key list
來查看當前存儲的密鑰,如果報錯沒有配置provider,我們可以這麼用:hadoop key list -metadata -provider kms://https@${hostname}:16000/kms
,需帶上provider - 配置KMS祕鑰訪問權限,配置文件是kms-acls.xml,KMS可整體控制祕鑰的權限,也可單獨就某個祕鑰配置它的具體權限,並且支持白名單和黑名單,策略是先白名單後黑名單。在開源Hadoop上,這個配置是熱加載的,但是在CDH裏改了它之後需要重啓KMS服務。配置示例如下:
<property>
<name>hadoop.kms.acl.CREATE</name>
<value>*</value>
</property>
<property>
<name>hadoop.kms.blacklist.CREATE</name>
<value>hdfs,hive</value>
</property>
<property>
<name>hadoop.kms.acl.DELETE</name>
<value>*</value>
</property>
<property>
<name>hadoop.kms.blacklist.DELETE</name>
<value>hdfs,hive</value>
</property>
<property>
<name>hadoop.kms.acl.ROLLOVER</name>
<value>*</value>
</property>
<property>
<name>hadoop.kms.blacklist.ROLLOVER</name>
<value>*</value>
</property>
<property>
<name>hadoop.kms.acl.GET</name>
<value>kavn,hive</value>
</property>
<property>
<name>hadoop.kms.blacklist.GET</name>
<value>hdfs</value>
</property>
<property>
<name>hadoop.kms.acl.GET_KEYS</name>
<value>*</value>
</property>
<property>
<name>hadoop.kms.blacklist.GET_KEYS</name>
<value>hdfs,hive</value>
</property>
<property>
<name>hadoop.kms.acl.GET_METADATA</name>
<value></value>
</property>
<property>
<name>hadoop.kms.blacklist.GET_METADATA</name>
<value>hdfs,hive</value>
</property>
<property>
<name>hadoop.kms.blacklist.GENERATE_EEK</name>
<value>*</value>
</property>
<property>
<name>hadoop.kms.blacklist.DECRYPT_EEK</name>
<value>*</value>
</property>
<!-- 要使用戶具備create key的權限,必須同時有 acl.CREATE 和 acl.SET_KEY_MATERIAL的權限,缺一不可 -->
<property>
<name>hadoop.kms.acl.SET_KEY_MATERIAL</name>
<value>*</value>
</property>
<property>
<name>hadoop.kms.blacklist.SET_KEY_MATERIAL</name>
<value>hdfs,hive</value>
</property>
<!-- 以下是對單個key做權限控制,下面的是默認配置項,當用戶create key時,如果沒有配置下面的默認配置項,用戶是沒法成功創建key的。因爲創建一個新key的名稱無法預料,在創建新key時後臺去校驗用戶對該key的權限就會失敗,所以需用這個默認列表 -->
<property>
<name>default.key.acl.MANAGEMENT</name>
<value>*</value>
</property>
<property>
<name>default.key.acl.READ</name>
<value>*</value>
</property>
<property>
<name>default.key.acl.ALL</name>
<value>*</value>
</property>
<!-- 以下是單個key的具體配置項,單個key的權限是在上面全部key權限判別之後的 -->
<property>
<name>key.acl.key_name.MANAGEMENT</name>
<value></value>
</property>
<property>
<name>key.acl.key_name.READ</name>
<value>kavn</value>
</property>
<property>
<name>key.acl.key_name.ALL</name>
<value></value>
</property>
9、 至此整個KMS就配置完成了,訪問KMS服務就需要以下三個條件:
- 有服務器的受信任證書(如這裏的 jssecacerts)
- 有kerberos認證並且票據沒過期
- 具備相應Key的訪問權限
##訪問代碼集成
KMS是在集羣環境中訪問,想要做加密就必須有身份認證,而身份認證就是Kerberos. 這裏KeyProviderFactory
內部封裝了Kerberos認證(實際通過UGI來做的),我們通過調用它拿到KMS的訪問實例,從而實現Kerberos集羣環境下的祕鑰管理。當用戶運行這段代碼時,可以使用當前用戶的身份認證,也可以利用UGI使用其他用戶的身份認證,達到祕鑰權限控制的目的。
這裏採用單例模式,但在獲取Instance的時候,加了獲取KeyProvider
的邏輯,這是因爲同一代碼裏可能會有多個不同的賬戶需要訪問祕鑰,每次訪問祕鑰都用新的賬戶去做Kerberos認證,可以保證權限正確。不會因爲第一次請求之後,以後的用戶請求都用成了第一次請求用戶的Kerberos憑據。
package encryption.codec;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.crypto.key.KeyProvider;
import org.apache.hadoop.crypto.key.KeyProviderFactory;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.Serializable;
import java.util.List;
/**
* KMS祕鑰創建和獲取類<br/>
*/
public class KeyManagement implements Serializable {
private static final Logger logger = Logger.getLogger(KeyManagement.class);
private static KeyProvider provider = null;
private static final String KEY_PROVIDER_PATH = "hadoop.security.key.provider.path";
private static final Configuration conf = new Configuration();
private static final String KMS = "kms://[email protected]:16000/kms";
private KeyManagement() {
}
private static class KeyManagementInstance {
private final static KeyManagement instance = new KeyManagement();
}
public static KeyManagement getInstance() {
provider = getKeyProvider();
return KeyManagementInstance.instance;
}
/**
* 獲取KeyProvider
* @return
*/
private static KeyProvider getKeyProvider() {
// 此處因爲拿不到KEY_PROVIDER_PATH配置,所以做了硬編碼
conf.set(KEY_PROVIDER_PATH, KMS);
KeyProvider provider = null;
List<KeyProvider> providers;
try {
providers = KeyProviderFactory.getProviders(conf);
for (KeyProvider p : providers) {
if (!p.isTransient()) {
provider = p;
break;
}
}
} catch (IOException ex) {
logger.error("Get KeyProvider failed! " + ex.getMessage());
ex.printStackTrace();
}
return provider;
}
/**
* 查看當前KMS裏有哪些Key,以及Key的信息
* @return
*/
public String[] listAllKeys() {
try {
final List<String> keys = provider.getKeys();
final KeyProvider.Metadata[] meta = provider.getKeysMetadata(keys.toArray(new String[keys.size()]));
String[] out = new String[keys.size()];
for (int i = 0; i < meta.length; ++i) {
out[i] = keys.get(i) + " : " + meta[i];
}
return out;
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
/**
* 獲取當前key的祕鑰
* @param name
* @return
* @throws IOException
*/
public byte[] getCurrentKey(String name) throws IOException {
if (null == provider) {
logger.error("KeyProvider is null!");
return null;
}
return provider.getCurrentKey(name).getMaterial();
}
/**
* 根據名稱,描述,祕鑰來創建一個Key, 祕鑰位長爲128位
* @param name
* @param description
* @param material
* @throws IOException
*/
public void createKey(String name, String description, byte[] material) throws IOException {
createKey(name, description, BitLength.ONE_TWO_EIGHT, material);
}
/**
* 根據名稱,描述,祕鑰位長和祕鑰來創建一個Key
* @param name
* @param description
* @param bitLengthOfKey
* @param material
* @throws IOException
*/
public void createKey(String name, String description, BitLength bitLengthOfKey, byte[] material) throws IOException {
final KeyProvider.Options options = KeyProvider.options(conf);
options.setDescription(description);
int length = 8 * material.length;
try {
switch (bitLengthOfKey) {
case ONE_TWO_EIGHT:
if (length == 128) {
options.setBitLength(128);
break;
} else {
throw new IllegalArgumentException("Wrong key length. Required 128, but got " + length);
}
case ONE_NIGHT_TWO:
if (length == 192) {
options.setBitLength(192);
break;
} else {
throw new IllegalArgumentException("Wrong key length. Required 192, but got " + length);
}
case TWO_FIVE_SIX:
if (length == 256) {
options.setBitLength(256);
break;
} else {
throw new IllegalArgumentException("Wrong key length. Required 256, but got " + length);
}
}
provider.createKey(name, material, options);
provider.flush();
logger.info(name + " has been successfully created with options "
+ options.toString() + ".");
} catch (Exception ex) {
logger.error(name + " has not been created. " + ex.getMessage());
throw ex;
}
}
}
enum BitLength {
ONE_TWO_EIGHT, ONE_NIGHT_TWO, TWO_FIVE_SIX
}
updata: 2017-03-25 對文中描述不全和之前的理解不到位做了修改補充
歡迎轉載,但請註明出處:https://my.oschina.net/u/2539801/blog/807974