KMS密鑰管理服務(Hadoop) 原

##前言 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服務,實現創建管理密鑰。

##配置

  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/kms

  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>

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章