Skip to content

Commit c5b73fc

Browse files
Set elastic password and generate enrollment token (#75816)
Set elastic password and generate enrollment token for initial node Co-authored-by: Elastic Machine <[email protected]>
1 parent f56d7fc commit c5b73fc

File tree

12 files changed

+640
-131
lines changed

12 files changed

+640
-131
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import java.util.List;
3232
import java.util.function.Function;
3333

34+
import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL;
35+
3436
public class ResetElasticPasswordTool extends BaseRunAsSuperuserCommand {
3537

3638
private final Function<Environment, CommandLineHttpClient> clientFunction;
@@ -92,7 +94,7 @@ protected void executeCommand(Terminal terminal, OptionSet options, Environment
9294
username,
9395
password,
9496
() -> requestBodySupplier(elasticPassword),
95-
this::responseBuilder
97+
CommandLineHttpClient::responseBuilder
9698
);
9799
final int responseStatus = httpResponse.getHttpStatus();
98100
if (httpResponse.getHttpStatus() != HttpURLConnection.HTTP_OK) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ void checkClusterHealth(Terminal terminal) throws Exception {
418418
terminal.errorPrintln("Failed to determine the health of the cluster running at " + url);
419419
terminal.errorPrintln("Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling GET " +
420420
route.toString());
421-
final String cause = getErrorCause(httpResponse);
421+
final String cause = CommandLineHttpClient.getErrorCause(httpResponse);
422422
if (cause != null) {
423423
terminal.errorPrintln("Cause: " + cause);
424424
}
@@ -477,7 +477,7 @@ private void changeUserPassword(String user, SecureString password, Terminal ter
477477
terminal.errorPrintln("");
478478
terminal.errorPrintln(
479479
"Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling PUT " + route.toString());
480-
String cause = getErrorCause(httpResponse);
480+
String cause = CommandLineHttpClient.getErrorCause(httpResponse);
481481
if (cause != null) {
482482
terminal.errorPrintln("Cause: " + cause);
483483
terminal.errorPrintln("");
@@ -564,32 +564,6 @@ private URL createURL(URL url, String path, String query) throws MalformedURLExc
564564
}
565565
}
566566

567-
private String getErrorCause(HttpResponse httpResponse) {
568-
final Object error = httpResponse.getResponseBody().get("error");
569-
if (error == null) {
570-
return null;
571-
}
572-
if (error instanceof Map) {
573-
Object reason = ((Map) error).get("reason");
574-
if (reason != null) {
575-
return reason.toString();
576-
}
577-
final Object root = ((Map) error).get("root_cause");
578-
if (root != null && root instanceof Map) {
579-
reason = ((Map) root).get("reason");
580-
if (reason != null) {
581-
return reason.toString();
582-
}
583-
final Object type = ((Map) root).get("type");
584-
if (type != null) {
585-
return (String) type;
586-
}
587-
}
588-
return String.valueOf(((Map) error).get("type"));
589-
}
590-
return error.toString();
591-
}
592-
593567
private byte[] toByteArray(InputStream is) throws IOException {
594568
ByteArrayOutputStream baos = new ByteArrayOutputStream();
595569
byte[] internalBuffer = new byte[1024];
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.security.enrollment;
9+
10+
import org.elasticsearch.common.Strings;
11+
import org.elasticsearch.common.xcontent.XContentBuilder;
12+
import org.elasticsearch.common.xcontent.json.JsonXContent;
13+
14+
import java.nio.charset.StandardCharsets;
15+
import java.util.Base64;
16+
import java.util.List;
17+
import java.util.Objects;
18+
19+
public class EnrollmentToken {
20+
private final String apiKey;
21+
private final String fingerprint;
22+
private final String version;
23+
private final List<String > boundAddress;
24+
25+
public String getApiKey() { return apiKey; }
26+
public String getFingerprint() { return fingerprint; }
27+
public String getVersion() { return version; }
28+
public List<String> getBoundAddress() { return boundAddress; }
29+
30+
/**
31+
* Create an EnrollmentToken
32+
*
33+
* @param apiKey API Key credential in the form apiKeyId:ApiKeySecret to be used for enroll calls
34+
* @param fingerprint hex encoded SHA256 fingerprint of the HTTP CA cert
35+
* @param version node version number
36+
* @param boundAddress IP Addresses and port numbers for the interfaces where the Elasticsearch node is listening on
37+
*/
38+
public EnrollmentToken(String apiKey, String fingerprint, String version, List<String> boundAddress) {
39+
this.apiKey = Objects.requireNonNull(apiKey);
40+
this.fingerprint = Objects.requireNonNull(fingerprint);
41+
this.version = Objects.requireNonNull(version);
42+
this.boundAddress = Objects.requireNonNull(boundAddress);
43+
}
44+
45+
public String getRaw() throws Exception {
46+
final XContentBuilder builder = JsonXContent.contentBuilder();
47+
builder.startObject();
48+
builder.field("ver", version);
49+
builder.startArray("adr");
50+
for (String bound_address : boundAddress) {
51+
builder.value(bound_address);
52+
}
53+
builder.endArray();
54+
builder.field("fgr", fingerprint);
55+
builder.field("key", apiKey);
56+
builder.endObject();
57+
return Strings.toString(builder);
58+
}
59+
60+
public String getEncoded() throws Exception {
61+
final String jsonString = getRaw();
62+
return Base64.getUrlEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8));
63+
}
64+
}
Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,71 +36,51 @@
3636
import java.net.URI;
3737
import java.net.URISyntaxException;
3838
import java.net.URL;
39-
import java.nio.charset.StandardCharsets;
4039
import java.security.PrivateKey;
4140
import java.security.cert.X509Certificate;
4241
import java.util.ArrayList;
43-
import java.util.Base64;
4442
import java.util.List;
4543
import java.util.Map;
4644
import java.util.Objects;
4745
import java.util.stream.Collectors;
4846

49-
public class CreateEnrollmentToken {
47+
public class EnrollmentTokenGenerator {
5048
protected static final String ENROLL_API_KEY_EXPIRATION = "30m";
5149

52-
private static final Logger logger = LogManager.getLogger(CreateEnrollmentToken.class);
50+
private static final Logger logger = LogManager.getLogger(EnrollmentTokenGenerator.class);
5351
private final Environment environment;
5452
private final SSLService sslService;
5553
private final CommandLineHttpClient client;
5654
private final URL defaultUrl;
5755

58-
public CreateEnrollmentToken(Environment environment) throws MalformedURLException {
56+
public EnrollmentTokenGenerator(Environment environment) throws MalformedURLException {
5957
this(environment, new CommandLineHttpClient(environment));
6058
}
6159

6260
// protected for testing
63-
protected CreateEnrollmentToken(Environment environment, CommandLineHttpClient client) throws MalformedURLException {
61+
protected EnrollmentTokenGenerator(Environment environment, CommandLineHttpClient client) throws MalformedURLException {
6462
this.environment = environment;
6563
this.sslService = new SSLService(environment);
6664
this.client = client;
6765
this.defaultUrl = new URL(client.getDefaultURL());
6866
}
6967

70-
public String createNodeEnrollmentToken(String user, SecureString password) throws Exception {
68+
public EnrollmentToken createNodeEnrollmentToken(String user, SecureString password) throws Exception {
7169
return this.create(user, password, NodeEnrollmentAction.NAME);
7270
}
7371

74-
public String createKibanaEnrollmentToken(String user, SecureString password) throws Exception {
72+
public EnrollmentToken createKibanaEnrollmentToken(String user, SecureString password) throws Exception {
7573
return this.create(user, password, KibanaEnrollmentAction.NAME);
7674
}
7775

78-
protected String create(String user, SecureString password, String action) throws Exception {
76+
protected EnrollmentToken create(String user, SecureString password, String action) throws Exception {
7977
if (XPackSettings.ENROLLMENT_ENABLED.get(environment.settings()) != true) {
8078
throw new IllegalStateException("[xpack.security.enrollment.enabled] must be set to `true` to create an enrollment token");
8179
}
8280
final String fingerprint = getCaFingerprint();
8381
final String apiKey = getApiKeyCredentials(user, password, action);
8482
final Tuple<List<String>, String> httpInfo = getNodeInfo(user, password);
85-
86-
try {
87-
final XContentBuilder builder = JsonXContent.contentBuilder();
88-
builder.startObject();
89-
builder.field("ver", httpInfo.v2());
90-
builder.startArray("adr");
91-
for (String bound_address : httpInfo.v1()) {
92-
builder.value(bound_address);
93-
}
94-
builder.endArray();
95-
builder.field("fgr", fingerprint);
96-
builder.field("key", apiKey);
97-
builder.endObject();
98-
final String jsonString = Strings.toString(builder);
99-
return Base64.getUrlEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8));
100-
} catch (Exception e) {
101-
logger.error(("Error generating enrollment token"), e);
102-
throw new IllegalStateException("Error generating enrollment token: " + e.getMessage());
103-
}
83+
return new EnrollmentToken(apiKey, fingerprint, httpInfo.v2(), httpInfo.v1());
10484
}
10585

10686
private HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws IOException {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.security.enrollment.tool;
9+
10+
import joptsimple.OptionSet;
11+
import joptsimple.OptionSpec;
12+
13+
import org.elasticsearch.cli.ExitCodes;
14+
import org.elasticsearch.cli.KeyStoreAwareCommand;
15+
import org.elasticsearch.cli.Terminal;
16+
import org.elasticsearch.cli.UserException;
17+
import org.elasticsearch.common.Strings;
18+
import org.elasticsearch.common.settings.KeyStoreWrapper;
19+
import org.elasticsearch.common.settings.SecureString;
20+
import org.elasticsearch.common.settings.Settings;
21+
import org.elasticsearch.common.xcontent.XContentBuilder;
22+
import org.elasticsearch.common.xcontent.json.JsonXContent;
23+
import org.elasticsearch.core.CheckedFunction;
24+
import org.elasticsearch.env.Environment;
25+
import org.elasticsearch.xpack.core.security.user.ElasticUser;
26+
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
27+
import org.elasticsearch.xpack.security.enrollment.EnrollmentToken;
28+
import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator;
29+
import org.elasticsearch.xpack.security.tool.CommandLineHttpClient;
30+
import org.elasticsearch.xpack.security.tool.HttpResponse;
31+
32+
import java.io.IOException;
33+
import java.net.HttpURLConnection;
34+
import java.net.MalformedURLException;
35+
import java.net.URISyntaxException;
36+
import java.net.URL;
37+
import java.security.SecureRandom;
38+
import java.util.function.Function;
39+
40+
import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL;
41+
42+
public class BootstrapPasswordAndEnrollmentTokenForInitialNode extends KeyStoreAwareCommand {
43+
private final CheckedFunction<Environment, EnrollmentTokenGenerator, Exception> createEnrollmentTokenFunction;
44+
private final Function<Environment, CommandLineHttpClient> clientFunction;
45+
private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction;
46+
private final OptionSpec<Void> includeNodeEnrollmentToken;
47+
private final SecureRandom secureRandom = new SecureRandom();
48+
49+
BootstrapPasswordAndEnrollmentTokenForInitialNode() {
50+
this(
51+
environment -> new CommandLineHttpClient(environment),
52+
environment -> KeyStoreWrapper.load(environment.configFile()),
53+
environment -> new EnrollmentTokenGenerator(environment)
54+
);
55+
}
56+
57+
BootstrapPasswordAndEnrollmentTokenForInitialNode(Function<Environment, CommandLineHttpClient> clientFunction,
58+
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction,
59+
CheckedFunction<Environment, EnrollmentTokenGenerator, Exception>
60+
createEnrollmentTokenFunction){
61+
super("Set elastic password and generate enrollment token for initial node");
62+
this.clientFunction = clientFunction;
63+
this.keyStoreFunction = keyStoreFunction;
64+
this.createEnrollmentTokenFunction = createEnrollmentTokenFunction;
65+
includeNodeEnrollmentToken = parser.accepts("include-node-enrollment-token", "determine that we have to generate " +
66+
"a node enrollment token");
67+
}
68+
69+
public static void main(String[] args) throws Exception {
70+
exit(new BootstrapPasswordAndEnrollmentTokenForInitialNode().main(args, Terminal.DEFAULT));
71+
}
72+
73+
@Override
74+
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
75+
final SecureString keystorePassword;
76+
try {
77+
keystorePassword = new SecureString(terminal.readSecret(""));
78+
} catch (Exception e) {
79+
throw new UserException(ExitCodes.USAGE, null);
80+
}
81+
82+
final Environment secureEnvironment = readSecureSettings(env, keystorePassword);
83+
final CommandLineHttpClient client = clientFunction.apply(secureEnvironment);
84+
final EnrollmentTokenGenerator enrollmentTokenGenerator = createEnrollmentTokenFunction.apply(secureEnvironment);
85+
final SecureString bootstrapPassword = ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.get(secureEnvironment.settings());
86+
try {
87+
String output;
88+
client.checkClusterHealthWithRetriesWaitingForCluster(ElasticUser.NAME, bootstrapPassword, 15);
89+
final EnrollmentToken kibanaToken = enrollmentTokenGenerator.createKibanaEnrollmentToken(ElasticUser.NAME, bootstrapPassword);
90+
output = "Kibana enrollment token: " + kibanaToken.getEncoded() + System.lineSeparator();
91+
output += "CA fingerprint: " + kibanaToken.getFingerprint() + System.lineSeparator();
92+
if (options.has(includeNodeEnrollmentToken)) {
93+
final EnrollmentToken nodeToken = enrollmentTokenGenerator.createNodeEnrollmentToken(ElasticUser.NAME, bootstrapPassword);
94+
output += "Node enrollment token: " + nodeToken.getEncoded() + System.lineSeparator();
95+
}
96+
if (ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.exists(secureEnvironment.settings()) == false) {
97+
output += "elastic user password: " + setElasticUserPassword(client, bootstrapPassword);
98+
}
99+
terminal.println(output);
100+
} catch (Exception e) {
101+
throw new UserException(ExitCodes.UNAVAILABLE, null);
102+
}
103+
}
104+
105+
protected SecureString setElasticUserPassword(CommandLineHttpClient client, SecureString bootstrapPassword) throws Exception {
106+
final URL passwordSetUrl = setElasticUserPasswordUrl(client);
107+
final HttpResponse response;
108+
SecureString password = new SecureString(generatePassword(20));
109+
try {
110+
response = client.execute("POST", passwordSetUrl, ElasticUser.NAME, bootstrapPassword,
111+
() -> {
112+
XContentBuilder xContentBuilder = JsonXContent.contentBuilder();
113+
xContentBuilder.startObject().field("password", password.toString()).endObject();
114+
return Strings.toString(xContentBuilder);
115+
}, CommandLineHttpClient::responseBuilder);
116+
if (response.getHttpStatus() != HttpURLConnection.HTTP_OK) {
117+
throw new UserException(ExitCodes.UNAVAILABLE, null);
118+
}
119+
} catch (IOException e) {
120+
throw new UserException(ExitCodes.IO_ERROR, null);
121+
}
122+
return password;
123+
}
124+
125+
Environment readSecureSettings(Environment env, SecureString keystorePassword) throws Exception {
126+
final KeyStoreWrapper keyStoreWrapper = keyStoreFunction.apply(env);
127+
keyStoreWrapper.decrypt(keystorePassword.getChars());
128+
Settings.Builder settingsBuilder = Settings.builder();
129+
settingsBuilder.put(env.settings(), true);
130+
if (settingsBuilder.getSecureSettings() == null) {
131+
settingsBuilder.setSecureSettings(keyStoreWrapper);
132+
}
133+
final Settings settings = settingsBuilder.build();
134+
return new Environment(settings, env.configFile());
135+
}
136+
137+
public static URL checkClusterHealthUrl(CommandLineHttpClient client) throws MalformedURLException, URISyntaxException {
138+
return createURL(new URL(client.getDefaultURL()), "_cluster/health", "?pretty");
139+
}
140+
141+
public static URL setElasticUserPasswordUrl(CommandLineHttpClient client) throws MalformedURLException, URISyntaxException {
142+
return createURL(new URL(client.getDefaultURL()), "/_security/user/" + ElasticUser.NAME + "/_password",
143+
"?pretty");
144+
}
145+
146+
protected char[] generatePassword(int passwordLength) {
147+
final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
148+
char[] characters = new char[passwordLength];
149+
for (int i = 0; i < passwordLength; ++i) {
150+
characters[i] = passwordChars[secureRandom.nextInt(passwordChars.length)];
151+
}
152+
return characters;
153+
}
154+
}

0 commit comments

Comments
 (0)