Skip to content

Commit 405cd67

Browse files
authored
[Backport 7.11] Add a smoke test for security realms (#68951)
This changes adds a new QA test that runs a smoke test on a node that has been configured with one realm of each type. Not all of the realms work, because some of them would depend on external fixtures (LDAP, SAML, etc) and this particularly test suite is intended to be as stable as possible and have no external dependencies. The primary purpose of this test is to catch any issues that prevent a node from starting with particular realms configurd (e.g. security manager or classpath issues). We don't depend on external fixtures becaused we want this to be a smoke test that clearly indicates when a (seemingly unrelated) change in Elasticsearch has unintended consequences on realms. The use of external dependencies would increase the number of things that could go wrong and move this from a smoke test to a potentially noisy integration test. Backport of: #68881
1 parent 20ae28a commit 405cd67

File tree

23 files changed

+823
-14
lines changed

23 files changed

+823
-14
lines changed

test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.apache.http.client.methods.HttpPut;
1717
import org.apache.http.message.BasicHeader;
1818
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
19+
import org.apache.http.ssl.SSLContextBuilder;
1920
import org.apache.http.ssl.SSLContexts;
2021
import org.apache.http.util.EntityUtils;
2122
import org.apache.logging.log4j.message.ParameterizedMessage;
@@ -68,10 +69,12 @@
6869
import java.nio.charset.StandardCharsets;
6970
import java.nio.file.Files;
7071
import java.nio.file.Path;
72+
import java.security.GeneralSecurityException;
7173
import java.security.KeyManagementException;
7274
import java.security.KeyStore;
7375
import java.security.KeyStoreException;
7476
import java.security.NoSuchAlgorithmException;
77+
import java.security.PrivateKey;
7578
import java.security.cert.Certificate;
7679
import java.security.cert.CertificateException;
7780
import java.util.ArrayList;
@@ -81,6 +84,7 @@
8184
import java.util.HashSet;
8285
import java.util.List;
8386
import java.util.Map;
87+
import java.util.Objects;
8488
import java.util.Set;
8589
import java.util.TreeSet;
8690
import java.util.concurrent.TimeUnit;
@@ -106,7 +110,13 @@
106110
public abstract class ESRestTestCase extends ESTestCase {
107111
public static final String TRUSTSTORE_PATH = "truststore.path";
108112
public static final String TRUSTSTORE_PASSWORD = "truststore.password";
113+
109114
public static final String CERTIFICATE_AUTHORITIES = "certificate_authorities";
115+
116+
public static final String CLIENT_CERT_PATH = "client.cert.path";
117+
public static final String CLIENT_KEY_PATH = "client.key.path";
118+
public static final String CLIENT_KEY_PASSWORD = "client.key.password";
119+
110120
public static final String CLIENT_SOCKET_TIMEOUT = "client.socket.timeout";
111121
public static final String CLIENT_PATH_PREFIX = "client.path.prefix";
112122

@@ -1013,29 +1023,30 @@ protected RestClient buildClient(Settings settings, HttpHost[] hosts) throws IOE
10131023
}
10141024

10151025
protected static void configureClient(RestClientBuilder builder, Settings settings) throws IOException {
1026+
String truststorePath = settings.get(TRUSTSTORE_PATH);
10161027
String certificateAuthorities = settings.get(CERTIFICATE_AUTHORITIES);
1017-
String keystorePath = settings.get(TRUSTSTORE_PATH);
1028+
String clientCertificatePath = settings.get(CLIENT_CERT_PATH);
10181029

1019-
if (certificateAuthorities != null && keystorePath != null) {
1030+
if (certificateAuthorities != null && truststorePath != null) {
10201031
throw new IllegalStateException("Cannot set both " + CERTIFICATE_AUTHORITIES + " and " + TRUSTSTORE_PATH
10211032
+ ". Please configure one of these.");
10221033

10231034
}
1024-
if (keystorePath != null) {
1035+
if (truststorePath != null) {
10251036
if (inFipsJvm()) {
1026-
throw new IllegalStateException("Keystore " + keystorePath + "cannot be used in FIPS 140 mode. Please configure "
1037+
throw new IllegalStateException("Keystore " + truststorePath + "cannot be used in FIPS 140 mode. Please configure "
10271038
+ CERTIFICATE_AUTHORITIES + " with a PEM encoded trusted CA/certificate instead");
10281039
}
10291040
final String keystorePass = settings.get(TRUSTSTORE_PASSWORD);
10301041
if (keystorePass == null) {
10311042
throw new IllegalStateException(TRUSTSTORE_PATH + " is provided but not " + TRUSTSTORE_PASSWORD);
10321043
}
1033-
Path path = PathUtils.get(keystorePath);
1034-
if (!Files.exists(path)) {
1044+
Path path = PathUtils.get(truststorePath);
1045+
if (Files.exists(path) == false) {
10351046
throw new IllegalStateException(TRUSTSTORE_PATH + " is set but points to a non-existing file");
10361047
}
10371048
try {
1038-
final String keyStoreType = keystorePath.endsWith(".p12") ? "PKCS12" : "jks";
1049+
final String keyStoreType = truststorePath.endsWith(".p12") ? "PKCS12" : "jks";
10391050
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
10401051
try (InputStream is = Files.newInputStream(path)) {
10411052
keyStore.load(is, keystorePass.toCharArray());
@@ -1048,21 +1059,35 @@ protected static void configureClient(RestClientBuilder builder, Settings settin
10481059
}
10491060
}
10501061
if (certificateAuthorities != null) {
1051-
Path path = PathUtils.get(certificateAuthorities);
1052-
if (!Files.exists(path)) {
1062+
Path caPath = PathUtils.get(certificateAuthorities);
1063+
if (Files.exists(caPath) == false) {
10531064
throw new IllegalStateException(CERTIFICATE_AUTHORITIES + " is set but points to a non-existing file");
10541065
}
10551066
try {
10561067
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
10571068
keyStore.load(null, null);
1058-
Certificate cert = PemUtils.readCertificates(Collections.singletonList(path)).get(0);
1059-
keyStore.setCertificateEntry(cert.toString(), cert);
1060-
SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(keyStore, null).build();
1069+
Certificate caCert = PemUtils.readCertificates(Collections.singletonList(caPath)).get(0);
1070+
keyStore.setCertificateEntry(caCert.toString(), caCert);
1071+
final SSLContextBuilder sslContextBuilder = SSLContexts.custom();
1072+
if (clientCertificatePath != null) {
1073+
final Path certPath = PathUtils.get(clientCertificatePath);
1074+
final Path keyPath = PathUtils.get(Objects.requireNonNull(settings.get(CLIENT_KEY_PATH), "No key provided"));
1075+
final String password = settings.get(CLIENT_KEY_PASSWORD);
1076+
final char[] passwordChars = password == null ? null : password.toCharArray();
1077+
final PrivateKey key = PemUtils.readPrivateKey(keyPath, () -> passwordChars);
1078+
final Certificate[] clientCertChain
1079+
= PemUtils.readCertificates(Collections.singletonList(certPath)).toArray(new Certificate[1]);
1080+
keyStore.setKeyEntry("client", key, passwordChars, clientCertChain);
1081+
sslContextBuilder.loadKeyMaterial(keyStore, passwordChars);
1082+
}
1083+
SSLContext sslcontext = sslContextBuilder.loadTrustMaterial(keyStore, null).build();
10611084
SSLIOSessionStrategy sessionStrategy = new SSLIOSessionStrategy(sslcontext);
10621085
builder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setSSLStrategy(sessionStrategy));
1063-
} catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException | CertificateException e) {
1086+
} catch (GeneralSecurityException e) {
10641087
throw new RuntimeException("Error setting up ssl", e);
10651088
}
1089+
} else if (clientCertificatePath != null) {
1090+
throw new IllegalStateException("Client certificates are currently only supported when using a custom CA");
10661091
}
10671092
Map<String, String> headers = ThreadContext.buildDefaultHeaders(settings);
10681093
Header[] defaultHeaders = new Header[headers.size()];

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ public static X509ExtendedTrustManager trustManager(Certificate[] certificates)
258258
return trustManager(store, TrustManagerFactory.getDefaultAlgorithm());
259259
}
260260

261-
static KeyStore trustStore(Certificate[] certificates)
261+
public static KeyStore trustStore(Certificate[] certificates)
262262
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
263263
assert certificates != null : "Cannot create trust store with null certificates";
264264
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* This QA test is intended to smoke test all security realms with minimal dependencies.
3+
* That is, it makes sure a node that has every realm configured can start, and tests those realms that can be tested without needing external services.
4+
* This tradeoff is intentional because we want this set of tests to be very stable - failures in this QA suite should be an indicator that
5+
* something is broken in Elasticsearch (and not that an external docker fixture broke)
6+
* This test is also intended to work correctly on FIPS mode because we also want to know if a realm breaks on FIPS.
7+
*/
8+
9+
apply plugin: 'elasticsearch.java-rest-test'
10+
11+
dependencies {
12+
javaRestTestImplementation project(path: xpackModule('core'))
13+
javaRestTestImplementation project(path: xpackModule('security'), configuration: 'testArtifacts')
14+
javaRestTestImplementation project(path: xpackModule('core'), configuration: 'testArtifacts')
15+
}
16+
17+
testClusters.javaRestTest {
18+
testDistribution = 'DEFAULT'
19+
numberOfNodes = 2
20+
21+
extraConfigFile 'http-server.key', file('src/javaRestTest/resources/ssl/http-server.key')
22+
extraConfigFile 'http-server.crt', file('src/javaRestTest/resources/ssl/http-server.crt')
23+
extraConfigFile 'http-client-ca.crt', file('src/javaRestTest/resources/ssl/http-client-ca.crt')
24+
extraConfigFile 'saml-metadata.xml', file('src/javaRestTest/resources/saml-metadata.xml')
25+
extraConfigFile 'kerberos.keytab', file('src/javaRestTest/resources/kerberos.keytab')
26+
extraConfigFile 'oidc-jwkset.json', file('src/javaRestTest/resources/oidc-jwkset.json')
27+
28+
setting 'xpack.ml.enabled', 'false'
29+
setting 'xpack.security.enabled', 'true'
30+
setting 'xpack.security.authc.token.enabled', 'true'
31+
setting 'xpack.security.authc.api_key.enabled', 'true'
32+
33+
// Need a trial license (not basic) to enable all realms
34+
setting 'xpack.license.self_generated.type', 'trial'
35+
// Need SSL to enable PKI realms
36+
setting 'xpack.security.http.ssl.enabled', 'true'
37+
setting 'xpack.security.http.ssl.certificate', 'http-server.crt'
38+
setting 'xpack.security.http.ssl.key', 'http-server.key'
39+
setting 'xpack.security.http.ssl.key_passphrase', 'http-password'
40+
setting 'xpack.security.http.ssl.client_authentication', 'optional'
41+
setting 'xpack.security.http.ssl.certificate_authorities', 'http-client-ca.crt'
42+
43+
// Don't need transport SSL, so leave it out
44+
setting 'xpack.security.transport.ssl.enabled', 'false'
45+
46+
// Configure every realm type
47+
// - File
48+
setting 'xpack.security.authc.realms.file.file0.order', '0'
49+
// - Native
50+
setting 'xpack.security.authc.realms.native.native1.order', '1'
51+
// - LDAP (configured but won't work because we don't want external fixtures in this test suite)
52+
setting 'xpack.security.authc.realms.ldap.ldap2.order', '2'
53+
setting 'xpack.security.authc.realms.ldap.ldap2.url', 'ldap://localhost:7777'
54+
setting 'xpack.security.authc.realms.ldap.ldap2.user_search.base_dn', 'OU=users,DC=example,DC=com'
55+
// - AD (configured but won't work because we don't want external fixtures in this test suite)
56+
setting 'xpack.security.authc.realms.active_directory.ad3.order', '3'
57+
setting 'xpack.security.authc.realms.active_directory.ad3.domain_name', 'localhost'
58+
// - PKI (works)
59+
setting 'xpack.security.authc.realms.pki.pki4.order', '4'
60+
// - SAML (configured but won't work because we don't want external fixtures in this test suite)
61+
setting 'xpack.security.authc.realms.saml.saml5.order', '5'
62+
setting 'xpack.security.authc.realms.saml.saml5.idp.metadata.path', 'saml-metadata.xml'
63+
setting 'xpack.security.authc.realms.saml.saml5.idp.entity_id', 'http://idp.example.com/'
64+
setting 'xpack.security.authc.realms.saml.saml5.sp.entity_id', 'http://kibana.example.net/'
65+
setting 'xpack.security.authc.realms.saml.saml5.sp.acs', 'http://kibana.example.net/api/security/v1/saml'
66+
setting 'xpack.security.authc.realms.saml.saml5.attributes.principal', 'uid'
67+
// - Kerberos (configured but won't work because we don't want external fixtures in this test suite)
68+
setting 'xpack.security.authc.realms.kerberos.kerb6.order', '6'
69+
setting 'xpack.security.authc.realms.kerberos.kerb6.keytab.path', 'kerberos.keytab'
70+
// - OIDC (configured but won't work because we don't want external fixtures in this test suite)
71+
setting 'xpack.security.authc.realms.oidc.openid7.order', '7'
72+
setting 'xpack.security.authc.realms.oidc.openid7.rp.client_id', 'http://rp.example.net'
73+
setting 'xpack.security.authc.realms.oidc.openid7.rp.response_type', 'id_token'
74+
setting 'xpack.security.authc.realms.oidc.openid7.rp.redirect_uri', 'https://kibana.example.net/api/security/v1/oidc'
75+
setting 'xpack.security.authc.realms.oidc.openid7.op.issuer', 'https://op.example.com/'
76+
setting 'xpack.security.authc.realms.oidc.openid7.op.authorization_endpoint', 'https://op.example.com/auth'
77+
setting 'xpack.security.authc.realms.oidc.openid7.op.jwkset_path', 'oidc-jwkset.json'
78+
setting 'xpack.security.authc.realms.oidc.openid7.claims.principal', 'sub'
79+
keystore 'xpack.security.authc.realms.oidc.openid7.rp.client_secret', 'this-is-my-secret'
80+
81+
extraConfigFile 'roles.yml', file('src/javaRestTest/resources/roles.yml')
82+
user username: "admin_user", password: "admin-password"
83+
user username: "security_test_user", password: "security-test-password", role: "security_test_role"
84+
}
85+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.authc;
9+
10+
import org.elasticsearch.client.RequestOptions;
11+
import org.elasticsearch.common.settings.SecureString;
12+
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
13+
14+
import java.io.IOException;
15+
import java.util.Map;
16+
17+
/**
18+
* Integration Rest Test for testing authentication when all possible realms are configured
19+
*/
20+
public class FileRealmAuthIT extends SecurityRealmSmokeTestCase {
21+
22+
// Declared in build.gradle
23+
private static final String USERNAME = "security_test_user";
24+
private static final SecureString PASSWORD = new SecureString("security-test-password".toCharArray());
25+
private static final String ROLE_NAME = "security_test_role";
26+
27+
public void testAuthenticationUsingFileRealm() throws IOException {
28+
Map<String, Object> authenticate = super.authenticate(
29+
RequestOptions.DEFAULT.toBuilder().addHeader("Authorization",
30+
UsernamePasswordToken.basicAuthHeaderValue(USERNAME, PASSWORD))
31+
);
32+
33+
assertUsername(authenticate, USERNAME);
34+
assertRealm(authenticate, "file", "file0");
35+
assertRoles(authenticate, ROLE_NAME);
36+
}
37+
38+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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.authc;
9+
10+
import org.elasticsearch.client.RequestOptions;
11+
import org.elasticsearch.common.settings.SecureString;
12+
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
13+
import org.junit.After;
14+
import org.junit.Before;
15+
16+
import java.io.IOException;
17+
import java.util.Collections;
18+
import java.util.Map;
19+
20+
/**
21+
* Integration Rest Test for testing authentication when all possible realms are configured
22+
*/
23+
public class NativeRealmAuthIT extends SecurityRealmSmokeTestCase {
24+
25+
private static final String USERNAME = "test_native_user";
26+
private static final SecureString PASSWORD = new SecureString("native-user-password".toCharArray());
27+
private static final String ROLE_NAME = "native_role";
28+
29+
@Before
30+
public void createUsersAndRoles() throws IOException {
31+
createUser(USERNAME, PASSWORD, Collections.singletonList(ROLE_NAME));
32+
createRole("native_role", Collections.singleton("monitor"));
33+
}
34+
35+
@After
36+
public void cleanUp() throws IOException {
37+
deleteUser(USERNAME);
38+
deleteRole(ROLE_NAME);
39+
}
40+
41+
public void testAuthenticationUsingNativeRealm() throws IOException {
42+
Map<String, Object> authenticate = super.authenticate(
43+
RequestOptions.DEFAULT.toBuilder().addHeader("Authorization",
44+
UsernamePasswordToken.basicAuthHeaderValue(USERNAME, PASSWORD))
45+
);
46+
47+
assertUsername(authenticate, USERNAME);
48+
assertRealm(authenticate, "native", "native1");
49+
assertRoles(authenticate, ROLE_NAME);
50+
}
51+
52+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.authc;
9+
10+
import org.elasticsearch.client.RequestOptions;
11+
import org.elasticsearch.common.settings.Settings;
12+
import org.elasticsearch.common.util.concurrent.ThreadContext;
13+
14+
import java.io.IOException;
15+
import java.util.Map;
16+
17+
/**
18+
* Integration Rest Test for testing authentication when all possible realms are configured
19+
*/
20+
public class PkiRealmAuthIT extends SecurityRealmSmokeTestCase {
21+
22+
// Derived from certificate attributes (pki-auth.crt)
23+
private static final String USERNAME = "pki-auth";
24+
25+
@Override
26+
protected Settings restClientSettings() {
27+
Settings.Builder builder = Settings.builder()
28+
.put(super.restClientSettings())
29+
.put(CLIENT_CERT_PATH, getDataPath("/ssl/pki-auth.crt"))
30+
.put(CLIENT_KEY_PATH, getDataPath("/ssl/pki-auth.key"))
31+
.put(CLIENT_KEY_PASSWORD, "http-password");
32+
builder.remove(ThreadContext.PREFIX + ".Authorization");
33+
return builder.build();
34+
}
35+
36+
public void testAuthenticationUsingFileRealm() throws IOException {
37+
Map<String, Object> authenticate = super.authenticate(RequestOptions.DEFAULT.toBuilder());
38+
39+
assertUsername(authenticate, USERNAME);
40+
assertRealm(authenticate, "pki", "pki4");
41+
assertRoles(authenticate, new String[0]);
42+
}
43+
44+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.authc;
9+
10+
import org.elasticsearch.client.Request;
11+
import org.elasticsearch.client.Response;
12+
import org.elasticsearch.test.rest.yaml.ObjectPath;
13+
import org.hamcrest.Matchers;
14+
15+
import java.io.IOException;
16+
import java.util.Map;
17+
18+
/**
19+
* Integration Rest Test for testing authentication when all possible realms are configured
20+
*/
21+
public class RealmInfoIT extends SecurityRealmSmokeTestCase {
22+
23+
public void testThatAllRealmTypesAreEnabled() throws IOException {
24+
final Request request = new Request("GET", "_xpack/usage");
25+
final Response response = client().performRequest(request);
26+
Map<String, Object> usage = entityAsMap(response);
27+
28+
Map<String, Object> realms = ObjectPath.evaluate(usage, "security.realms");
29+
realms.forEach((type, config) -> {
30+
assertThat(config, Matchers.instanceOf(Map.class));
31+
assertThat("Realm type [" + type + "] is not enabled",
32+
((Map<?, ?>) config).get("enabled"), Matchers.equalTo(true));
33+
});
34+
}
35+
36+
}

0 commit comments

Comments
 (0)