KMS密鑰管理服務(Hadoop)

##前言 KMS是Hadoop下的一個密鑰管理服務,它實際是與Hadoop結合,提供HDFS文件作AES加密用的。因此它是用來存儲AES祕鑰的,AES提供三種位數的祕鑰,分別是128, 192, 256,因此KMS只能存儲這三種位數的byte數組。php

若是你是爲了給HDFS文件加密,那麼直接經過配置就能夠完成,它與Hadoop可以完美契合,由Hadoop調用自動產生祕鑰並管理祕鑰。可是HDFS文件加密粒度太粗,咱們的數據並不是要所有加密,而當前針對Hive表列的加密並無集成方案,官方提供了AES的encrypt函數,可是祕鑰key還需明文傳入。這樣對集羣來講其實很不安全。html

若是咱們本身實現加密UDF,而後借用KMS來作密鑰管理,在KMS上加上Kerberos認證,祕鑰的處理就能夠都封裝在UDF內部,不對外暴露,並且能夠實現身份認證。java

KMS自己提供了一系列API來建立,獲取和維護密鑰,官網介紹中主要以RESTFUL的形式提供,但若是集羣上了Kerberos,請求的認證在RESTFULL裏就很差作(具體沒操做過)。在Hadoop源碼裏,提供了KMSClientProvider用於Hadoop的加密,因此咱們能夠利用這個接口來獲取KMS服務,實現建立管理密鑰。web

##配置apache

  1. 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/logexport KMS_TEMP=/path/to/logkms.keystore文件自己和裏面的存儲密鑰都有密碼保護,默認配置項爲hadoop.security.keystore.java-keystore-provider.password-file,密碼存儲在文件裏,不可換行,因爲KMS是經過ClassLoader.getResource來加載該文件,因此該配置必須配在KMS Web服務啓動對應的conf目錄下。此外也可經過環境變量設置,爲HADOOP_KEYSTORE_PASSWORD,可將其配置在kms-env.sh裏,環境變量的設置優先級最高!數組

  2. 而後在hadoop的core-site.xml裏配上hadoop.security.key.provider.path,未啓用https,其值爲kms://http@${hostname}:16000/kms,若是啓用了https,則應爲kms://https@${hostname}:16000/kmstomcat

  3. 以上兩步配完後,重啓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。安全

  4. 配置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/.keystoreexport KMS_SSL_KEYSTORE_PASS=xxx.c0m,而後更改core-site.xml裏的hadoop.security.key.provider.pathhttps。到這裏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/目錄下。
  1. 配置KMS Kerberos KMS須要HTTP的憑據,在KMS服務機器上生成憑據,配置kms-site.xml文件,設置hadoop.kms.authentication.typekerberos,而後添加hadoop.kms.authentication.kerberos.keytabhadoop.kms.authentication.kerberos.principal,設置hadoop.kms.authentication.kerberos.name.rulesDEFAULT
  2. 在CDH裏配置KMS:CDH裏配置很簡單,在Cluster界面,Actions -> Add a Service,而後添加Java KeyStore Service,而後一步步走配置流程便可,SSL的配置與上面的同樣。而後在Set Up HDFS Dependency這一步裏,點擊關閉,不配置HDFS文件加密。對於Kerberos配置,選擇Administration -> Security -> Kerberos Credentials,查看是否有當前主機的HTTP憑據,沒有就生成一個
  3. 配置完後,咱們可使用hadoop key list來查看當前存儲的密鑰,若是報錯沒有配置provider,咱們能夠這麼用:hadoop key list -metadata -provider kms://https@${hostname}:16000/kms,需帶上provider
  4. 配置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>

九、 至此整個KMS就配置完成了,訪問KMS服務就須要如下三個條件:oracle

  • 有服務器的受信任證書(如這裏的 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://https@hostname.domain: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 對文中描述不全和以前的理解不到位作了修改補充

歡迎轉載,但請註明出處:http://www.javashuo.com/article/p-bchodjwf-gs.html

相關文章
相關標籤/搜索