X-Pack-Security: Making setup-passwords work with protected keystores (elastic/x-pack-elasticsearch#3918)

Changes are done in SetupPasswordTool to load the keystore
and set security settings to load password protected keys in SSL configuration.
Check for the verification mode and appropriately use hostname verifier.
Close the keystore after setup-password is complete.
Unit test for CommandLineHttpClient

TODO: TLS documentation needs to be fixed, which will be taken up as a separate
fix due to documentation refactoring in progress.

relates elastic/x-pack-elasticsearch#3760 

Original commit: elastic/x-pack-elasticsearch@72e8666e21
This commit is contained in:
Yogesh Gaikwad 2018-03-21 09:48:01 +11:00 committed by GitHub
parent 7cb5378f82
commit 57946a97df
4 changed files with 163 additions and 36 deletions

View File

@ -22,8 +22,6 @@ import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken
import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.SSLService;
import org.elasticsearch.xpack.security.authc.esnative.tool.HttpResponse.HttpResponseBuilder; import org.elasticsearch.xpack.security.authc.esnative.tool.HttpResponse.HttpResponseBuilder;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -37,10 +35,11 @@ import java.security.PrivilegedAction;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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_PORT;
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PUBLISH_HOST; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PUBLISH_HOST;
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PUBLISH_PORT; 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 * 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 class CommandLineHttpClient {
public static final String HTTP_SSL_SETTING = setting("http.ssl.");
/** /**
* Timeout HTTP(s) reads after 35 seconds. * 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 * 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, public HttpResponse execute(String method, URL url, String user, SecureString password,
CheckedSupplier<String, Exception> requestBodySupplier, CheckedSupplier<String, Exception> requestBodySupplier,
CheckedFunction<InputStream, HttpResponseBuilder, Exception> responseHandler) throws Exception { CheckedFunction<InputStream, HttpResponseBuilder, Exception> responseHandler) throws Exception {
HttpURLConnection conn; final HttpURLConnection conn;
// If using SSL, need a custom service because it's likely a self-signed certificate // If using SSL, need a custom service because it's likely a self-signed certificate
if ("https".equalsIgnoreCase(url.getProtocol())) { if ("https".equalsIgnoreCase(url.getProtocol())) {
Settings sslSettings = settings.getByPrefix(HTTP_SSL_SETTING);
final SSLService sslService = new SSLService(settings, env); final SSLService sslService = new SSLService(settings, env);
final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection(); final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection();
AccessController.doPrivileged((PrivilegedAction<Void>) () -> { AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
final Settings sslSettings = SSLService.getHttpTransportSSLSettings(settings);
// Requires permission java.lang.RuntimePermission "setFactory"; // Requires permission java.lang.RuntimePermission "setFactory";
httpsConn.setSSLSocketFactory(sslService.sslSocketFactory(sslSettings)); httpsConn.setSSLSocketFactory(sslService.sslSocketFactory(sslSettings));
final boolean isHostnameVerificationEnabled =
sslService.getVerificationMode(sslSettings, Settings.EMPTY).isHostnameVerificationEnabled();
if (isHostnameVerificationEnabled == false) {
httpsConn.setHostnameVerifier((hostname, session) -> true);
}
return null; return null;
}); });
conn = httpsConn; conn = httpsConn;
@ -162,4 +164,5 @@ public class CommandLineHttpClient {
throw new UncheckedIOException("failed to resolve default URL", e); throw new UncheckedIOException("failed to resolve default URL", e);
} }
} }
} }

View File

@ -10,6 +10,7 @@ import joptsimple.OptionSet;
import joptsimple.OptionSpec; import joptsimple.OptionSpec;
import org.bouncycastle.util.io.Streams; import org.bouncycastle.util.io.Streams;
import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.EnvironmentAwareCommand; import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.LoggingAwareMultiCommand; 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.XContentBuilder;
import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.env.Environment; 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.support.Validation;
import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser;
import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.ElasticUser;
@ -48,6 +50,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.BiFunction;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@ -64,12 +67,15 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
private static final char[] CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").toCharArray(); private static final char[] CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").toCharArray();
public static final List<String> USERS = asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME); public static final List<String> USERS = asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME);
private final CheckedFunction<Environment, CommandLineHttpClient, Exception> clientFunction; private final BiFunction<Environment, Settings, CommandLineHttpClient> clientFunction;
private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction; private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction;
private CommandLineHttpClient client; private CommandLineHttpClient client;
SetupPasswordTool() { SetupPasswordTool() {
this((environment) -> new CommandLineHttpClient(environment.settings(), environment), (environment) -> { this((environment, settings) -> {
return new CommandLineHttpClient(settings, environment);
}, (environment) -> {
KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(environment.configFile()); KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(environment.configFile());
if (keyStoreWrapper == null) { if (keyStoreWrapper == null) {
throw new UserException(ExitCodes.CONFIG, throw new UserException(ExitCodes.CONFIG,
@ -79,8 +85,8 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
}); });
} }
SetupPasswordTool(CheckedFunction<Environment, CommandLineHttpClient, Exception> clientFunction, SetupPasswordTool(BiFunction<Environment, Settings, CommandLineHttpClient> clientFunction,
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction) { CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction) {
super("Sets the passwords for reserved users"); super("Sets the passwords for reserved users");
subcommands.put("auto", newAutoSetup()); subcommands.put("auto", newAutoSetup());
subcommands.put("interactive", newInteractiveSetup()); subcommands.put("interactive", newInteractiveSetup());
@ -223,6 +229,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
private String elasticUser = ElasticUser.NAME; private String elasticUser = ElasticUser.NAME;
private SecureString elasticUserPassword; private SecureString elasticUserPassword;
private KeyStoreWrapper keyStoreWrapper;
private URL url; private URL url;
SetupCommand(String description) { SetupCommand(String description) {
@ -230,18 +237,35 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
setParser(); setParser();
} }
void setupOptions(OptionSet options, Environment env) throws Exception { @Override
client = clientFunction.apply(env); public void close() {
try (KeyStoreWrapper keyStore = keyStoreFunction.apply(env)) { if (keyStoreWrapper != null) {
String providedUrl = urlOption.value(options); keyStoreWrapper.close();
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);
} }
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() { private void setParser() {
@ -312,7 +336,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
} catch (SSLException e) { } catch (SSLException e) {
terminal.println(""); terminal.println("");
terminal.println("SSL connection to " + route.toString() + " failed: " + e.getMessage()); 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, "");
terminal.println(Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e)); terminal.println(Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e));
terminal.println(""); terminal.println("");
@ -324,8 +348,8 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
terminal.println(Verbosity.VERBOSE, ""); terminal.println(Verbosity.VERBOSE, "");
terminal.println(Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e)); terminal.println(Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e));
terminal.println(""); terminal.println("");
throw new UserException(ExitCodes.CONFIG, "Failed to connect to elasticsearch at " + throw new UserException(ExitCodes.CONFIG,
route.toString() + ". Is the URL correct and elasticsearch running?", e); "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 { private XPackSecurityFeatureConfig getXPackSecurityConfig(Terminal terminal) throws Exception {
// Get x-pack security info. // Get x-pack security info.
URL route = createURL(url, "/_xpack", "?categories=features&human=false&pretty"); URL route = createURL(url, "/_xpack", "?categories=features&human=false&pretty");
final HttpResponse httpResponse = client.execute("GET", route, elasticUser, elasticUserPassword, () -> null, final HttpResponse httpResponse =
is -> responseBuilder(is, terminal)); client.execute("GET", route, elasticUser, elasticUserPassword, () -> null, is -> responseBuilder(is, terminal));
if (httpResponse.getHttpStatus() != HttpURLConnection.HTTP_OK) { if (httpResponse.getHttpStatus() != HttpURLConnection.HTTP_OK) {
terminal.println(""); terminal.println("");
terminal.println("Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling GET " + route.toString()); 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("* Check the elasticsearch logs for additional error details.");
terminal.println("* Use the change password API manually. "); terminal.println("* Use the change password API manually. ");
terminal.println(""); terminal.println("");
throw new UserException(ExitCodes.TEMP_FAILURE, throw new UserException(ExitCodes.TEMP_FAILURE, "Failed to set password for user [" + user + "].");
"Failed to set password for user [" + user + "].");
} }
} catch (IOException e) { } catch (IOException e) {
terminal.println(""); terminal.println("");
@ -516,8 +539,12 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
} }
return httpResponseBuilder; 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) { private String getErrorCause(HttpResponse httpResponse) {
final Object error = httpResponse.getResponseBody().get("error"); final Object error = httpResponse.getResponseBody().get("error");
if (error == null) { if (error == null) {
@ -544,11 +571,6 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
return error.toString(); 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. * This class is used to capture x-pack security feature configuration.
*/ */

View File

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

View File

@ -128,7 +128,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
@Override @Override
protected Command newCommand() { protected Command newCommand() {
return new SetupPasswordTool((e) -> httpClient, (e) -> keyStore) { return new SetupPasswordTool((e, s) -> httpClient, (e) -> keyStore) {
@Override @Override
protected AutoSetup newAutoSetup() { protected AutoSetup newAutoSetup() {