diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java index 82447378ff2..f14911402d6 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java @@ -22,8 +22,6 @@ import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.esnative.tool.HttpResponse.HttpResponseBuilder; -import javax.net.ssl.HttpsURLConnection; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -37,10 +35,11 @@ import java.security.PrivilegedAction; import java.util.Collections; import java.util.List; +import javax.net.ssl.HttpsURLConnection; + import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PORT; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PUBLISH_HOST; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PUBLISH_PORT; -import static org.elasticsearch.xpack.core.security.SecurityField.setting; /** * A simple http client for usage in command line tools. This client only uses internal jdk classes and does @@ -48,8 +47,6 @@ import static org.elasticsearch.xpack.core.security.SecurityField.setting; */ public class CommandLineHttpClient { - public static final String HTTP_SSL_SETTING = setting("http.ssl."); - /** * Timeout HTTP(s) reads after 35 seconds. * The default timeout for discovering a master is 30s, and we want to be longer than this, otherwise a querying a disconnected node @@ -83,15 +80,20 @@ public class CommandLineHttpClient { public HttpResponse execute(String method, URL url, String user, SecureString password, CheckedSupplier requestBodySupplier, CheckedFunction responseHandler) throws Exception { - HttpURLConnection conn; + final HttpURLConnection conn; // If using SSL, need a custom service because it's likely a self-signed certificate if ("https".equalsIgnoreCase(url.getProtocol())) { - Settings sslSettings = settings.getByPrefix(HTTP_SSL_SETTING); final SSLService sslService = new SSLService(settings, env); final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection(); AccessController.doPrivileged((PrivilegedAction) () -> { + final Settings sslSettings = SSLService.getHttpTransportSSLSettings(settings); // Requires permission java.lang.RuntimePermission "setFactory"; httpsConn.setSSLSocketFactory(sslService.sslSocketFactory(sslSettings)); + final boolean isHostnameVerificationEnabled = + sslService.getVerificationMode(sslSettings, Settings.EMPTY).isHostnameVerificationEnabled(); + if (isHostnameVerificationEnabled == false) { + httpsConn.setHostnameVerifier((hostname, session) -> true); + } return null; }); conn = httpsConn; @@ -162,4 +164,5 @@ public class CommandLineHttpClient { throw new UncheckedIOException("failed to resolve default URL", e); } } + } diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java index 32508cab110..d429b32ba05 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java @@ -10,6 +10,7 @@ import joptsimple.OptionSet; import joptsimple.OptionSpec; import org.bouncycastle.util.io.Streams; import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.cli.Command; import org.elasticsearch.cli.EnvironmentAwareCommand; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.LoggingAwareMultiCommand; @@ -26,6 +27,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.env.Environment; +import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.support.Validation; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; @@ -48,6 +50,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BiFunction; import static java.util.Arrays.asList; @@ -64,12 +67,15 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { private static final char[] CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").toCharArray(); public static final List USERS = asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME); - private final CheckedFunction clientFunction; + private final BiFunction clientFunction; private final CheckedFunction keyStoreFunction; + private CommandLineHttpClient client; SetupPasswordTool() { - this((environment) -> new CommandLineHttpClient(environment.settings(), environment), (environment) -> { + this((environment, settings) -> { + return new CommandLineHttpClient(settings, environment); + }, (environment) -> { KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(environment.configFile()); if (keyStoreWrapper == null) { throw new UserException(ExitCodes.CONFIG, @@ -79,8 +85,8 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { }); } - SetupPasswordTool(CheckedFunction clientFunction, - CheckedFunction keyStoreFunction) { + SetupPasswordTool(BiFunction clientFunction, + CheckedFunction keyStoreFunction) { super("Sets the passwords for reserved users"); subcommands.put("auto", newAutoSetup()); subcommands.put("interactive", newInteractiveSetup()); @@ -223,6 +229,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { private String elasticUser = ElasticUser.NAME; private SecureString elasticUserPassword; + private KeyStoreWrapper keyStoreWrapper; private URL url; SetupCommand(String description) { @@ -230,18 +237,35 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { setParser(); } - void setupOptions(OptionSet options, Environment env) throws Exception { - client = clientFunction.apply(env); - try (KeyStoreWrapper keyStore = keyStoreFunction.apply(env)) { - String providedUrl = urlOption.value(options); - url = new URL(providedUrl == null ? client.getDefaultURL() : providedUrl); - setShouldPrompt(options); - - // TODO: We currently do not support keystore passwords - keyStore.decrypt(new char[0]); - Settings build = Settings.builder().setSecureSettings(keyStore).build(); - elasticUserPassword = ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.get(build); + @Override + public void close() { + if (keyStoreWrapper != null) { + keyStoreWrapper.close(); } + if (elasticUserPassword != null) { + elasticUserPassword.close(); + } + } + + void setupOptions(OptionSet options, Environment env) throws Exception { + keyStoreWrapper = keyStoreFunction.apply(env); + // TODO: We currently do not support keystore passwords + keyStoreWrapper.decrypt(new char[0]); + + Settings.Builder settingsBuilder = Settings.builder(); + settingsBuilder.put(env.settings(), true); + if (settingsBuilder.getSecureSettings() == null) { + settingsBuilder.setSecureSettings(keyStoreWrapper); + } + Settings settings = settingsBuilder.build(); + elasticUserPassword = ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.get(settings); + + client = clientFunction.apply(env, settings); + + String providedUrl = urlOption.value(options); + url = new URL(providedUrl == null ? client.getDefaultURL() : providedUrl); + setShouldPrompt(options); + } private void setParser() { @@ -312,7 +336,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { } catch (SSLException e) { terminal.println(""); terminal.println("SSL connection to " + route.toString() + " failed: " + e.getMessage()); - terminal.println("Please check the elasticsearch SSL settings under " + CommandLineHttpClient.HTTP_SSL_SETTING); + terminal.println("Please check the elasticsearch SSL settings under " + XPackSettings.HTTP_SSL_PREFIX); terminal.println(Verbosity.VERBOSE, ""); terminal.println(Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e)); terminal.println(""); @@ -324,8 +348,8 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { terminal.println(Verbosity.VERBOSE, ""); terminal.println(Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e)); terminal.println(""); - throw new UserException(ExitCodes.CONFIG, "Failed to connect to elasticsearch at " + - route.toString() + ". Is the URL correct and elasticsearch running?", e); + throw new UserException(ExitCodes.CONFIG, + "Failed to connect to elasticsearch at " + route.toString() + ". Is the URL correct and elasticsearch running?", e); } } @@ -333,8 +357,8 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { private XPackSecurityFeatureConfig getXPackSecurityConfig(Terminal terminal) throws Exception { // Get x-pack security info. URL route = createURL(url, "/_xpack", "?categories=features&human=false&pretty"); - final HttpResponse httpResponse = client.execute("GET", route, elasticUser, elasticUserPassword, () -> null, - is -> responseBuilder(is, terminal)); + final HttpResponse httpResponse = + client.execute("GET", route, elasticUser, elasticUserPassword, () -> null, is -> responseBuilder(is, terminal)); if (httpResponse.getHttpStatus() != HttpURLConnection.HTTP_OK) { terminal.println(""); terminal.println("Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling GET " + route.toString()); @@ -453,8 +477,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { terminal.println("* Check the elasticsearch logs for additional error details."); terminal.println("* Use the change password API manually. "); terminal.println(""); - throw new UserException(ExitCodes.TEMP_FAILURE, - "Failed to set password for user [" + user + "]."); + throw new UserException(ExitCodes.TEMP_FAILURE, "Failed to set password for user [" + user + "]."); } } catch (IOException e) { terminal.println(""); @@ -516,8 +539,12 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { } return httpResponseBuilder; } - } + private URL createURL(URL url, String path, String query) throws MalformedURLException, URISyntaxException { + return new URL(url, (url.toURI().getPath() + path).replaceAll("/+", "/") + query); + } + } + private String getErrorCause(HttpResponse httpResponse) { final Object error = httpResponse.getResponseBody().get("error"); if (error == null) { @@ -544,11 +571,6 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { return error.toString(); } - private static URL createURL(URL url, String path, String query) throws MalformedURLException, URISyntaxException { - URL route = new URL(url, (url.toURI().getPath() + path).replaceAll("/+", "/") + query); - return route; - } - /** * This class is used to capture x-pack security feature configuration. */ diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java new file mode 100644 index 00000000000..d127a45d532 --- /dev/null +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.esnative.tool; + +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.util.io.Streams; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.http.MockResponse; +import org.elasticsearch.test.http.MockWebServer; +import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettingsTests; +import org.elasticsearch.xpack.core.ssl.TestsSSLService; +import org.elasticsearch.xpack.core.ssl.VerificationMode; +import org.elasticsearch.xpack.security.authc.esnative.tool.HttpResponse.HttpResponseBuilder; +import org.junit.After; +import org.junit.Before; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.security.auth.DestroyFailedException; + +/** + * This class tests {@link CommandLineHttpClient} For extensive tests related to + * ssl settings can be found {@link SSLConfigurationSettingsTests} + */ +public class CommandLineHttpClientTests extends ESTestCase { + + private MockWebServer webServer; + private Environment environment = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); + + @Before + public void setup() throws Exception { + webServer = createMockWebServer(); + webServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\"test\": \"complete\"}")); + webServer.start(); + } + + @After + public void shutdown() throws Exception { + webServer.close(); + } + + public void testCommandLineHttpClientCanExecuteAndReturnCorrectResultUsingSSLSettings() throws Exception { + Path resource = getDataPath("/org/elasticsearch/xpack/security/keystore/testnode.jks"); + MockSecureSettings secureSettings = new MockSecureSettings(); + Settings settings; + if (randomBoolean()) { + // with http ssl settings + secureSettings.setString("xpack.security.http.ssl.truststore.secure_password", "testnode"); + settings = Settings.builder().put("xpack.security.http.ssl.truststore.path", resource.toString()) + .put("xpack.security.http.ssl.verification_mode", VerificationMode.CERTIFICATE).setSecureSettings(secureSettings) + .build(); + } else { + // with global settings + secureSettings.setString("xpack.ssl.truststore.secure_password", "testnode"); + settings = Settings.builder().put("xpack.ssl.truststore.path", resource.toString()) + .put("xpack.ssl.verification_mode", VerificationMode.CERTIFICATE).setSecureSettings(secureSettings).build(); + } + CommandLineHttpClient client = new CommandLineHttpClient(settings, environment); + HttpResponse httpResponse = client.execute("GET", new URL("https://localhost:" + webServer.getPort() + "/test"), "u1", + new SecureString(new char[] { 'p' }), () -> null, is -> responseBuilder(is)); + + assertNotNull("Should have http response", httpResponse); + assertEquals("Http status code does not match", 200, httpResponse.getHttpStatus()); + assertEquals("Http response body does not match", "complete", httpResponse.getResponseBody().get("test")); + } + + private MockWebServer createMockWebServer() throws IOException, UnrecoverableKeyException, CertificateException, + NoSuchAlgorithmException, KeyStoreException, OperatorCreationException, DestroyFailedException { + Path resource = getDataPath("/org/elasticsearch/xpack/security/keystore/testnode.jks"); + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.ssl.keystore.secure_password", "testnode"); + Settings settings = + Settings.builder().put("xpack.ssl.keystore.path", resource.toString()).setSecureSettings(secureSettings).build(); + TestsSSLService sslService = new TestsSSLService(settings, environment); + return new MockWebServer(sslService.sslContext(), false); + } + + private HttpResponseBuilder responseBuilder(final InputStream is) throws IOException { + final HttpResponseBuilder httpResponseBuilder = new HttpResponseBuilder(); + if (is != null) { + byte[] bytes = Streams.readAll(is); + httpResponseBuilder.withResponseBody(new String(bytes, StandardCharsets.UTF_8)); + } + return httpResponseBuilder; + } +} diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java index ff03fcee1a5..d614afc0aeb 100644 --- a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java @@ -128,7 +128,7 @@ public class SetupPasswordToolTests extends CommandTestCase { @Override protected Command newCommand() { - return new SetupPasswordTool((e) -> httpClient, (e) -> keyStore) { + return new SetupPasswordTool((e, s) -> httpClient, (e) -> keyStore) { @Override protected AutoSetup newAutoSetup() {