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 <fdeant...@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 <fdeant...@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; }