Skip to content

Set elastic password and generate enrollment token #75816

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

Merged
merged 29 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
97f799a
Set elastic password and generate enrollment token
BigPandaToo Jul 28, 2021
45c0080
Set elastic password and generate enrollment token
BigPandaToo Jul 28, 2021
d83f324
Set elastic password and generate enrollment token
BigPandaToo Jul 28, 2021
8e2c773
Remove redefinition
BigPandaToo Jul 29, 2021
3b0ca34
Merge branch 'master' into Pwd_initNode
elasticmachine Jul 30, 2021
6a4821f
Merge branch 'master' into Pwd_initNode
elasticmachine Aug 4, 2021
8d0b281
- Moving code under the tool
BigPandaToo Aug 4, 2021
6e1c717
Addressing PR feedback
BigPandaToo Aug 4, 2021
8ad067f
Addressing PR feedback
BigPandaToo Aug 4, 2021
1d40d0c
Addressing PR feedback
BigPandaToo Aug 4, 2021
b5f464e
Addressing PR feedback
BigPandaToo Aug 4, 2021
791c195
Merge branch 'master' into Pwd_initNode
elasticmachine Aug 4, 2021
e1c996e
- Create EnrollmentToken class
BigPandaToo Aug 9, 2021
7a6b58b
Merge branch 'master' into Pwd_initNode
elasticmachine Aug 9, 2021
eb0eae8
Addressing more PR feedback
BigPandaToo Aug 9, 2021
4c4c69e
Addressing more PR feedback
BigPandaToo Aug 10, 2021
5f9b007
Addressing more PR feedback
BigPandaToo Aug 11, 2021
cebdbc8
Fixing conflict
BigPandaToo Aug 11, 2021
1a169c1
Fixing conflict
BigPandaToo Aug 11, 2021
945bddb
Merge branch 'master' into Pwd_initNode
BigPandaToo Aug 11, 2021
faae76a
Fixing conflict
BigPandaToo Aug 11, 2021
c8c0214
Simplifying tests/ Fixing tests
BigPandaToo Aug 11, 2021
23d1b38
Addressing PR feedback
BigPandaToo Aug 11, 2021
d56fac3
Merge branch 'master' into Pwd_initNode
elasticmachine Aug 11, 2021
14395a8
Moving checkForClusterHealth back to `BaseRunAsSuperuserCommand`;
BigPandaToo Aug 12, 2021
19e228f
Fixing tests
BigPandaToo Aug 12, 2021
d28e251
Some changes to `checkClusterHealthWithRetriesWaitingForCluster':
BigPandaToo Aug 13, 2021
d67d318
Merge branch 'master' into Pwd_initNode
elasticmachine Aug 13, 2021
eb7e14b
Addressing more PR feedback
BigPandaToo Aug 13, 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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import java.util.List;
import java.util.function.Function;

import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL;

public class ResetElasticPasswordTool extends BaseRunAsSuperuserCommand {

private final Function<Environment, CommandLineHttpClient> clientFunction;
Expand Down Expand Up @@ -92,7 +94,7 @@ protected void executeCommand(Terminal terminal, OptionSet options, Environment
username,
password,
() -> requestBodySupplier(elasticPassword),
this::responseBuilder
CommandLineHttpClient::responseBuilder
);
final int responseStatus = httpResponse.getHttpStatus();
if (httpResponse.getHttpStatus() != HttpURLConnection.HTTP_OK) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ void checkClusterHealth(Terminal terminal) throws Exception {
terminal.errorPrintln("Failed to determine the health of the cluster running at " + url);
terminal.errorPrintln("Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling GET " +
route.toString());
final String cause = getErrorCause(httpResponse);
final String cause = CommandLineHttpClient.getErrorCause(httpResponse);
if (cause != null) {
terminal.errorPrintln("Cause: " + cause);
}
Expand Down Expand Up @@ -477,7 +477,7 @@ private void changeUserPassword(String user, SecureString password, Terminal ter
terminal.errorPrintln("");
terminal.errorPrintln(
"Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling PUT " + route.toString());
String cause = getErrorCause(httpResponse);
String cause = CommandLineHttpClient.getErrorCause(httpResponse);
if (cause != null) {
terminal.errorPrintln("Cause: " + cause);
terminal.errorPrintln("");
Expand Down Expand Up @@ -564,32 +564,6 @@ private URL createURL(URL url, String path, String query) throws MalformedURLExc
}
}

private String getErrorCause(HttpResponse httpResponse) {
final Object error = httpResponse.getResponseBody().get("error");
if (error == null) {
return null;
}
if (error instanceof Map) {
Object reason = ((Map) error).get("reason");
if (reason != null) {
return reason.toString();
}
final Object root = ((Map) error).get("root_cause");
if (root != null && root instanceof Map) {
reason = ((Map) root).get("reason");
if (reason != null) {
return reason.toString();
}
final Object type = ((Map) root).get("type");
if (type != null) {
return (String) type;
}
}
return String.valueOf(((Map) error).get("type"));
}
return error.toString();
}

private byte[] toByteArray(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] internalBuffer = new byte[1024];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.enrollment;

import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.json.JsonXContent;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Objects;

public class EnrollmentToken {
private final String apiKey;
private final String fingerprint;
private final String version;
private final List<String > boundAddress;

public String getApiKey() { return apiKey; }
public String getFingerprint() { return fingerprint; }
public String getVersion() { return version; }
public List<String> getBoundAddress() { return boundAddress; }

/**
* Create an EnrollmentToken
*
* @param apiKey API Key credential in the form apiKeyId:ApiKeySecret to be used for enroll calls
* @param fingerprint hex encoded SHA256 fingerprint of the HTTP CA cert
* @param version node version number
* @param boundAddress IP Addresses and port numbers for the interfaces where the Elasticsearch node is listening on
*/
public EnrollmentToken(String apiKey, String fingerprint, String version, List<String> boundAddress) {
this.apiKey = Objects.requireNonNull(apiKey);
this.fingerprint = Objects.requireNonNull(fingerprint);
this.version = Objects.requireNonNull(version);
this.boundAddress = Objects.requireNonNull(boundAddress);
}

public String getRaw() throws Exception {
final XContentBuilder builder = JsonXContent.contentBuilder();
builder.startObject();
builder.field("ver", version);
builder.startArray("adr");
for (String bound_address : boundAddress) {
builder.value(bound_address);
}
builder.endArray();
builder.field("fgr", fingerprint);
builder.field("key", apiKey);
builder.endObject();
return Strings.toString(builder);
}

public String getEncoded() throws Exception {
final String jsonString = getRaw();
return Base64.getUrlEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,71 +36,51 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class CreateEnrollmentToken {
public class EnrollmentTokenGenerator {
protected static final String ENROLL_API_KEY_EXPIRATION = "30m";

private static final Logger logger = LogManager.getLogger(CreateEnrollmentToken.class);
private static final Logger logger = LogManager.getLogger(EnrollmentTokenGenerator.class);
private final Environment environment;
private final SSLService sslService;
private final CommandLineHttpClient client;
private final URL defaultUrl;

public CreateEnrollmentToken(Environment environment) throws MalformedURLException {
public EnrollmentTokenGenerator(Environment environment) throws MalformedURLException {
this(environment, new CommandLineHttpClient(environment));
}

// protected for testing
protected CreateEnrollmentToken(Environment environment, CommandLineHttpClient client) throws MalformedURLException {
protected EnrollmentTokenGenerator(Environment environment, CommandLineHttpClient client) throws MalformedURLException {
this.environment = environment;
this.sslService = new SSLService(environment);
this.client = client;
this.defaultUrl = new URL(client.getDefaultURL());
}

public String createNodeEnrollmentToken(String user, SecureString password) throws Exception {
public EnrollmentToken createNodeEnrollmentToken(String user, SecureString password) throws Exception {
return this.create(user, password, NodeEnrollmentAction.NAME);
}

public String createKibanaEnrollmentToken(String user, SecureString password) throws Exception {
public EnrollmentToken createKibanaEnrollmentToken(String user, SecureString password) throws Exception {
return this.create(user, password, KibanaEnrollmentAction.NAME);
}

protected String create(String user, SecureString password, String action) throws Exception {
protected EnrollmentToken create(String user, SecureString password, String action) throws Exception {
if (XPackSettings.ENROLLMENT_ENABLED.get(environment.settings()) != true) {
throw new IllegalStateException("[xpack.security.enrollment.enabled] must be set to `true` to create an enrollment token");
}
final String fingerprint = getCaFingerprint();
final String apiKey = getApiKeyCredentials(user, password, action);
final Tuple<List<String>, String> httpInfo = getNodeInfo(user, password);

try {
final XContentBuilder builder = JsonXContent.contentBuilder();
builder.startObject();
builder.field("ver", httpInfo.v2());
builder.startArray("adr");
for (String bound_address : httpInfo.v1()) {
builder.value(bound_address);
}
builder.endArray();
builder.field("fgr", fingerprint);
builder.field("key", apiKey);
builder.endObject();
final String jsonString = Strings.toString(builder);
return Base64.getUrlEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
logger.error(("Error generating enrollment token"), e);
throw new IllegalStateException("Error generating enrollment token: " + e.getMessage());
}
return new EnrollmentToken(apiKey, fingerprint, httpInfo.v2(), httpInfo.v1());
}

private HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* 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.enrollment.tool;

import joptsimple.OptionSet;
import joptsimple.OptionSpec;

import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.KeyStoreAwareCommand;
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.settings.Settings;
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.user.ElasticUser;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import org.elasticsearch.xpack.security.enrollment.EnrollmentToken;
import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator;
import org.elasticsearch.xpack.security.tool.CommandLineHttpClient;
import org.elasticsearch.xpack.security.tool.HttpResponse;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.SecureRandom;
import java.util.function.Function;

import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL;

public class BootstrapPasswordAndEnrollmentTokenForInitialNode extends KeyStoreAwareCommand {
private final CheckedFunction<Environment, EnrollmentTokenGenerator, Exception> createEnrollmentTokenFunction;
private final Function<Environment, CommandLineHttpClient> clientFunction;
private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction;
private final OptionSpec<Void> includeNodeEnrollmentToken;
private final SecureRandom secureRandom = new SecureRandom();

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

BootstrapPasswordAndEnrollmentTokenForInitialNode(Function<Environment, CommandLineHttpClient> clientFunction,
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction,
CheckedFunction<Environment, EnrollmentTokenGenerator, Exception>
createEnrollmentTokenFunction){
super("Set elastic password and generate enrollment token for initial node");
this.clientFunction = clientFunction;
this.keyStoreFunction = keyStoreFunction;
this.createEnrollmentTokenFunction = createEnrollmentTokenFunction;
includeNodeEnrollmentToken = parser.accepts("include-node-enrollment-token", "determine that we have to generate " +
"a node enrollment token");
}

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

@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
final SecureString keystorePassword;
try {
keystorePassword = new SecureString(terminal.readSecret(""));
} catch (Exception e) {
throw new UserException(ExitCodes.USAGE, null);
}

final Environment secureEnvironment = readSecureSettings(env, keystorePassword);
final CommandLineHttpClient client = clientFunction.apply(secureEnvironment);
final EnrollmentTokenGenerator enrollmentTokenGenerator = createEnrollmentTokenFunction.apply(secureEnvironment);
final SecureString bootstrapPassword = ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.get(secureEnvironment.settings());
try {
String output;
client.checkClusterHealthWithRetriesWaitingForCluster(ElasticUser.NAME, bootstrapPassword, 15);
final EnrollmentToken kibanaToken = enrollmentTokenGenerator.createKibanaEnrollmentToken(ElasticUser.NAME, bootstrapPassword);
output = "Kibana enrollment token: " + kibanaToken.getEncoded() + System.lineSeparator();
output += "CA fingerprint: " + kibanaToken.getFingerprint() + System.lineSeparator();
if (options.has(includeNodeEnrollmentToken)) {
final EnrollmentToken nodeToken = enrollmentTokenGenerator.createNodeEnrollmentToken(ElasticUser.NAME, bootstrapPassword);
output += "Node enrollment token: " + nodeToken.getEncoded() + System.lineSeparator();
}
if (ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.exists(secureEnvironment.settings()) == false) {
output += "elastic user password: " + setElasticUserPassword(client, bootstrapPassword);
}
terminal.println(output);
} catch (Exception e) {
throw new UserException(ExitCodes.UNAVAILABLE, null);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we should have one big try-catch block with all the endpoint calling methods grouped inside. I think it is easier to follow this way.

Lets avoid swallowing exceptions. I know we're not going to show these to users in the first phase but it bothers me every time I see it, and it is distracting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on having one block. But what do you mean by swallowing exceptions? We don't swallow them, just don't have custom messages

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by swallowing exceptions? We don't swallow them, just don't have custom messages

        } catch (Exception e) {
            throw new UserException(ExitCodes.UNAVAILABLE, null);
        }

->

       } catch (Exception e) {
           throw new UserException(ExitCodes.UNAVAILABLE, "", e);
       }

}

protected SecureString setElasticUserPassword(CommandLineHttpClient client, SecureString bootstrapPassword) throws Exception {
final URL passwordSetUrl = setElasticUserPasswordUrl(client);
final HttpResponse response;
SecureString password = new SecureString(generatePassword(20));
try {
response = client.execute("POST", passwordSetUrl, ElasticUser.NAME, bootstrapPassword,
() -> {
XContentBuilder xContentBuilder = JsonXContent.contentBuilder();
xContentBuilder.startObject().field("password", password.toString()).endObject();
return Strings.toString(xContentBuilder);
}, CommandLineHttpClient::responseBuilder);
if (response.getHttpStatus() != HttpURLConnection.HTTP_OK) {
throw new UserException(ExitCodes.UNAVAILABLE, null);
}
} catch (IOException e) {
throw new UserException(ExitCodes.IO_ERROR, null);
}
return password;
}

Environment readSecureSettings(Environment env, SecureString keystorePassword) throws Exception {
final KeyStoreWrapper keyStoreWrapper = keyStoreFunction.apply(env);
keyStoreWrapper.decrypt(keystorePassword.getChars());
Settings.Builder settingsBuilder = Settings.builder();
settingsBuilder.put(env.settings(), true);
if (settingsBuilder.getSecureSettings() == null) {
settingsBuilder.setSecureSettings(keyStoreWrapper);
}
final Settings settings = settingsBuilder.build();
return new Environment(settings, env.configFile());
}

public static URL checkClusterHealthUrl(CommandLineHttpClient client) throws MalformedURLException, URISyntaxException {
return createURL(new URL(client.getDefaultURL()), "_cluster/health", "?pretty");
}

public static URL setElasticUserPasswordUrl(CommandLineHttpClient client) throws MalformedURLException, URISyntaxException {
return createURL(new URL(client.getDefaultURL()), "/_security/user/" + ElasticUser.NAME + "/_password",
"?pretty");
}

protected char[] generatePassword(int passwordLength) {
final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
char[] characters = new char[passwordLength];
for (int i = 0; i < passwordLength; ++i) {
characters[i] = passwordChars[secureRandom.nextInt(passwordChars.length)];
}
return characters;
}
}
Loading