Add tool to setup passwords for internal users (elastic/x-pack-elasticsearch#1434)

This is related to elastic/x-pack-elasticsearch#1217. This change introduces a tool
bin/x-pack/setup-passwords that will streamline the setting of
internal user passwords. There are two modes of operation. One mode
called auto, automatically generates passwords and prints them to
the console. The second mode called interactive allows the user to 
enter passwords.

All passwords are changed using the elastic superuser. The elastic
password is the first password to be set.

Original commit: elastic/x-pack-elasticsearch@00974234a2
This commit is contained in:
Tim Brooks 2017-06-15 10:48:02 -05:00 committed by GitHub
parent d920cc7348
commit 7c7e47aa0f
5 changed files with 587 additions and 0 deletions

View File

@ -0,0 +1,104 @@
#!/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;
# you may not use this file except in compliance with the Elastic License.
SCRIPT="$0"
# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
while [ -h "$SCRIPT" ] ; do
ls=`ls -ld "$SCRIPT"`
# Drop everything prior to ->
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=`dirname "$SCRIPT"`/"$link"
fi
done
# determine elasticsearch home
ES_HOME=`dirname "$SCRIPT"`/../..
# make ELASTICSEARCH_HOME absolute
ES_HOME=`cd "$ES_HOME"; pwd`
# If an include wasn't specified in the environment, then search for one...
if [ "x$ES_INCLUDE" = "x" ]; then
# Locations (in order) to use when searching for an include file.
for include in /usr/share/elasticsearch/elasticsearch.in.sh \
/usr/local/share/elasticsearch/elasticsearch.in.sh \
/opt/elasticsearch/elasticsearch.in.sh \
~/.elasticsearch.in.sh \
"`dirname "$0"`"/../elasticsearch.in.sh \
"$ES_HOME/bin/elasticsearch.in.sh"; do
if [ -r "$include" ]; then
. "$include"
break
fi
done
# ...otherwise, source the specified include.
elif [ -r "$ES_INCLUDE" ]; then
. "$ES_INCLUDE"
fi
if [ -x "$JAVA_HOME/bin/java" ]; then
JAVA="$JAVA_HOME/bin/java"
else
JAVA=`which java`
fi
if [ ! -x "$JAVA" ]; then
echo "Could not find any executable java binary. Please install java in your PATH or set JAVA_HOME"
exit 1
fi
if [ -z "$ES_CLASSPATH" ]; then
echo "You must set the ES_CLASSPATH var" >&2
exit 1
fi
if [ -z "$CONF_DIR" ]; then
# Try to read package config files
if [ -f "/etc/sysconfig/elasticsearch" ]; then
CONF_DIR=/etc/elasticsearch
. "/etc/sysconfig/elasticsearch"
elif [ -f "/etc/default/elasticsearch" ]; then
CONF_DIR=/etc/elasticsearch
. "/etc/default/elasticsearch"
fi
fi
export HOSTNAME=`hostname -s`
# include x-pack jars in classpath
ES_CLASSPATH="$ES_CLASSPATH:$ES_HOME/plugins/x-pack/*"
# don't let JAVA_TOOL_OPTIONS slip in (e.g. crazy agents in ubuntu)
# works around https://bugs.launchpad.net/ubuntu/+source/jayatana/+bug/1441487
if [ "x$JAVA_TOOL_OPTIONS" != "x" ]; then
echo "Warning: Ignoring JAVA_TOOL_OPTIONS=$JAVA_TOOL_OPTIONS"
echo "Please pass JVM parameters via ES_JAVA_OPTS instead"
unset JAVA_TOOL_OPTIONS
fi
# CONF_FILE setting was removed
if [ ! -z "$CONF_FILE" ]; then
echo "CONF_FILE setting is no longer supported. elasticsearch.yml must be placed in the config directory and cannot be renamed."
exit 1
fi
declare -a args=("$@")
if [ -e "$CONF_DIR" ]; then
args=("${args[@]}" -Edefault.path.conf="$CONF_DIR")
fi
cd "$ES_HOME" > /dev/null
"$JAVA" $ES_JAVA_OPTS -cp "$ES_CLASSPATH" -Des.path.home="$ES_HOME" org.elasticsearch.xpack.security.authc.esnative.tool.SetupPasswordTool "${args[@]}"
status=$?
cd - > /dev/null
exit $status

View File

@ -0,0 +1,9 @@
@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 you may not use this file except in compliance with the Elastic License.
PUSHD "%~dp0"
CALL "%~dp0.in.bat" org.elasticsearch.xpack.security.authc.esnative.tool.SetupPasswordTool %*
POPD

View File

@ -0,0 +1,97 @@
/*
* 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.util.io.Streams;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.lease.Releasables;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.common.socket.SocketAccess;
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.ssl.SSLService;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import static org.elasticsearch.xpack.security.Security.setting;
/**
* A simple http client for usage in command line tools. This client only uses internal jdk classes and does
* not rely on an external http libraries.
*/
public class CommandLineHttpClient {
private final Settings settings;
private final Environment env;
public CommandLineHttpClient(Settings settings, Environment env) {
this.settings = settings;
this.env = env;
}
// We do not install the security manager when calling from the commandline.
// However, doPrivileged blocks will be necessary for any test code that calls this.
@SuppressForbidden(reason = "We call connect in doPrivileged and provide SocketPermission")
public String postURL(String method, String urlString, String user, SecureString password, @Nullable String bodyString)
throws Exception {
URI uri = new URI(urlString);
URL url = uri.toURL();
HttpURLConnection conn;
// If using SSL, need a custom service because it's likely a self-signed certificate
if ("https".equalsIgnoreCase(uri.getScheme())) {
Settings sslSettings = settings.getByPrefix(setting("http.ssl."));
final SSLService sslService = new SSLService(settings, env);
final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection();
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
// Requires permission java.lang.RuntimePermission "setFactory";
httpsConn.setSSLSocketFactory(sslService.sslSocketFactory(sslSettings));
return null;
});
conn = httpsConn;
} else {
conn = (HttpURLConnection) url.openConnection();
}
conn.setRequestMethod(method);
conn.setReadTimeout(30 * 1000); // 30 second timeout
// Add basic-auth header
String token = UsernamePasswordToken.basicAuthHeaderValue(user, password);
conn.setRequestProperty("Authorization", token);
conn.setRequestProperty("Content-Type", XContentType.JSON.mediaType());
conn.setDoOutput(bodyString != null); // set true if we are sending a body
SocketAccess.doPrivileged(conn::connect);
if (bodyString != null) {
try (OutputStream out = conn.getOutputStream()) {
out.write(bodyString.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
Releasables.closeWhileHandlingException(conn::disconnect);
throw e;
}
}
try (InputStream inputStream = conn.getInputStream()) {
byte[] bytes = Streams.readAll(inputStream);
return new String(bytes, StandardCharsets.UTF_8);
} catch (IOException e) {
try (InputStream errorStream = conn.getErrorStream()) {
byte[] bytes = Streams.readAll(errorStream);
throw new IOException(new String(bytes, StandardCharsets.UTF_8), e);
}
} finally {
conn.disconnect();
}
}
}

