You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by zj...@apache.org on 2019/09/10 01:35:14 UTC

[zeppelin] branch master updated: [ZEPPELIN-4324]: Support two-way SSL authentication.

This is an automated email from the ASF dual-hosted git repository.

zjffdu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 79f751a  [ZEPPELIN-4324]: Support two-way SSL authentication.
79f751a is described below

commit 79f751a0e03c9fe731a2c44238976cdab479b2d5
Author: fdeantoni <fd...@gmail.com>
AuthorDate: Mon Sep 9 12:56:54 2019 +0800

    [ZEPPELIN-4324]: Support two-way SSL authentication.
    
    ### What is this PR for?
    Livy can run behind a reverse proxy that requires SSL authentication.
    To support this, three additional properties have been added:
    
     - zeppelin.livy.ssl.keyStore
     - zeppelin.livy.ssl.keyStorePassword
     - zeppelin.livy.ssl.keyStoreType
    
    The keystore type can either be JKS or PKCS12. The default is JKS. To
    keep things streamlined, a property `zeppelin.livy.ssl.trustStoreType`
    has been been added as well. Default value is also JKS.
    
    ### What type of PR is it?
    Improvement
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/ZEPPELIN-4324
    
    ### How should this be tested?
    Set up a livy instance behind a reverse proxy (e.g. HAProxy) that requires two way SSL authentication to access it. Configure the Livy interpreter to access this instance by setting the following properties:
    
    - zeppelin.livy.ssl.keyStore: Path to keystore containing client certificate and key
    - zeppelin.livy.ssl.keyStorePassword: Password of keystore
    - zeppelin.livy.ssl.keyStoreType: Either JKS or PKCS12
    - zeppelin.livy.ssl.trustStore: Path to trust store containing proxy host certificate
    - zeppelin.livy.ssl.trustStorePassword: Password of trust store
    - zeppelin.livy.ssl.keyStoreType: Either JKS or PKCS12
    
    Author: fdeantoni <fd...@gmail.com>
    
    Closes #3441 from fdeantoni/two-way-ssl and squashes the following commits:
    
    a0f18cc7c [fdeantoni] ZEPPELIN-4324: Support two-way SSL authentication.
---
 docs/interpreter/livy.md                           |  25 ++++
 .../apache/zeppelin/livy/BaseLivyInterpreter.java  | 127 +++++++++++++--------
 2 files changed, 104 insertions(+), 48 deletions(-)

diff --git a/docs/interpreter/livy.md b/docs/interpreter/livy.md
index 954eb8c..c7a96ba 100644
--- a/docs/interpreter/livy.md
+++ b/docs/interpreter/livy.md
@@ -146,6 +146,31 @@ Example: `spark.driver.memory` to `livy.spark.driver.memory`
     <td>password for trustStore file. Used when livy ssl is enabled</td>
   </tr>
   <tr>
+    <td>zeppelin.livy.ssl.trustStoreType</td>
+    <td>JKS</td>
+    <td>type of truststore. Either JKS or PKCS12.</td>
+  </tr>
+  <tr>
+    <td>zeppelin.livy.ssl.keyStore</td>
+    <td></td>
+    <td>client keyStore file. Needed if Livy requires two way SSL authentication.</td>
+  </tr>
+  <tr>
+    <td>zeppelin.livy.ssl.keyStorePassword</td>
+    <td></td>
+    <td>password for keyStore file.</td>
+  </tr>
+  <tr>
+    <td>zeppelin.livy.ssl.keyStoreType</td>
+    <td>JKS</td>
+    <td>type of keystore. Either JKS or PKCS12.</td>
+  </tr>
+  <tr>
+    <td>zeppelin.livy.ssl.keyPassword</td>
+    <td></td>
+    <td>password for key in the keyStore file. Defaults to zeppelin.livy.ssl.keyStorePassword.</td>
+  </tr>       
+  <tr>
     <td>zeppelin.livy.http.headers</td>
     <td>key_1: value_1; key_2: value_2</td>
     <td>custom http headers when calling livy rest api. Each http header is separated by `;`, and each header is one key value pair where key value is separated by `:`</td>
diff --git a/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java
index d47a322..afaf55c 100644
--- a/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java
+++ b/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java
@@ -34,6 +34,7 @@ import org.apache.http.config.Registry;
 import org.apache.http.config.RegistryBuilder;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 import org.apache.http.conn.ssl.SSLContexts;
+import org.apache.http.conn.ssl.SSLContextBuilder;
 import org.apache.http.impl.auth.SPNegoSchemeFactory;
 import org.apache.http.impl.client.BasicCredentialsProvider;
 import org.apache.http.impl.client.HttpClientBuilder;
@@ -55,7 +56,7 @@ import org.springframework.web.client.RestTemplate;
 
 import java.io.FileInputStream;
 import java.io.IOException;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.security.KeyStore;
 import java.security.Principal;
 import java.util.ArrayList;
@@ -571,6 +572,59 @@ public abstract class BaseLivyInterpreter extends Interpreter {
     callRestAPI("/sessions/" + sessionInfo.id + "/statements/" + statementId + "/cancel", "POST");
   }
 
