Skip to content

Commit 063a1f2

Browse files
Filter out CA PrivateKeyEntry when creating a KeyManager (#73807)
In 8.0, with security on by default, we store the HTTP layer CA PrivateKeyEntry in the http.ssl keystore (along with the node certificate) so that it is available in our Enrollment API transport actions. When loading a keystore, the current behavior is that the X509ExtendedKeyManager will iterate through the PrivateKeyEntry objects and will return the first key/certificate that satisfies the requirements of the client and the server configuration, and lacks any additional logic/filters. We need the KeyManager to deterministically pick the node certificate/key in all cases as this is the intended entry to be used for TLS on the HTTP layer. This change introduces filtering when creating the in-memory keystore the KeyManager is loaded with, so that it will not include PrivateKeyEntry objects when: - there are more than 1 PrivateKeyEntry objects in the keystore - The leaf certificate associated with the PrivateKeyEntry is a CA certificate Related: #75097 Co-authored-by: Ioannis Kakavas <[email protected]>
1 parent cdf67e0 commit 063a1f2

File tree

17 files changed

+231
-61
lines changed

17 files changed

+231
-61
lines changed

client/rest-high-level/build.gradle

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ apply plugin: 'elasticsearch.rest-test'
1414
apply plugin: 'elasticsearch.publish'
1515
apply plugin: 'com.github.johnrengelman.shadow'
1616
apply plugin: 'elasticsearch.rest-resources'
17+
apply plugin: 'elasticsearch.internal-test-artifact'
1718

1819
group = 'org.elasticsearch.client'
1920
archivesBaseName = 'elasticsearch-rest-high-level-client'
@@ -67,8 +68,6 @@ tasks.named('forbiddenApisMain').configure {
6768
File nodeCert = file("./testnode.crt")
6869
File nodeTrustStore = file("./testnode.jks")
6970
File pkiTrustCert = file("./src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.crt")
70-
File httpCaKeystore = file("./httpCa.p12");
71-
File transportKeystore = file("./transport.p12");
7271

7372
tasks.named("integTest").configure {
7473
systemProperty 'tests.rest.async', 'false'
@@ -118,8 +117,6 @@ testClusters.all {
118117
extraConfigFile nodeCert.name, nodeCert
119118
extraConfigFile nodeTrustStore.name, nodeTrustStore
120119
extraConfigFile pkiTrustCert.name, pkiTrustCert
121-
extraConfigFile httpCaKeystore.name, httpCaKeystore
122-
extraConfigFile transportKeystore.name, transportKeystore
123120

124121
setting 'xpack.searchable.snapshot.shared_cache.size', '1mb'
125122
setting 'xpack.searchable.snapshot.shared_cache.region_size', '16kb'
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 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import org.elasticsearch.gradle.internal.test.RestIntegTestTask
10+
import org.elasticsearch.gradle.internal.info.BuildParams
11+
12+
apply plugin: 'elasticsearch.java-rest-test'
13+
dependencies {
14+
javaRestTestImplementation(testArtifact(project(':client:rest-high-level')))
15+
}
16+
17+
tasks.matching{ it.name == "javaRestTest" }.configureEach {
18+
onlyIf { BuildParams.inFipsJvm == false}
19+
systemProperty 'tests.rest.cluster.username', System.getProperty('tests.rest.cluster.username', 'test_user')
20+
systemProperty 'tests.rest.cluster.password', System.getProperty('tests.rest.cluster.password', 'test-user-password')
21+
}
22+
23+
testClusters.matching { it.name == 'javaRestTest' }.configureEach {
24+
testDistribution = 'DEFAULT'
25+
numberOfNodes = 2
26+
setting 'xpack.license.self_generated.type', 'trial'
27+
setting 'xpack.security.enabled', 'true'
28+
setting 'xpack.security.authc.token.enabled', 'true'
29+
setting 'xpack.security.authc.api_key.enabled', 'true'
30+
31+
extraConfigFile 'httpCa.p12', file('./src/javaRestTest/resources/httpCa.p12')
32+
extraConfigFile 'transport.p12', file('./src/javaRestTest/resources/transport.p12')
33+
34+
// TBD: sync these settings (which options are set) with the ones we will be generating in #74868
35+
setting 'xpack.security.http.ssl.enabled', 'true'
36+
setting 'xpack.security.transport.ssl.enabled', 'true'
37+
setting 'xpack.security.http.ssl.keystore.path', 'httpCa.p12'
38+
setting 'xpack.security.transport.ssl.keystore.path', 'transport.p12'
39+
setting 'xpack.security.transport.ssl.verification_mode', 'certificate'
40+
41+
42+
keystore 'xpack.security.http.ssl.keystore.secure_password', 'password'
43+
keystore 'xpack.security.transport.ssl.keystore.secure_password', 'password'
44+
user username: 'admin_user', password: 'admin-password', role: 'superuser'
45+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.client;
10+
11+
import org.elasticsearch.client.security.KibanaEnrollmentResponse;
12+
import org.elasticsearch.client.security.NodeEnrollmentResponse;
13+
import org.elasticsearch.common.settings.SecureString;
14+
import org.elasticsearch.common.settings.Settings;
15+
import org.elasticsearch.common.util.concurrent.ThreadContext;
16+
import org.elasticsearch.core.PathUtils;
17+
import org.junit.AfterClass;
18+
import org.junit.BeforeClass;
19+
20+
import java.io.FileNotFoundException;
21+
import java.net.URL;
22+
import java.nio.file.Path;
23+
import java.util.List;
24+
25+
import static org.hamcrest.Matchers.endsWith;
26+
import static org.hamcrest.Matchers.equalTo;
27+
import static org.hamcrest.Matchers.notNullValue;
28+
29+
public class EnrollmentIT extends ESRestHighLevelClientTestCase {
30+
private static Path httpTrustStore;
31+
32+
@BeforeClass
33+
public static void findTrustStore() throws Exception {
34+
final URL resource = EnrollmentIT.class.getResource("/httpCa.p12");
35+
if (resource == null) {
36+
throw new FileNotFoundException("Cannot find classpath resource /httpCa.p12");
37+
}
38+
httpTrustStore = PathUtils.get(resource.toURI());
39+
}
40+
41+
@AfterClass
42+
public static void cleanupStatics() {
43+
httpTrustStore = null;
44+
}
45+
46+
@Override
47+
protected String getProtocol() {
48+
return "https";
49+
}
50+
51+
@Override
52+
protected Settings restClientSettings() {
53+
String token = basicAuthHeaderValue("admin_user", new SecureString("admin-password".toCharArray()));
54+
return Settings.builder()
55+
.put(ThreadContext.PREFIX + ".Authorization", token)
56+
.put(TRUSTSTORE_PATH, httpTrustStore)
57+
.put(TRUSTSTORE_PASSWORD, "password")
58+
.build();
59+
}
60+
61+
public void testEnrollNode() throws Exception {
62+
final NodeEnrollmentResponse nodeEnrollmentResponse =
63+
execute(highLevelClient().security()::enrollNode, highLevelClient().security()::enrollNodeAsync, RequestOptions.DEFAULT);
64+
assertThat(nodeEnrollmentResponse, notNullValue());
65+
assertThat(nodeEnrollmentResponse.getHttpCaKey(), endsWith("K2S3vidA="));
66+
assertThat(nodeEnrollmentResponse.getHttpCaCert(), endsWith("LfkRjirc="));
67+
assertThat(nodeEnrollmentResponse.getTransportKey(), endsWith("1I-r8vOQ=="));
68+
assertThat(nodeEnrollmentResponse.getTransportCert(), endsWith("OpTdtgJo="));
69+
List<String> nodesAddresses = nodeEnrollmentResponse.getNodesAddresses();
70+
assertThat(nodesAddresses.size(), equalTo(2));
71+
}
72+
73+
public void testEnrollKibana() throws Exception {
74+
KibanaEnrollmentResponse kibanaResponse =
75+
execute(highLevelClient().security()::enrollKibana, highLevelClient().security()::enrollKibanaAsync, RequestOptions.DEFAULT);
76+
assertThat(kibanaResponse, notNullValue());
77+
assertThat(kibanaResponse.getHttpCa()
78+
, endsWith("brcNC5xq6YE7C4_06nH7F6le4kE4Uo6c9fpkl4ehOxQxndNLn462tFF-8VBA8IftJ1PPWzqGxLsCTzM6p6w8sa-XhgNYglLfkRjirc="));
79+
assertNotNull(kibanaResponse.getPassword());
80+
assertThat(kibanaResponse.getPassword().toString().length(), equalTo(14));
81+
}
82+
}

client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import org.elasticsearch.client.security.GetRolesResponse;
2222
import org.elasticsearch.client.security.GetUsersRequest;
2323
import org.elasticsearch.client.security.GetUsersResponse;
24-
import org.elasticsearch.client.security.NodeEnrollmentResponse;
2524
import org.elasticsearch.client.security.PutRoleRequest;
2625
import org.elasticsearch.client.security.PutRoleResponse;
2726
import org.elasticsearch.client.security.PutUserRequest;
@@ -34,7 +33,6 @@
3433
import org.elasticsearch.client.security.user.privileges.IndicesPrivileges;
3534
import org.elasticsearch.client.security.user.privileges.IndicesPrivilegesTests;
3635
import org.elasticsearch.client.security.user.privileges.Role;
37-
import org.elasticsearch.client.security.KibanaEnrollmentResponse;
3836
import org.elasticsearch.core.CharArrays;
3937

4038
import java.io.IOException;
@@ -47,12 +45,10 @@
4745
import java.util.Map;
4846

4947
import static org.hamcrest.Matchers.empty;
50-
import static org.hamcrest.Matchers.endsWith;
5148
import static org.hamcrest.Matchers.equalTo;
5249
import static org.hamcrest.Matchers.is;
5350
import static org.hamcrest.Matchers.containsString;
5451
import static org.hamcrest.Matchers.contains;
55-
import static org.hamcrest.Matchers.notNullValue;
5652
import static org.hamcrest.Matchers.nullValue;
5753

5854
public class SecurityIT extends ESRestHighLevelClientTestCase {
@@ -196,30 +192,6 @@ public void testPutRole() throws Exception {
196192
assertThat(deleteRoleResponse.isFound(), is(true));
197193
}
198194

199-
@AwaitsFix(bugUrl = "Determine behavior for keystore with multiple keys")
200-
public void testEnrollNode() throws Exception {
201-
final NodeEnrollmentResponse nodeEnrollmentResponse =
202-
execute(highLevelClient().security()::enrollNode, highLevelClient().security()::enrollNodeAsync, RequestOptions.DEFAULT);
203-
assertThat(nodeEnrollmentResponse, notNullValue());
204-
assertThat(nodeEnrollmentResponse.getHttpCaKey(), endsWith("ECAwGGoA=="));
205-
assertThat(nodeEnrollmentResponse.getHttpCaCert(), endsWith("ECAwGGoA=="));
206-
assertThat(nodeEnrollmentResponse.getTransportKey(), endsWith("fSI09on8AgMBhqA="));
207-
assertThat(nodeEnrollmentResponse.getTransportCert(), endsWith("fSI09on8AgMBhqA="));
208-
List<String> nodesAddresses = nodeEnrollmentResponse.getNodesAddresses();
209-
assertThat(nodesAddresses.size(), equalTo(1));
210-
}
211-
212-
@AwaitsFix(bugUrl = "Determine behavior for keystores with multiple keys")
213-
public void testEnrollKibana() throws Exception {
214-
KibanaEnrollmentResponse kibanaResponse =
215-
execute(highLevelClient().security()::enrollKibana, highLevelClient().security()::enrollKibanaAsync, RequestOptions.DEFAULT);
216-
assertThat(kibanaResponse, notNullValue());
217-
assertThat(kibanaResponse.getHttpCa()
218-
, endsWith("OWFyeGNmcwovSDJReE1tSG1leXJRaWxYbXJPdk9PUDFTNGRrSTFXbFJLOFdaN3c9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"));
219-
assertNotNull(kibanaResponse.getPassword());
220-
assertThat(kibanaResponse.getPassword().toString().length(), equalTo(14));
221-
}
222-
223195
private void deleteUser(User user) throws IOException {
224196
final Request deleteUserRequest = new Request(HttpDelete.METHOD_NAME, "/_security/user/" + user.getUsername());
225197
highLevelClient().getLowLevelClient().performRequest(deleteUserRequest);

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ List projects = [
1313
'docs',
1414
'client:rest',
1515
'client:rest-high-level',
16+
'client:rest-high-level:qa:ssl-enabled',
1617
'client:sniffer',
1718
'client:transport',
1819
'client:test',

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ public static List<Setting<?>> getAllSettings() {
231231
settings.add(API_KEY_SERVICE_ENABLED_SETTING);
232232
settings.add(USER_SETTING);
233233
settings.add(PASSWORD_HASHING_ALGORITHM);
234+
settings.add(ENROLLMENT_ENABLED);
234235
return Collections.unmodifiableList(settings);
235236
}
236237

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfig.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ X509ExtendedKeyManager createKeyManager(@Nullable Environment environment) {
7777
try {
7878
KeyStore ks = getStore(ksPath, keyStoreType, keyStorePassword);
7979
checkKeyStore(ks);
80+
// TBD: filter out only http.ssl.keystore
81+
List<String> aliases = new ArrayList<>();
82+
for (String s : Collections.list(ks.aliases())) {
83+
if (ks.isKeyEntry(s)) {
84+
aliases.add(s);
85+
}
86+
}
87+
if (aliases.size() > 1) {
88+
for (String alias : aliases) {
89+
Certificate certificate = ks.getCertificate(alias);
90+
if (certificate instanceof X509Certificate) {
91+
if (((X509Certificate) certificate).getBasicConstraints() != -1) {
92+
ks.deleteEntry(alias);
93+
}
94+
}
95+
}
96+
}
8097
return CertParsingUtils.keyManager(ks, keyPassword.getChars(), keyStoreAlgorithm);
8198
} catch (FileNotFoundException | NoSuchFileException e) {
8299
throw missingKeyConfigFile(e, KEYSTORE_FILE, ksPath);

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfigTests.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@
1616
import javax.net.ssl.TrustManagerFactory;
1717
import javax.net.ssl.X509ExtendedKeyManager;
1818

19+
import java.io.InputStream;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.security.KeyStore;
1923
import java.security.PrivateKey;
24+
import java.security.cert.X509Certificate;
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
2028

2129
import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
2230
import static org.hamcrest.Matchers.containsString;
@@ -52,6 +60,52 @@ public void testKeyStorePathCanBeEmptyForPkcs11() throws Exception {
5260
assertThat(ee.getCause().getMessage(), containsString("PKCS11 not found"));
5361
}
5462

63+
public void testCreateKeyManagerFromPKCS12ContainingCA() throws Exception {
64+
assumeFalse("Can't run in a FIPS JVM", inFipsJvm());
65+
final Settings settings = Settings.builder().put("path.home", createTempDir()).build();
66+
final Path path = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/httpCa.p12");
67+
final SecureString keyStorePassword = new SecureString("password".toCharArray());
68+
final StoreKeyConfig keyConfig = new StoreKeyConfig(path.toString(), "PKCS12", keyStorePassword, keyStorePassword,
69+
KeyManagerFactory.getDefaultAlgorithm(), TrustManagerFactory.getDefaultAlgorithm());
70+
KeyStore keyStore = KeyStore.getInstance("PKCS12");
71+
try (InputStream in = Files.newInputStream(path)) {
72+
keyStore.load(in, keyStorePassword.getChars());
73+
}
74+
List<String> aliases = new ArrayList<>();
75+
for (String s : Collections.list(keyStore.aliases())) {
76+
if (keyStore.isKeyEntry(s)) {
77+
aliases.add(s);
78+
}
79+
}
80+
assertThat(aliases.size(), equalTo(2));
81+
final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager(TestEnvironment.newEnvironment(settings));
82+
for (String alias : aliases) {
83+
PrivateKey key = keyManager.getPrivateKey(alias);
84+
assertTrue(key == null || alias.equals("http"));
85+
}
86+
final String[] new_aliases = keyManager.getServerAliases("RSA", null);
87+
final X509Certificate[] certificates = keyManager.getCertificateChain("http");
88+
assertThat(new_aliases.length, equalTo(1));
89+
assertThat(certificates.length, equalTo(2));
90+
}
91+
92+
public void testCreateKeyManagerFromPKCS12ContainingCAOnly() throws Exception {
93+
assumeFalse("Can't run in a FIPS JVM", inFipsJvm());
94+
final Settings settings = Settings.builder().put("path.home", createTempDir()).build();
95+
final String path = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/ca.p12").toString();
96+
final SecureString keyStorePassword = new SecureString("password".toCharArray());
97+
final StoreKeyConfig keyConfig = new StoreKeyConfig(path, "PKCS12", keyStorePassword, keyStorePassword,
98+
KeyManagerFactory.getDefaultAlgorithm(), TrustManagerFactory.getDefaultAlgorithm());
99+
final X509ExtendedKeyManager keyManager = keyConfig.createKeyManager(TestEnvironment.newEnvironment(settings));
100+
final PrivateKey ca_key = keyManager.getPrivateKey("ca");
101+
final String[] aliases = keyManager.getServerAliases("RSA", null);
102+
final X509Certificate[] certificates = keyManager.getCertificateChain("ca");
103+
assertThat(ca_key, notNullValue());
104+
assertThat(aliases.length, equalTo(1));
105+
assertThat(aliases[0], equalTo("ca"));
106+
assertThat(certificates.length, equalTo(1));
107+
}
108+
55109
private void tryReadPrivateKeyFromKeyStore(String type, String extension) {
56110
final Settings settings = Settings.builder().put("path.home", createTempDir()).build();
57111
final String path = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode" + extension).toString();

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public class TransportKibanaEnrollmentAction extends HandledTransportAction<Kiba
101101
final char[] password = generateKibanaSystemPassword();
102102
final ChangePasswordRequest changePasswordRequest =
103103
new ChangePasswordRequestBuilder(client).username("kibana_system")
104-
.password(password, Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(environment.settings())))
104+
.password(password.clone(), Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(environment.settings())))
105105
.request();
106106
client.execute(ChangePasswordAction.INSTANCE, changePasswordRequest, ActionListener.wrap(response -> {
107107
logger.debug("Successfully set the password for user [kibana_system] during kibana enrollment");

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportNodeEnrollmentAction.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,20 +101,22 @@ protected void doExecute(Task task, NodeEnrollmentRequest request, ActionListene
101101
for (NodeInfo nodeInfo : response.getNodes()) {
102102
nodeList.add(nodeInfo.getInfo(TransportInfo.class).getAddress().publishAddress().toString());
103103
}
104+
try {
105+
final String httpCaKey = Base64.getUrlEncoder().encodeToString(httpCaKeysAndCertificates.get(0).v1().getEncoded());
106+
final String httpCaCert = Base64.getUrlEncoder().encodeToString(httpCaKeysAndCertificates.get(0).v2().getEncoded());
107+
final String transportKey =
108+
Base64.getUrlEncoder().encodeToString(transportKeysAndCertificates.get(0).v1().getEncoded());
109+
final String transportCert =
110+
Base64.getUrlEncoder().encodeToString(transportKeysAndCertificates.get(0).v2().getEncoded());
111+
listener.onResponse(new NodeEnrollmentResponse(httpCaKey,
112+
httpCaCert,
113+
transportKey,
114+
transportCert,
115+
nodeList));
116+
} catch (CertificateEncodingException e) {
117+
listener.onFailure(new ElasticsearchException("Unable to enroll node", e));
118+
}
104119
}, listener::onFailure
105120
));
106-
try {
107-
final String httpCaKey = Base64.getUrlEncoder().encodeToString(httpCaKeysAndCertificates.get(0).v1().getEncoded());
108-
final String httpCaCert = Base64.getUrlEncoder().encodeToString(httpCaKeysAndCertificates.get(0).v2().getEncoded());
109-
final String transportKey = Base64.getUrlEncoder().encodeToString(transportKeysAndCertificates.get(0).v1().getEncoded());
110-
final String transportCert = Base64.getUrlEncoder().encodeToString(transportKeysAndCertificates.get(0).v2().getEncoded());
111-
listener.onResponse(new NodeEnrollmentResponse(httpCaKey,
112-
httpCaCert,
113-
transportKey,
114-
transportCert,
115-
nodeList));
116-
} catch (CertificateEncodingException e) {
117-
listener.onFailure(new ElasticsearchException("Unable to enroll node", e));
118-
}
119121
}
120122
}

0 commit comments

Comments
 (0)