Skip to content

Commit 4c149e8

Browse files
authored
Generate and store password hash for elastic user (#76276)
For package installations (DEB,RPM), we are generating a random strong password for the elastic user on installation time so that we can show it to the user. We subsequently hash and store this password in the elasticsearch.keystore so that the node can pick it up on the first run and use it to populate the relevant document for the elastic user in the security index. This change implements a class that can be called from the package installation scripts (postinst) to - Generate a strong password - Hash it with the configured(default) password hashing algo - Store it in the elasticsearch.keystore - Print it in stdout so that it the bash script can capture it.
1 parent 22e9d37 commit 4c149e8

File tree

8 files changed

+231
-39
lines changed

8 files changed

+231
-39
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public class ReservedRealm extends CachingUsernamePasswordRealm {
5959
private final ReservedUserInfo bootstrapUserInfo;
6060
public static final Setting<SecureString> BOOTSTRAP_ELASTIC_PASSWORD = SecureSetting.secureString("bootstrap.password",
6161
KeyStoreWrapper.SEED_SETTING);
62+
public static final Setting<SecureString> AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH =
63+
SecureSetting.secureString("autoconfig.password_hash", null);
6264

6365
private final NativeUsersStore nativeUsersStore;
6466
private final AnonymousUser anonymousUser;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.function.Function;
3333

3434
import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL;
35+
import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword;
3536

3637
public class ResetElasticPasswordTool extends BaseRunAsSuperuserCommand {
3738

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
12+
import org.elasticsearch.cli.ExitCodes;
13+
import org.elasticsearch.cli.KeyStoreAwareCommand;
14+
import org.elasticsearch.cli.Terminal;
15+
import org.elasticsearch.cli.UserException;
16+
import org.elasticsearch.common.settings.KeyStoreWrapper;
17+
import org.elasticsearch.common.settings.SecureString;
18+
import org.elasticsearch.env.Environment;
19+
import org.elasticsearch.xpack.core.XPackSettings;
20+
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
21+
22+
import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH;
23+
import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword;
24+
25+
/**
26+
* This tool is not meant to be used in user facing CLI tools. It is called by the package installers only upon installation. It
27+
* <ul>
28+
* <li>generates a random strong password for the elastic user/li>
29+
* <li>stores a salted hash of that password in the elasticsearch keystore, in the autoconfiguration.password_hash setting</li>
30+
* </ul>
31+
* This password is subsequently picked up by the node on startup and is set as the password of the elastic user in the security index.
32+
*
33+
* There is currently no way to set the password of the elasticsearch keystore during package installation. This tool
34+
* is called by the package installer only on installation (not on upgrades) so we can be certain that the keystore
35+
* has an empty password (obfuscated).
36+
*
37+
* The generated password is written to stdout upon success. Error messages are printed to stderr.
38+
*/
39+
public class AutoConfigGenerateElasticPasswordHash extends KeyStoreAwareCommand {
40+
41+
public AutoConfigGenerateElasticPasswordHash() {
42+
super("Generates a password hash for for the elastic user and stores it in elasticsearch.keystore");
43+
}
44+
45+
public static void main(String[] args) throws Exception {
46+
exit(new AutoConfigGenerateElasticPasswordHash().main(args, Terminal.DEFAULT));
47+
}
48+
49+
@Override
50+
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
51+
final Hasher hasher = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(env.settings()));
52+
try (
53+
SecureString elasticPassword = new SecureString(generatePassword(20));
54+
KeyStoreWrapper nodeKeystore = KeyStoreWrapper.bootstrap(env.configFile(), () -> new SecureString(new char[0]))
55+
) {
56+
nodeKeystore.setString(AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), hasher.hash(elasticPassword));
57+
nodeKeystore.save(env.configFile(), new char[0]);
58+
terminal.print(Terminal.Verbosity.NORMAL, elasticPassword.toString());
59+
} catch (Exception e) {
60+
throw new UserException(ExitCodes.CANT_CREATE, "Failed to generate a password for the elastic user", e);
61+
}
62+
}
63+
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNode.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,16 @@
3434
import java.net.MalformedURLException;
3535
import java.net.URISyntaxException;
3636
import java.net.URL;
37-
import java.security.SecureRandom;
3837
import java.util.function.Function;
3938

4039
import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL;
40+
import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword;
4141

4242
public class BootstrapPasswordAndEnrollmentTokenForInitialNode extends KeyStoreAwareCommand {
4343
private final CheckedFunction<Environment, EnrollmentTokenGenerator, Exception> createEnrollmentTokenFunction;
4444
private final Function<Environment, CommandLineHttpClient> clientFunction;
4545
private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction;
4646
private final OptionSpec<Void> includeNodeEnrollmentToken;
47-
private final SecureRandom secureRandom = new SecureRandom();
4847

4948
BootstrapPasswordAndEnrollmentTokenForInitialNode() {
5049
this(
@@ -142,13 +141,4 @@ public static URL setElasticUserPasswordUrl(CommandLineHttpClient client) throws
142141
return createURL(new URL(client.getDefaultURL()), "/_security/user/" + ElasticUser.NAME + "/_password",
143142
"?pretty");
144143
}
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-
}
154144
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import java.net.HttpURLConnection;
3333
import java.net.URL;
3434
import java.nio.file.Path;
35-
import java.security.SecureRandom;
3635
import java.util.ArrayList;
3736
import java.util.Arrays;
3837
import java.util.HashMap;
@@ -42,6 +41,9 @@
4241
import java.util.function.Function;
4342
import java.util.stream.Collectors;
4443

44+
import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword;
45+
import static org.elasticsearch.xpack.security.tool.CommandUtils.generateUsername;
46+
4547
/**
4648
* A {@link KeyStoreAwareCommand} that can be extended fpr any CLI tool that needs to allow a local user with
4749
* filesystem write access to perform actions on the node as a superuser. It leverages temporary file realm users
@@ -55,7 +57,6 @@ public abstract class BaseRunAsSuperuserCommand extends KeyStoreAwareCommand {
5557
private final OptionSpecBuilder force;
5658
private final Function<Environment, CommandLineHttpClient> clientFunction;
5759
private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction;
58-
final SecureRandom secureRandom = new SecureRandom();
5960

6061
public BaseRunAsSuperuserCommand(
6162
Function<Environment, CommandLineHttpClient> clientFunction,
@@ -90,7 +91,7 @@ protected final void execute(Terminal terminal, OptionSet options, Environment e
9091
settings = env.settings();
9192
}
9293

93-
final String username = generateUsername();
94+
final String username = generateUsername("autogenerated_", null, 8);
9495
try (SecureString password = new SecureString(generatePassword(PASSWORD_LENGTH))){
9596
final Hasher hasher = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings));
9697
final Path passwordFile = FileUserPasswdStore.resolveFile(newEnv);
@@ -244,25 +245,6 @@ private void checkClusterHealthWithRetries(Environment env, Terminal terminal, S
244245
}
245246
}
246247

247-
protected char[] generatePassword(int passwordLength) {
248-
final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
249-
char[] characters = new char[passwordLength];
250-
for (int i = 0; i < passwordLength; ++i) {
251-
characters[i] = passwordChars[secureRandom.nextInt(passwordChars.length)];
252-
}
253-
return characters;
254-
}
255-
256-
private String generateUsername() {
257-
final char[] usernameChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").toCharArray();
258-
int usernameLength = 8;
259-
char[] characters = new char[usernameLength];
260-
for (int i = 0; i < usernameLength; ++i) {
261-
characters[i] = usernameChars[secureRandom.nextInt(usernameChars.length)];
262-
}
263-
return "enrollment_autogenerated_" + new String(characters);
264-
}
265-
266248
/**
267249
* This is called after we have created a temporary superuser in the file realm and verified that its
268250
* credentials work. The username and password of the generated user are passed as parameters. Overriding methods should
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.tool;
9+
10+
import org.elasticsearch.core.Nullable;
11+
import java.security.SecureRandom;
12+
13+
public class CommandUtils {
14+
15+
static final SecureRandom SECURE_RANDOM = new SecureRandom();
16+
17+
/**
18+
* Generates a password of a given length from a set of predefined allowed chars.
19+
* @param passwordLength the length of the password
20+
* @return the char array with the password
21+
*/
22+
public static char[] generatePassword(int passwordLength) {
23+
final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
24+
char[] characters = new char[passwordLength];
25+
for (int i = 0; i < passwordLength; ++i) {
26+
characters[i] = passwordChars[SECURE_RANDOM.nextInt(passwordChars.length)];
27+
}
28+
return characters;
29+
}
30+
31+
/**
32+
* Generates a string that can be used as a username, possibly consisting of a chosen prefix and suffix
33+
*/
34+
protected static String generateUsername(@Nullable String prefix, @Nullable String suffix, int length) {
35+
final char[] usernameChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").toCharArray();
36+
37+
final String prefixString = null == prefix ? "" : prefix;
38+
final String suffixString = null == suffix ? "" : prefix;
39+
char[] characters = new char[length];
40+
for (int i = 0; i < length; ++i) {
41+
characters[i] = usernameChars[SECURE_RANDOM.nextInt(usernameChars.length)];
42+
}
43+
return prefixString + new String(characters) + suffixString;
44+
}
45+
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNodeTests.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@ protected Command newCommand() {
5757
return new BootstrapPasswordAndEnrollmentTokenForInitialNode(environment -> client, environment -> keyStoreWrapper,
5858
environment -> enrollmentTokenGenerator) {
5959
@Override
60-
protected char[] generatePassword(int passwordLength) {
61-
String password = "Aljngvodjb94j8HSY803";
62-
return password.toCharArray();
63-
}
64-
@Override
6560
protected Environment readSecureSettings(Environment env, SecureString password) {
6661
return new Environment(settings, tempDir);
6762
}
@@ -129,7 +124,7 @@ public void testGenerateNewPasswordSuccess() throws Exception {
129124
terminal.addSecretInput("password");
130125
String includeNodeEnrollmentToken = randomBoolean() ? "--include-node-enrollment-token" : "";
131126
String output = execute(includeNodeEnrollmentToken);
132-
assertThat(output, containsString("elastic user password: Aljngvodjb94j8HSY803"));
127+
assertThat(output, containsString("elastic user password: "));
133128
assertThat(output, containsString("CA fingerprint: ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428" +
134129
"f8a91362d"));
135130
assertThat(output, containsString("Kibana enrollment token: eyJ2ZXIiOiI4LjAuMCIsImFkciI6WyJbMTkyLjE2OC4wL" +
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 com.google.common.jimfs.Configuration;
11+
import com.google.common.jimfs.Jimfs;
12+
13+
import org.elasticsearch.cli.Command;
14+
import org.elasticsearch.cli.CommandTestCase;
15+
import org.elasticsearch.cli.UserException;
16+
import org.elasticsearch.common.settings.KeyStoreWrapper;
17+
import org.elasticsearch.common.settings.Settings;
18+
import org.elasticsearch.core.PathUtilsForTesting;
19+
import org.elasticsearch.core.internal.io.IOUtils;
20+
import org.elasticsearch.env.Environment;
21+
import org.elasticsearch.xpack.core.XPackSettings;
22+
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
23+
import org.junit.AfterClass;
24+
import org.junit.Before;
25+
import org.junit.BeforeClass;
26+
27+
import java.io.IOException;
28+
import java.nio.file.FileSystem;
29+
import java.nio.file.Files;
30+
import java.nio.file.Path;
31+
import java.util.Map;
32+
33+
import static org.elasticsearch.test.SecurityIntegTestCase.getFastStoredHashAlgoForTests;
34+
import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH;
35+
import static org.hamcrest.Matchers.containsInAnyOrder;
36+
import static org.hamcrest.Matchers.emptyString;
37+
import static org.hamcrest.Matchers.equalTo;
38+
import static org.hamcrest.Matchers.hasLength;
39+
import static org.hamcrest.Matchers.is;
40+
41+
public class AutoConfigGenerateElasticPasswordHashTests extends CommandTestCase {
42+
43+
private static FileSystem jimfs;
44+
private static Hasher hasher;
45+
private Path confDir;
46+
private Settings settings;
47+
private Environment env;
48+
49+
@BeforeClass
50+
public static void muteInFips() {
51+
assumeFalse("Can't run in a FIPS JVM, uses keystore that is not password protected", inFipsJvm());
52+
}
53+
54+
@BeforeClass
55+
public static void setupJimfs() {
56+
Configuration conf = Configuration.unix().toBuilder().setAttributeViews("posix").build();
57+
jimfs = Jimfs.newFileSystem(conf);
58+
PathUtilsForTesting.installMock(jimfs);
59+
}
60+
61+
@Before
62+
public void setup() throws Exception {
63+
Path homeDir = jimfs.getPath("eshome");
64+
IOUtils.rm(homeDir);
65+
confDir = homeDir.resolve("config");
66+
Files.createDirectories(confDir);
67+
hasher = getFastStoredHashAlgoForTests();
68+
settings = Settings.builder()
69+
.put("path.home", homeDir)
70+
.put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), hasher.name())
71+
.build();
72+
env = new Environment(AutoConfigGenerateElasticPasswordHashTests.this.settings, confDir);
73+
KeyStoreWrapper keystore = KeyStoreWrapper.create();
74+
keystore.save(confDir, new char[0]);
75+
}
76+
77+
@AfterClass
78+
public static void closeJimfs() throws IOException {
79+
if (jimfs != null) {
80+
jimfs.close();
81+
jimfs = null;
82+
}
83+
}
84+
85+
@Override protected Command newCommand() {
86+
return new AutoConfigGenerateElasticPasswordHash() {
87+
@Override
88+
protected Environment createEnv(Map<String, String> settings) throws UserException {
89+
return env;
90+
}
91+
};
92+
}
93+
94+
public void testSuccessfullyGenerateAndStoreHash() throws Exception {
95+
execute();
96+
assertThat(terminal.getOutput(), hasLength(20));
97+
KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(env.configFile());
98+
assertNotNull(keyStoreWrapper);
99+
keyStoreWrapper.decrypt(new char[0]);
100+
assertThat(keyStoreWrapper.getSettingNames(),
101+
containsInAnyOrder(AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), "keystore.seed"));
102+
}
103+
104+
public void testExistingKeystoreWithWrongPassword() throws Exception {
105+
KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(env.configFile());
106+
assertNotNull(keyStoreWrapper);
107+
keyStoreWrapper.decrypt(new char[0]);
108+
// set a random password so that we fail to decrypt it in GenerateElasticPasswordHash#execute
109+
keyStoreWrapper.save(env.configFile(), randomAlphaOfLength(16).toCharArray());
110+
UserException e = expectThrows(UserException.class, this::execute);
111+
assertThat(e.getMessage(), equalTo("Failed to generate a password for the elastic user"));
112+
assertThat(terminal.getOutput(), is(emptyString()));
113+
}
114+
}

0 commit comments

Comments
 (0)