Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reset elastic password cli tool #74892

Merged
merged 22 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
104617d
Add a tool for creating enrollment tokens
jkakavas Jul 1, 2021
f67eaf1
elastic user password reset CLI tool
jkakavas Jul 3, 2021
173ce85
Merge remote-tracking branch 'origin/master' into reset-elastic-passw…
jkakavas Jul 4, 2021
3776bb9
qa test
jkakavas Jul 4, 2021
d6d9385
add docs
jkakavas Jul 5, 2021
7497bc9
support batch silent mode
jkakavas Jul 5, 2021
ad30c3b
remove leftover files
jkakavas Jul 5, 2021
c5c1759
Bring along updates from #74890
jkakavas Jul 8, 2021
7efae2c
Merge remote-tracking branch 'origin/master' into reset-elastic-passw…
jkakavas Jul 8, 2021
a40f4cc
Merge feedback from 74890
jkakavas Jul 12, 2021
f75e2db
Merge remote-tracking branch 'origin/master' into reset-elastic-passw…
jkakavas Jul 12, 2021
4d8cfa4
Include error code in exception message
jkakavas Jul 12, 2021
1f14b39
don't explicitly check for multiple file realms, node does that already
jkakavas Jul 12, 2021
ed292c4
Add link in top-level page
Jul 13, 2021
704b4d8
Apply doc suggestions from code review
jkakavas Jul 13, 2021
cc2404c
More suggestions from doc review
jkakavas Jul 13, 2021
cb87765
Merge remote-tracking branch 'origin/master' into reset-elastic-passw…
jkakavas Jul 14, 2021
aa73e85
Add note that you can't use the CLI if the file realm is disabled
Jul 14, 2021
0d7c7aa
Merge remote-tracking branch 'origin/master' into reset-elastic-passw…
jkakavas Jul 15, 2021
c2c64cf
Merge remote-tracking branch 'origin/master' into reset-elastic-passw…
jkakavas Jul 20, 2021
4a246cd
message
jkakavas Jul 20, 2021
d8eaca9
Fix package in CLI tool
jkakavas Jul 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/reference/commands/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ tasks from the command line:
* <<elasticsearch-croneval>>
* <<elasticsearch-keystore>>
* <<node-tool>>
* <<reset-elastic-password>>
* <<saml-metadata>>
* <<setup-passwords>>
* <<shard-tool>>
Expand All @@ -27,6 +28,7 @@ include::create-enrollment-token.asciidoc[]
include::croneval.asciidoc[]
include::keystore.asciidoc[]
include::node-tool.asciidoc[]
include::reset-elastic-password.asciidoc[]
include::saml-metadata.asciidoc[]
include::service-tokens-command.asciidoc[]
include::setup-passwords.asciidoc[]
Expand Down
63 changes: 63 additions & 0 deletions docs/reference/commands/reset-elastic-password.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[roles="xpack"]
[[reset-elastic-password]]
== elasticsearch-reset-elastic-password

The `elasticsearch-reset-elastic-password` command resets the password for the
`elastic` <<built-in-users,built-in superuser>>.

[discrete]
=== Synopsis

[source,shell]
----
bin/elasticsearch-reset-elastic-password
[-a, --auto] [-b, --batch] [-E <KeyValuePair]
[-f, --force] [-h, --help] [-i, --interactive]
----

[discrete]
=== Description

Use this command to reset the password of the `elastic` superuser. By default, a
strong password is generated for you. To explicitly set a password, run the
tool in interactive mode with `-i`. The command generates (and subsequently
removes) a temporary user in the <<file-realm,file realm>> to run the request
that changes the `elastic` user password.
IMPORTANT: You cannot use this tool if the file realm is disabled in your `elasticsearch.yml` file.

This command uses an HTTP connection to connect to the cluster and run the user
management requests. The command automatically attempts to establish the connection
over HTTPS by using the `xpack.security.http.ssl` settings in
the `elasticsearch.yml` file. If you do not use the default config directory
location, ensure that the `ES_PATH_CONF` environment variable returns the
correct path before you run the `elasticsearch-reset-elastic-password` command. You can
override settings in your `elasticsearch.yml` file by using the `-E` command
option. For more information about debugging connection failures, see
<<trb-security-setup>>.

[discrete]
[[reset-elastic-password-parameters]]
=== Parameters

`-a, --auto`:: Resets the password of the `elastic` user to an auto-generated strong password. (Default)

`-b, --batch`:: Runs the reset password process without prompting the user for verification.

jkakavas marked this conversation as resolved.
Show resolved Hide resolved
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
`-E <KeyValuePair>`:: Configures a standard {es} or {xpack} setting.

`-f, --force`:: Forces the command to run against an unhealthy cluster.

`-h, --help`:: Returns all of the command parameters.

`-i, --interactive`:: Prompts the user for the password of the `elastic` user. Use this option to explicitly set a password.

[discrete]
=== Examples

The following example resets the password of the `elastic` user to an auto-generated value and
prints the new password in the console.