+  private SSLContext getSslContext() {
+    try {
+      // Build truststore
+      String trustStoreFile = getProperty("zeppelin.livy.ssl.trustStore");
+      String trustStorePassword = getProperty("zeppelin.livy.ssl.trustStorePassword");
+      String trustStoreType = getProperty("zeppelin.livy.ssl.trustStoreType",
+              KeyStore.getDefaultType());
+      if (StringUtils.isBlank(trustStoreFile)) {
+        throw new RuntimeException("No zeppelin.livy.ssl.trustStore specified for livy ssl");
+      }
+      if (StringUtils.isBlank(trustStorePassword)) {
+        throw new RuntimeException("No zeppelin.livy.ssl.trustStorePassword specified " +
+                "for livy ssl");
+      }
+      KeyStore trustStore = getStore(trustStoreFile, trustStoreType, trustStorePassword);
+      SSLContextBuilder builder = SSLContexts.custom();
+      builder.loadTrustMaterial(trustStore);
+
+      // Build keystore
+      String keyStoreFile = getProperty("zeppelin.livy.ssl.keyStore");
+      String keyStorePassword = getProperty("zeppelin.livy.ssl.keyStorePassword");
+      String keyPassword = getProperty("zeppelin.livy.ssl.keyPassword", keyStorePassword);
+      String keyStoreType = getProperty("zeppelin.livy.ssl.keyStoreType",
+              KeyStore.getDefaultType());
+      if (StringUtils.isNotBlank(keyStoreFile)) {
+        KeyStore keyStore = getStore(keyStoreFile, keyStoreType, keyStorePassword);
+        builder.loadKeyMaterial(keyStore, keyPassword.toCharArray()).useTLS();
+      }
+      return builder.build();
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create SSL Context", e);
+    }
+  }
+
+  private KeyStore getStore(String file, String type, String password) {
+    FileInputStream inputStream = null;
+    try {
+      inputStream = new FileInputStream(file);
+      KeyStore trustStore = KeyStore.getInstance(type);
+      trustStore.load(new FileInputStream(file), password.toCharArray());
+      return trustStore;
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to open keystore " + file, e);
+    } finally {
+      if (inputStream != null) {
+        try {
+          inputStream.close();
+        } catch (IOException e) {
+          LOGGER.error("Failed to close keystore file", e);
+        }
+      }
+    }
+  }
 
   private RestTemplate createRestTemplate() {
     String keytabLocation = getProperty("zeppelin.livy.keytab");
@@ -580,47 +634,32 @@ public abstract class BaseLivyInterpreter extends Interpreter {
 
     HttpClient httpClient = null;
     if (livyURL.startsWith("https:")) {
-      String keystoreFile = getProperty("zeppelin.livy.ssl.trustStore");
-      String password = getProperty("zeppelin.livy.ssl.trustStorePassword");
-      if (StringUtils.isBlank(keystoreFile)) {
-        throw new RuntimeException("No zeppelin.livy.ssl.trustStore specified for livy ssl");
-      }
-      if (StringUtils.isBlank(password)) {
-        throw new RuntimeException("No zeppelin.livy.ssl.trustStorePassword specified " +
-            "for livy ssl");
-      }
-      FileInputStream inputStream = null;
       try {
-        inputStream = new FileInputStream(keystoreFile);
-        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
-        trustStore.load(new FileInputStream(keystoreFile), password.toCharArray());
-        SSLContext sslContext = SSLContexts.custom()
-            .loadTrustMaterial(trustStore)
-            .build();
+        SSLContext sslContext = getSslContext();
         SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
         HttpClientBuilder httpClientBuilder = HttpClients.custom().setSSLSocketFactory(csf);
-        RequestConfig reqConfig = new RequestConfig() {
-          @Override
-          public boolean isAuthenticationEnabled() {
-            return true;
-          }
-        };
-        httpClientBuilder.setDefaultRequestConfig(reqConfig);
-        Credentials credentials = new Credentials() {
-          @Override
-          public String getPassword() {
-            return null;
-          }
-
-          @Override
-          public Principal getUserPrincipal() {
-            return null;
-          }
-        };
-        CredentialsProvider credsProvider = new BasicCredentialsProvider();
-        credsProvider.setCredentials(AuthScope.ANY, credentials);
-        httpClientBuilder.setDefaultCredentialsProvider(credsProvider);
         if (isSpnegoEnabled) {
+          RequestConfig reqConfig = new RequestConfig() {
+            @Override
+            public boolean isAuthenticationEnabled() {
+              return true;
+            }
+          };
+          httpClientBuilder.setDefaultRequestConfig(reqConfig);
+          Credentials credentials = new Credentials() {
+            @Override
+            public String getPassword() {
+              return null;
+            }
+
+            @Override
+            public Principal getUserPrincipal() {
+              return null;
+            }
+          };
+          CredentialsProvider credsProvider = new BasicCredentialsProvider();
+          credsProvider.setCredentials(AuthScope.ANY, credentials);
+          httpClientBuilder.setDefaultCredentialsProvider(credsProvider);
           Registry<AuthSchemeProvider> authSchemeProviderRegistry =
               RegistryBuilder.<AuthSchemeProvider>create()
                   .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory())
@@ -631,18 +670,10 @@ public abstract class BaseLivyInterpreter extends Interpreter {
         httpClient = httpClientBuilder.build();
       } catch (Exception e) {
         throw new RuntimeException("Failed to create SSL HttpClient", e);
-      } finally {
-        if (inputStream != null) {
-          try {
-            inputStream.close();
-          } catch (IOException e) {
-            LOGGER.error("Failed to close keystore file", e);
-          }
-        }
       }
     }
 
-    RestTemplate restTemplate = null;
+    RestTemplate restTemplate;
     if (isSpnegoEnabled) {
       if (httpClient == null) {
         restTemplate = new KerberosRestTemplate(keytabLocation, principal);
@@ -657,7 +688,7 @@ public abstract class BaseLivyInterpreter extends Interpreter {
       }
     }
     restTemplate.getMessageConverters().add(0,
-            new StringHttpMessageConverter(Charset.forName("UTF-8")));
+            new StringHttpMessageConverter(StandardCharsets.UTF_8));
     return restTemplate;
   }