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

Set elastic password and generate enrollment token #75816

Merged
merged 29 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -564,7 +564,7 @@ private URL createURL(URL url, String path, String query) throws MalformedURLExc
}
}

private String getErrorCause(HttpResponse httpResponse) {
public static String getErrorCause(HttpResponse httpResponse) {
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved
final Object error = httpResponse.getResponseBody().get("error");
if (error == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* 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 joptsimple.OptionParser;
import joptsimple.OptionSet;

import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.settings.SecureSetting;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.user.ElasticUser;
import org.elasticsearch.xpack.security.tool.CommandLineHttpClient;
import org.elasticsearch.xpack.security.tool.HttpResponse;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import static org.elasticsearch.xpack.security.authc.esnative.tool.SetupPasswordTool.getErrorCause;

public class PasswordAndEnrollmentInitialNode extends EnvironmentAwareCommand {
private static final Setting<SecureString> SEED_SETTING = SecureSetting.secureString("keystore.seed", null);
private static final Setting<SecureString> BOOTSTRAP_ELASTIC_PASSWORD = SecureSetting.secureString("bootstrap.password",
null);
private static final Setting<SecureString> CREDENTIALS_PASSWORD = SecureSetting.secureString("bootstrap.password",
SEED_SETTING);
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved
private static final String elasticUser = ElasticUser.NAME;

private SecureString password;
private String token;
private String fingerprint;

// Protected for testing
protected SecureString getPassword() {
return password;
}

protected String getToken() {
return token;
}

protected String getFingerprint() {
return fingerprint;
}

protected CommandLineHttpClient getClient(Environment env) {
return new CommandLineHttpClient(env);
}

protected CreateEnrollmentToken getCreateEnrollmentToken(Environment env) throws Exception{
return new CreateEnrollmentToken(env);
}


PasswordAndEnrollmentInitialNode (){
super("Set elastic password and generate enrollment token for initial node");
parser.allowsUnrecognizedOptions();
}

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

@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
if (options.nonOptionArguments().contains("--explicitly-acknowledge-execution") == false) {
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved
throw new UserException(ExitCodes.NOOP, "This command is not intended for end users");
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved
}
if (env.settings().hasValue(XPackSettings.ENROLLMENT_ENABLED.getKey()) && false ==
XPackSettings.ENROLLMENT_ENABLED.get(env.settings())) {
throw new UserException(ExitCodes.NOOP, "Enrollment is explicitly disabled.");
}
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved
CommandLineHttpClient client = getClient(env);
CreateEnrollmentToken cet = getCreateEnrollmentToken(env);
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved

// Try 5 times and rethrow the last exception from checkClusterHealth
for (int retry = 5; ; retry--) {
try {
checkClusterHealth(env, client);
break;
} catch (Exception e) {
if (retry == 1) {
throw e;
}
Thread.sleep(1000);
}
}
if (Strings.isNullOrEmpty(BOOTSTRAP_ELASTIC_PASSWORD.get(env.settings()).toString())) {
changeElasticUserPassword(env, terminal, client);
} else {
password = BOOTSTRAP_ELASTIC_PASSWORD.get(env.settings());
}
token = cet.createKibanaEnrollmentToken(elasticUser, password);
Map<String, String> infoNode = getDecoded(token);
fingerprint = infoNode.get("fgr");
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved

terminal.println("'elastic' user password: " + password);
terminal.println("enrollment token: " + token);
terminal.println("CA fingerprint: " + fingerprint);
}

protected void checkClusterHealth(Environment env, CommandLineHttpClient client) throws Exception {
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved
final URL clusterHealthUrl = checkClusterHealthUrl(client);
final HttpResponse response;
try {
response = client.execute("GET", clusterHealthUrl, elasticUser, CREDENTIALS_PASSWORD.get(env.settings()),
() -> null, this::responseBuilder);
} catch (Exception e) {
throw new UserException(ExitCodes.UNAVAILABLE, "Failed to determine the health of the cluster. ", e);
}
final int responseStatus = response.getHttpStatus();
if (responseStatus != HttpURLConnection.HTTP_OK) {
throw new UserException(
ExitCodes.DATA_ERROR,
"Failed to determine the health of the cluster. Unexpected http status [" + responseStatus + "]"
);
} else {
final String clusterStatus = Objects.toString(response.getResponseBody().get("status"), "");
if (clusterStatus.isEmpty()) {
throw new UserException(
ExitCodes.DATA_ERROR,
"Failed to determine the health of the cluster. Cluster health API did not return a status value."
);
} else if ("red".equalsIgnoreCase(clusterStatus)) {
throw new UserException(ExitCodes.UNAVAILABLE,
"Failed to determine the health of the cluster. Cluster health is currently RED.");
}
}
}

protected void changeElasticUserPassword(Environment env, Terminal terminal, CommandLineHttpClient client) throws Exception {
final URL passwordChangeUrl = changeElasticUserPasswordUrl(client);
final HttpResponse response;
password = new SecureString(generatePassword(20));
try {
response = client.execute("POST", passwordChangeUrl, elasticUser, CREDENTIALS_PASSWORD.get(env.settings()),
() -> {
XContentBuilder xContentBuilder = JsonXContent.contentBuilder();
xContentBuilder.startObject().field("password", password).endObject();
return Strings.toString(xContentBuilder);
}, this::responseBuilder);
if (response.getHttpStatus() != HttpURLConnection.HTTP_OK) {
terminal.errorPrintln("");
terminal.errorPrintln(
"Unexpected response code [" + response.getHttpStatus() + "] from calling PUT " + passwordChangeUrl.toString());
String cause = getErrorCause(response);
if (cause != null) {
terminal.errorPrintln("Cause: " + cause);
terminal.errorPrintln("");
}
terminal.errorPrintln("");
throw new UserException(ExitCodes.TEMP_FAILURE, "Failed to set password for user [" + elasticUser + "].");
}
} catch (IOException e) {
terminal.errorPrintln("");
terminal.errorPrintln("Connection failure to: " + passwordChangeUrl.toString() + " failed: " + e.getMessage());
terminal.errorPrintln("");
terminal.errorPrintln(ExceptionsHelper.stackTrace(e));
terminal.errorPrintln("");
throw new UserException(ExitCodes.TEMP_FAILURE, "Failed to set password for user [" + elasticUser + "].", e);
}
}

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

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

URL changeElasticUserPasswordUrl(CommandLineHttpClient client) throws MalformedURLException, URISyntaxException {
return createURL(new URL(client.getDefaultURL()), "/_security/user/" + elasticUser + "/_password",
"?pretty");
}

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) {
final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
char[] characters = new char[passwordLength];
for (int i = 0; i < passwordLength; ++i) {
characters[i] = passwordChars[new SecureRandom().nextInt(passwordChars.length)];
}
return characters;
}

private Map<String, String> getDecoded(String token) throws Exception {
final String jsonString = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
try (XContentParser parser = JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY,
LoggingDeprecationHandler.INSTANCE, jsonString)) {
final Map<String, Object> info = parser.map();
if (info == null) {
throw new UserException(ExitCodes.DATA_ERROR,
"Unable to decode enrollment token.");
}
return info.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toString()));
}
}

// For testing
OptionParser getParser() {
return parser;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public void testCreateSuccess() throws Exception {
assertEquals("ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", infoNode.get("fgr"));
assertEquals("DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", infoNode.get("key"));

final String tokenKibana = createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic"));
final String tokenKibana = createEnrollmentToken.createKibanaEnrollmentToken("elastic", new SecureString("elastic"));

Map<String, String> infoKibana = getDecoded(tokenKibana);
assertEquals("8.0.0", infoKibana.get("ver"));
Expand Down
Loading