[source,shell]
----
bin/elasticsearch-reset-elastic-password
----
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.authc.esnative.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
@@ -0,0 +1,21 @@
@echo off

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

setlocal enabledelayedexpansion
setlocal enableextensions

set ES_MAIN_CLASS=org.elasticsearch.xpack.security.authc.esnative.tool.ResetElasticPasswordTool
set ES_ADDITIONAL_SOURCES=x-pack-env;x-pack-security-env
set ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli
call "%~dp0elasticsearch-cli.bat" ^
%%* ^
|| goto exit

endlocal
endlocal
:exit
exit /b %ERRORLEVEL%
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* 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.authc.esnative.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 org.elasticsearch.xpack.security.tool.BaseRunAsSuperuserCommand;
import org.elasticsearch.xpack.security.tool.CommandLineHttpClient;
import org.elasticsearch.xpack.security.tool.HttpResponse;

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;

public 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, "Resets the password of the elastic built-in user");
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, String username, SecureString password)
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 {
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,
username,
password,
() -> requestBodySupplier(elasticPassword),
this::responseBuilder
);
final int responseStatus = httpResponse.getHttpStatus();
if (httpResponse.getHttpStatus() != HttpURLConnection.HTTP_OK) {
throw new UserException(ExitCodes.TEMP_FAILURE,
"Failed to reset password for the elastic user. Unexpected http status [" + responseStatus + "]");
} 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.print(Terminal.Verbosity.NORMAL,"New value: ");
terminal.println(Terminal.Verbosity.SILENT, elasticPassword.toString());
}
}
} 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("Re-enter 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 @@ -249,14 +249,14 @@ private void checkClusterHealthWithRetries(Environment env, Terminal terminal, S
}
}

HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws IOException {
protected 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;
}

char[] generatePassword(int passwordLength) {
protected char[] generatePassword(int passwordLength) {
final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
char[] characters = new char[passwordLength];
for (int i = 0; i < passwordLength; ++i) {
Expand All @@ -275,7 +275,7 @@ private String generateUsername() {
return "enrollment_autogenerated_" + new String(characters);
}
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved

URL createURL(URL url, String path, String query) throws MalformedURLException, URISyntaxException {
protected 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,85 @@
/*
* 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.authc.esnative.tool;

import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.Response;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.network.InetAddresses;
import org.elasticsearch.common.network.NetworkService;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.test.rest.ESRestTestCase;
import org.junit.Before;

import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public abstract class AbstractPasswordToolTestCase extends ESRestTestCase {

@Override
protected Settings restClientSettings() {
String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray()));
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
}

@Before
@SuppressWarnings("unchecked")
void writeConfigurationToDisk() throws Exception {
final String testConfigDir = System.getProperty("tests.config.dir");
logger.info("--> CONF: {}", testConfigDir);
final Path configPath = PathUtils.get(testConfigDir);
setSystemPropsForTool(configPath);

Response nodesResponse = client().performRequest(new Request("GET", "/_nodes/http"));
Map<String, Object> nodesMap = entityAsMap(nodesResponse);

Map<String, Object> nodes = (Map<String, Object>) nodesMap.get("nodes");
Map<String, Object> firstNode = (Map<String, Object>) nodes.entrySet().iterator().next().getValue();
Map<String, Object> firstNodeHttp = (Map<String, Object>) firstNode.get("http");
String nodePublishAddress = (String) firstNodeHttp.get("publish_address");
final int lastColonIndex = nodePublishAddress.lastIndexOf(':');
InetAddress actualPublishAddress = InetAddresses.forString(nodePublishAddress.substring(0, lastColonIndex));
InetAddress expectedPublishAddress = new NetworkService(Collections.emptyList()).resolvePublishHostAddresses(Strings.EMPTY_ARRAY);
final int port = Integer.valueOf(nodePublishAddress.substring(lastColonIndex + 1));

List<String> lines = Files.readAllLines(configPath.resolve("elasticsearch.yml"));
lines = lines.stream()
.filter(s -> s.startsWith("http.port") == false && s.startsWith("http.publish_port") == false)
.collect(Collectors.toList());
lines.add(randomFrom("http.port", "http.publish_port") + ": " + port);
if (expectedPublishAddress.equals(actualPublishAddress) == false) {
lines.add("http.publish_address: " + InetAddresses.toAddrString(actualPublishAddress));
}
Files.write(configPath.resolve("elasticsearch.yml"), lines, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING);
}

protected void possiblyDecryptKeystore(MockTerminal mockTerminal) {
if (inFipsJvm()) {
// In our FIPS 140-2 tests, we set the keystore password to `keystore-password`
mockTerminal.addSecretInput("keystore-password");
}
}

@SuppressForbidden(reason = "need to set sys props for CLI tool")
void setSystemPropsForTool(Path configPath) {
System.setProperty("es.path.conf", configPath.toString());
System.setProperty("es.path.home", configPath.getParent().toString());
}
}
Loading