View File

@ -0,0 +1,227 @@
/*
* 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 joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.MultiCommand;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.Booleans;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import org.elasticsearch.xpack.security.user.BeatsSystemUser;
import org.elasticsearch.xpack.security.user.ElasticUser;
import org.elasticsearch.xpack.security.user.KibanaUser;
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* A tool to set passwords of internal users. It first sets the elastic user password. After the elastic user
* password is set, it will set the remaining user passwords. This tool will only work if the passwords have
* not already been set by something else.
*/
public class SetupPasswordTool extends MultiCommand {
private static final char[] CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" +
"~!@#$%^&*-_=+?").toCharArray();
private static final String[] USERS = new String[]{ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME};
private final Function<Environment, CommandLineHttpClient> clientFunction;
private CommandLineHttpClient client;
SetupPasswordTool() {
this((environment) -> new CommandLineHttpClient(environment.settings(), environment));
}
SetupPasswordTool(Function<Environment, CommandLineHttpClient> clientFunction) {
super("Sets the passwords for reserved users");
subcommands.put("auto", new AutoSetup());
subcommands.put("interactive", new InteractiveSetup());
this.clientFunction = clientFunction;
}
public static void main(String[] args) throws Exception {
exit(new SetupPasswordTool().main(args, Terminal.DEFAULT));
}
/**
* This class sets the passwords using automatically generated random passwords. The passwords will be
* printed to the console.
*/
private class AutoSetup extends SetupCommand {
AutoSetup() {
super("Uses randomly generated passwords");
}
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
setupOptions(options, env);
if (shouldPrompt) {
terminal.println("Initiating the setup of reserved user " + Arrays.toString(USERS) + " passwords.");
terminal.println("The passwords will be randomly generated and printed to 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");
}
}
SecureRandom secureRandom = new SecureRandom();
changePasswords(terminal, (user) -> generatePassword(secureRandom, user),
(user, password) -> changedPasswordCallback(terminal, user, password));
}
private SecureString generatePassword(SecureRandom secureRandom, String user) {
int passwordLength = 20; // Generate 20 character passwords
char[] characters = new char[passwordLength];
for (int i = 0; i < passwordLength; ++i) {
characters[i] = CHARS[secureRandom.nextInt(CHARS.length)];
}
return new SecureString(characters);
}
private void changedPasswordCallback(Terminal terminal, String user, SecureString password) {
terminal.println("Changed password for user " + user + "\n" + "PASSWORD " + user + " = " + password + "\n");
}
}
/**
* This class sets the passwords using password entered manually by the user from the console.
*/
private class InteractiveSetup extends SetupCommand {
InteractiveSetup() {
super("Uses passwords entered by a user");
}
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
setupOptions(options, env);
if (shouldPrompt) {
terminal.println("Initiating the setup of reserved user " + Arrays.toString(USERS) + " passwords.");
terminal.println("You will be prompted to enter passwords as the process progresses.");
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");
}
}
changePasswords(terminal, user -> promptForPassword(terminal, user),
(user, password) -> changedPasswordCallback(terminal, user, password));
}
private SecureString promptForPassword(Terminal terminal, String user) throws UserException {
SecureString password1 = new SecureString(terminal.readSecret("Enter password for [" + user + "]: "));
try (SecureString password2 = new SecureString(terminal.readSecret("Reenter password for [" + user + "]: "))) {
if (password1.equals(password2) == false) {
password1.close();
throw new UserException(ExitCodes.USAGE, "Passwords for user [" + user+ "] do not match");
}
}
return password1;
}
private void changedPasswordCallback(Terminal terminal, String user, SecureString password) {
terminal.println("Changed password for user " + user + "\n");
}
}
/**
* An abstract class that provides functionality common to both the auto and interactive setup modes.
*/
private abstract class SetupCommand extends EnvironmentAwareCommand {
boolean shouldPrompt;
private OptionSpec<String> urlOption;
private OptionSpec<String> noPromptOption;
private String elasticUser = ElasticUser.NAME;
private SecureString elasticUserPassword = ReservedRealm.DEFAULT_PASSWORD_TEXT;
private String url;
SetupCommand(String description) {
super(description);
setParser();
}
void setupOptions(OptionSet options, Environment env) {
client = clientFunction.apply(env);
String providedUrl = urlOption.value(options);
url = providedUrl == null ? "http://localhost:9200" : providedUrl;
setShouldPrompt(options);
}
private void setParser() {
urlOption = parser.acceptsAll(Arrays.asList("u", "url"), "The url for the change password request").withOptionalArg();
noPromptOption = parser.acceptsAll(Arrays.asList("b", "batch"), "Whether the user should be prompted to initiate the "
+ "change password process").withOptionalArg();
}
private void setShouldPrompt(OptionSet options) {
String optionalNoPrompt = noPromptOption.value(options);
if (options.has(noPromptOption)) {
shouldPrompt = optionalNoPrompt != null && Booleans.parseBoolean(optionalNoPrompt) == false;
} else {
shouldPrompt = true;
}
}
void changePasswords(Terminal terminal, CheckedFunction<String, SecureString, UserException> passwordFn,
BiConsumer<String, SecureString> callback) throws Exception {
for (String user : USERS) {
changePassword(terminal, url, user, passwordFn, callback);
}
}
private void changePassword(Terminal terminal, String url, String user,
CheckedFunction<String, SecureString, UserException> passwordFn,
BiConsumer<String, SecureString> callback) throws Exception {
boolean isSuperUser = user.equals(elasticUser);
SecureString password = passwordFn.apply(user);
try {
String route = url + "/_xpack/security/user/" + user + "/_password";
String response = client.postURL("PUT", route, elasticUser, elasticUserPassword, buildPayload(password));
callback.accept(user, password);
if (isSuperUser) {
elasticUserPassword = password;
}
} catch (Exception e) {
terminal.println("Exception making http rest request for user [" + user + "]");
throw e;
} finally {
// We do not close the password if it is the super user as we are going to use the super user
// password in the followup requests to change other user passwords
if (isSuperUser == false) {
password.close();
}
}
}
private String buildPayload(SecureString password) throws IOException {
XContentBuilder xContentBuilder = JsonXContent.contentBuilder();
xContentBuilder.startObject().field("password", password.toString()).endObject();
return xContentBuilder.string();
}
}
}

