Skip to content

Commit

Permalink
elastic user password reset CLI tool
Browse files Browse the repository at this point in the history
This change introduces a new CLI tool that allows users to reset
the password for the elastic user, setting it to a user defined
or an auto-generated value.

Resolves: elastic#70113
  • Loading branch information
jkakavas committed Jul 4, 2021
1 parent 104617d commit f67eaf1
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash

# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. Licensed under the Elastic License
# 2.0; you may not use this file except in compliance with the Elastic License
# 2.0.

ES_MAIN_CLASS=org.elasticsearch.xpack.security.tool.ResetElasticPasswordTool \
ES_ADDITIONAL_SOURCES="x-pack-env;x-pack-security-env" \
"`dirname "$0"`"/elasticsearch-cli \
"$@"
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@
*/
public abstract class BaseRunAsSuperuserCommand extends KeyStoreAwareCommand {

private static final String[] ROLES = new String[] { "superuser" };
private static final int PASSWORD_LENGTH = 14;

private final Function<Environment, CommandLineHttpClient> clientFunction;
private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction;
private SecureString password;
private String username;
private static final String[] ROLES = new String[] { "superuser" };
final SecureRandom secureRandom = new SecureRandom();

public BaseRunAsSuperuserCommand(
Expand Down Expand Up @@ -92,7 +94,7 @@ protected final void execute(Terminal terminal, OptionSet options, Environment e
ensureFileRealmEnabled(settings);
try {
final Hasher hasher = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings));
password = new SecureString(generatePassword());
password = new SecureString(generatePassword(PASSWORD_LENGTH));
username = generateUsername();

final Path passwordFile = FileUserPasswdStore.resolveFile(newEnv);
Expand Down Expand Up @@ -199,7 +201,7 @@ private void checkClusterHealthWithRetries(Environment env, Terminal terminal, i
try {
response = client.execute("GET", clusterHealthUrl, username, password, () -> null, this::responseBuilder);
} catch (Exception e) {
throw new UserException(ExitCodes.UNAVAILABLE, "Failed to determine the health of the cluster. " + e.getMessage());
throw new UserException(ExitCodes.UNAVAILABLE, "Failed to determine the health of the cluster. ", e);
}
final int responseStatus = response.getHttpStatus();
if (responseStatus != HttpURLConnection.HTTP_OK) {
Expand Down Expand Up @@ -233,16 +235,15 @@ private void checkClusterHealthWithRetries(Environment env, Terminal terminal, i
}
}

private HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws IOException {
HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws IOException {
final HttpResponse.HttpResponseBuilder httpResponseBuilder = new HttpResponse.HttpResponseBuilder();
final String responseBody = Streams.readFully(is).utf8ToString();
httpResponseBuilder.withResponseBody(responseBody);
return httpResponseBuilder;
}

private char[] generatePassword() {
char[] generatePassword(int passwordLength) {
final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
int passwordLength = 14;
char[] characters = new char[passwordLength];
for (int i = 0; i < passwordLength; ++i) {
characters[i] = passwordChars[secureRandom.nextInt(passwordChars.length)];
Expand All @@ -260,7 +261,7 @@ private String generateUsername() {
return "enrollment_autogenerated_" + new String(characters);
}

private URL createURL(URL url, String path, String query) throws MalformedURLException, URISyntaxException {
URL createURL(URL url, String path, String query) throws MalformedURLException, URISyntaxException {
return new URL(url, (url.toURI().getPath() + path).replaceAll("/+", "/") + query);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.security.tool;

import joptsimple.OptionSet;

import joptsimple.OptionSpecBuilder;

import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.KeyStoreWrapper;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.core.CheckedFunction;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.core.security.support.Validation;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.function.Function;

public class ResetElasticPasswordTool extends BaseRunAsSuperuserCommand {

private final Function<Environment, CommandLineHttpClient> clientFunction;
private final OptionSpecBuilder interactive;
private final OptionSpecBuilder auto;
private final OptionSpecBuilder batch;

ResetElasticPasswordTool() {
this(environment -> new CommandLineHttpClient(environment), environment -> KeyStoreWrapper.load(environment.configFile()));
}

public static void main(String[] args) throws Exception {
exit(new ResetElasticPasswordTool().main(args, Terminal.DEFAULT));
}

protected ResetElasticPasswordTool(
Function<Environment, CommandLineHttpClient> clientFunction,
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction) {
super(clientFunction, keyStoreFunction);
interactive = parser.acceptsAll(List.of("i", "interactive"));
auto = parser.acceptsAll(List.of("a", "auto")); // default
batch = parser.acceptsAll(List.of("b", "batch"));
this.clientFunction = clientFunction;
}

@Override
protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
final SecureString elasticPassword;
if (options.has(interactive)) {
if (options.has(batch) == false) {
terminal.println("This tool will reset the password of the [elastic] user.");
terminal.println("You will be prompted to enter the password.");
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
terminal.println("\n");
if (shouldContinue == false) {
throw new UserException(ExitCodes.OK, "User cancelled operation");
}
}
elasticPassword = promptForPassword(terminal);
} else {
if (options.has(batch) == false) {
terminal.println("This tool will reset the password of the [elastic] user to an autogenerated value.");
terminal.println("The password will be printed in the console.");
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
terminal.println("\n");
if (shouldContinue == false) {
throw new UserException(ExitCodes.OK, "User cancelled operation");
}
}
elasticPassword = new SecureString(generatePassword(20));
}
try (SecureString fileRealmSuperuserPassword = getPassword()) {
final String fileRealmSuperuser = getUsername();
final CommandLineHttpClient client = clientFunction.apply(env);
final URL changePasswordUrl = createURL(new URL(client.getDefaultURL()), "_security/user/elastic/_password", "?pretty");
final HttpResponse httpResponse = client.execute(
"POST",
changePasswordUrl,
fileRealmSuperuser,
fileRealmSuperuserPassword,
() -> requestBodySupplier(elasticPassword),
this::responseBuilder
);
if (httpResponse.getHttpStatus() != HttpURLConnection.HTTP_OK) {
throw new UserException(ExitCodes.TEMP_FAILURE, "Failed to reset password for the elastic user");
} else {
if (options.has(interactive)) {
terminal.println("Password for the elastic user successfully reset.");
} else {
terminal.println("Password for the elastic user successfully reset.");
terminal.println("New value: " + elasticPassword);
}
}
} catch (Exception e) {
throw new UserException(ExitCodes.TEMP_FAILURE, "Failed to reset password for the elastic user", e);
} finally {
elasticPassword.close();
}
}

private SecureString promptForPassword(Terminal terminal) {
while (true) {
SecureString password1 = new SecureString(terminal.readSecret("Enter password for [elastic]: "));
Validation.Error err = Validation.Users.validatePassword(password1);
if (err != null) {
terminal.errorPrintln(err.toString());
terminal.errorPrintln("Try again.");
password1.close();
continue;
}
try (SecureString password2 = new SecureString(terminal.readSecret("Reenter password for [elastic]: "))) {
if (password1.equals(password2) == false) {
terminal.errorPrintln("Passwords do not match.");
terminal.errorPrintln("Try again.");
password1.close();
continue;
}
}
return password1;
}
}

private String requestBodySupplier(SecureString pwd) throws Exception {
XContentBuilder xContentBuilder = JsonXContent.contentBuilder();
xContentBuilder.startObject().field("password", pwd.toString()).endObject();
return Strings.toString(xContentBuilder);
}

@Override
protected void validate(Terminal terminal, OptionSet options, Environment env) throws Exception {
if ((options.has("i") || options.has("interactive")) && (options.has("a") || options.has("auto"))) {
throw new UserException(ExitCodes.USAGE, "You can only run the tool in one of [auto] or [interactive] modes");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -90,39 +89,14 @@ public void setup() throws Exception {
Files.createDirectories(confDir);
Files.write(confDir.resolve("users"), List.of(), StandardCharsets.UTF_8);
Files.write(confDir.resolve("users_roles"), List.of(), StandardCharsets.UTF_8);
final Path httpCaPath = confDir.resolve("httpCa.p12");
final Path srcHttpCaPath =
Paths.get(getClass().getResource("/org/elasticsearch/xpack/security/enrollment/tool/httpCa.p12").toURI()).toAbsolutePath()
.normalize();
Files.copy(srcHttpCaPath, httpCaPath);
final Path srcTransportPath = Paths.get(getClass().getResource("/org/elasticsearch/xpack/security/enrollment/tool/transport.p12")
.toURI()).toAbsolutePath().normalize();
final Path transportPath = confDir.resolve("transport.p12");
Files.copy(srcTransportPath, transportPath);
settings = Settings.builder()
.put("path.home", homeDir)
.put("xpack.security.enabled", true)
.put("xpack.security.enrollment.enabled", true)
.put("xpack.security.authc.api_key.enabled", true)
.put("xpack.security.http.ssl.enabled", true)
.put("xpack.security.http.ssl.keystore.path", "/work/" + httpCaPath)
.put("xpack.security.http.ssl.truststore.path", "/work/" + httpCaPath)
.put("xpack.security.transport.ssl.enabled", true)
.put("xpack.security.transport.ssl.keystore.path", "/work/" + transportPath)
.put("xpack.security.transport.ssl.truststore.path", "/work/" + transportPath)
.build();
pathHomeParameter = "-Epath.home=" + homeDir;

this.keyStoreWrapper = mock(KeyStoreWrapper.class);
when(keyStoreWrapper.isLoaded()).thenReturn(true);
when(keyStoreWrapper.getString("xpack.security.http.ssl.keystore.secure_password"))
.thenReturn(new SecureString("password".toCharArray()));
when(keyStoreWrapper.getString("xpack.security.http.ssl.truststore.secure_password"))
.thenReturn(new SecureString("password".toCharArray()));
when(keyStoreWrapper.getString("xpack.security.transport.ssl.keystore.secure_password"))
.thenReturn(new SecureString("password".toCharArray()));
when(keyStoreWrapper.getString("xpack.security.transport.ssl.truststore.secure_password"))
.thenReturn(new SecureString("password".toCharArray()));

this.client = mock(CommandLineHttpClient.class);
when(client.getDefaultURL()).thenReturn("https://localhost:9200");
Expand Down
Loading

0 comments on commit f67eaf1

Please sign in to comment.