-
Notifications
You must be signed in to change notification settings - Fork 24.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Reset elastic password cli tool (#74892)
This change introduces a CLI tool that can be used to create enrollment tokens. It doesn't require credentials, but simply write access to the local filesystem of a node. It uses an auto-generated user in the file-realm with superuser role. For this purpose, this change also introduces a base class for a CLI tool that can be used by any CLI tool needs to perform actions against an ES node as a superuser without requiring credentials from the user. It is worth noting that this doesn't change our existing thread model, because already an actor with write access to the fs of an ES node, can become superuser (again, by adding a superuser to the file realm, albeit manually).
- Loading branch information
Showing
10 changed files
with
631 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
`-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 | ||
---- |
11 changes: 11 additions & 0 deletions
11
x-pack/plugin/security/src/main/bin/elasticsearch-reset-elastic-password
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 \ | ||
"$@" |
21 changes: 21 additions & 0 deletions
21
x-pack/plugin/security/src/main/bin/elasticsearch-reset-elastic-password.bat
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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% |
152 changes: 152 additions & 0 deletions
152
...n/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
...va/org/elasticsearch/xpack/security/authc/esnative/tool/AbstractPasswordToolTestCase.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
Oops, something went wrong.