View File

@ -0,0 +1,150 @@
/*
* 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.elasticsearch.cli.Command;
import org.elasticsearch.cli.CommandTestCase;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.xpack.security.user.BeatsSystemUser;
import org.elasticsearch.xpack.security.user.ElasticUser;
import org.elasticsearch.xpack.security.user.KibanaUser;
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
import org.junit.Before;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
import java.io.IOException;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.contains;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
public class SetupPasswordToolTests extends CommandTestCase {
private final String pathHomeParameter = "-Epath.home=" + createTempDir();
private final String ep = "elastic-password";
private final String kp = "kibana-password";
private final String lp = "logstash-password";
private final String bp = "beats-password";
private CommandLineHttpClient httpClient;
@Before
public void setSecrets() {
terminal.addSecretInput(ep);
terminal.addSecretInput(ep);
terminal.addSecretInput(kp);
terminal.addSecretInput(kp);
terminal.addSecretInput(lp);
terminal.addSecretInput(lp);
terminal.addSecretInput(bp);
terminal.addSecretInput(bp);
}
@Override
protected Command newCommand() {
this.httpClient = mock(CommandLineHttpClient.class);
return new SetupPasswordTool((e) -> httpClient);
}
public void testAutoSetup() throws Exception {
execute("auto", pathHomeParameter, "-b", "true");
ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class);
SecureString defaultPassword = new SecureString("changeme".toCharArray());
InOrder inOrder = Mockito.inOrder(httpClient);
String elasticUrl = "http://localhost:9200/_xpack/security/user/elastic/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(elasticUrl), eq(ElasticUser.NAME), eq(defaultPassword), passwordCaptor.capture());
String[] users = {KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME};
SecureString newPassword = new SecureString(parsePassword(passwordCaptor.getValue()).toCharArray());
for (String user : users) {
String urlWithRoute = "http://localhost:9200/_xpack/security/user/" + user + "/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(newPassword), anyString());
}
}
public void testUrlOption() throws Exception {
String url = "http://localhost:9202";
execute("auto", pathHomeParameter, "-u", url, "-b");
ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class);
SecureString defaultPassword = new SecureString("changeme".toCharArray());
InOrder inOrder = Mockito.inOrder(httpClient);
String elasticUrl = url + "/_xpack/security/user/elastic/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(elasticUrl), eq(ElasticUser.NAME), eq(defaultPassword), passwordCaptor.capture());
String[] users = {KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME};
SecureString newPassword = new SecureString(parsePassword(passwordCaptor.getValue()).toCharArray());
for (String user : users) {
String urlWithRoute = url + "/_xpack/security/user/" + user + "/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(newPassword), anyString());
}
}
public void testInteractiveSetup() throws Exception {
terminal.addTextInput("Y");
execute("interactive", pathHomeParameter);
SecureString defaultPassword = new SecureString("changeme".toCharArray());
InOrder inOrder = Mockito.inOrder(httpClient);
String elasticUrl = "http://localhost:9200/_xpack/security/user/elastic/_password";
SecureString newPassword = new SecureString(ep.toCharArray());
inOrder.verify(httpClient).postURL(eq("PUT"), eq(elasticUrl), eq(ElasticUser.NAME), eq(defaultPassword), contains(ep));
String kibanaUrl = "http://localhost:9200/_xpack/security/user/" + KibanaUser.NAME + "/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(kibanaUrl), eq(ElasticUser.NAME), eq(newPassword), contains(kp));
String logstashUrl = "http://localhost:9200/_xpack/security/user/" + LogstashSystemUser.NAME + "/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(logstashUrl), eq(ElasticUser.NAME), eq(newPassword), contains(lp));
String beatsUrl = "http://localhost:9200/_xpack/security/user/" + BeatsSystemUser.NAME + "/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(beatsUrl), eq(ElasticUser.NAME), eq(newPassword), contains(bp));
}
public void testInteractivePasswordsNotMatching() throws Exception {
String ep = "elastic-password";
terminal.reset();
terminal.addTextInput("Y");
terminal.addSecretInput(ep);
terminal.addSecretInput(ep + "typo");
String url = "http://localhost:9200";
try {
execute("interactive", pathHomeParameter, "-u", url);
fail("Should have thrown exception");
} catch (UserException e) {
assertEquals(ExitCodes.USAGE, e.exitCode);
assertEquals("Passwords for user [elastic] do not match", e.getMessage());
}
verifyZeroInteractions(httpClient);
}
private String parsePassword(String value) throws IOException {
try (XContentParser parser = JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, value)) {
XContentParser.Token token = parser.nextToken();
if (token == XContentParser.Token.START_OBJECT) {
if (parser.nextToken() == XContentParser.Token.FIELD_NAME) {
if (parser.nextToken() == XContentParser.Token.VALUE_STRING) {
return parser.text();
}
}
}
}
throw new RuntimeException("Did not properly parse password.");
}
}