Marvel: Add integration test for Marvel+Shield with SSL

closes elastic/elasticsearch#1467

Original commit: elastic/x-pack-elasticsearch@9dd6bf9629
This commit is contained in:
Tanguy Leroux 2016-02-08 14:33:27 +01:00
parent a8e52eb520
commit 66e49a0546
4 changed files with 288 additions and 33 deletions

View File

@ -7,22 +7,28 @@ dependencies {
testCompile project(path: ':x-plugins:elasticsearch:x-pack', configuration: 'runtime') testCompile project(path: ':x-plugins:elasticsearch:x-pack', configuration: 'runtime')
} }
// location of keystore and files to generate it // needed to be consistent with ssl host checking
File keystore = new File(project.buildDir, 'keystore/test-node.jks') String san = getSubjectAlternativeNameString()
// generate the keystore // location of generated keystores and certificates
task createKey(type: LoggedExec) { File keystoreDir = new File(project.buildDir, 'keystore')
// Generate the node's keystore
File nodeKeystore = new File(keystoreDir, 'test-node.jks')
task createNodeKeyStore(type: LoggedExec) {
doFirst { doFirst {
project.delete(keystore.parentFile) if (nodeKeystore.parentFile.exists() == false) {
keystore.parentFile.mkdirs() nodeKeystore.parentFile.mkdirs()
}
if (nodeKeystore.exists()) {
delete nodeKeystore
}
} }
// needed to be consistent with ssl host checking
String san = getSubjectAlternativeNameString()
executable = 'keytool' executable = 'keytool'
standardInput = new ByteArrayInputStream('FirstName LastName\nUnit\nOrganization\nCity\nState\nNL\nyes\n\n'.getBytes('UTF-8')) standardInput = new ByteArrayInputStream('FirstName LastName\nUnit\nOrganization\nCity\nState\nNL\nyes\n\n'.getBytes('UTF-8'))
args '-genkey', args '-genkey',
'-alias', 'test-node', '-alias', 'test-node',
'-keystore', keystore, '-keystore', nodeKeystore,
'-keyalg', 'RSA', '-keyalg', 'RSA',
'-keysize', '2048', '-keysize', '2048',
'-validity', '712', '-validity', '712',
@ -32,9 +38,103 @@ task createKey(type: LoggedExec) {
'-ext', san '-ext', san
} }
// add keystore to test classpath: it expects it there // Generate the client's keystore
sourceSets.test.resources.srcDir(keystore.parentFile) File clientKeyStore = new File(keystoreDir, 'test-client.jks')
processTestResources.dependsOn(createKey) task createClientKeyStore(type: LoggedExec) {
doFirst {
if (clientKeyStore.parentFile.exists() == false) {
clientKeyStore.parentFile.mkdirs()
}
if (clientKeyStore.exists()) {
delete clientKeyStore
}
}
executable = 'keytool'
standardInput = new ByteArrayInputStream('FirstName LastName\nUnit\nOrganization\nCity\nState\nNL\nyes\n\n'.getBytes('UTF-8'))
args '-genkey',
'-alias', 'test-client',
'-keystore', clientKeyStore,
'-keyalg', 'RSA',
'-keysize', '2048',
'-validity', '712',
'-dname', 'CN=smoke-test-plugins-ssl',
'-keypass', 'keypass',
'-storepass', 'keypass',
'-ext', san
}
// Export the node's certificate
File nodeCertificate = new File(keystoreDir, 'test-node.cert')
task exportNodeCertificate(type: LoggedExec) {
doFirst {
if (nodeCertificate.parentFile.exists() == false) {
nodeCertificate.parentFile.mkdirs()
}
if (nodeCertificate.exists()) {
delete nodeCertificate
}
}
executable = 'keytool'
args '-export',
'-alias', 'test-node',
'-keystore', nodeKeystore,
'-storepass', 'keypass',
'-file', nodeCertificate
}
// Import the node certificate in the client's keystore
task importNodeCertificateInClientKeyStore(type: LoggedExec) {
dependsOn exportNodeCertificate
executable = 'keytool'
args '-import',
'-alias', 'test-node',
'-keystore', clientKeyStore,
'-storepass', 'keypass',
'-file', nodeCertificate,
'-noprompt'
}
// Export the client's certificate
File clientCertificate = new File(keystoreDir, 'test-client.cert')
task exportClientCertificate(type: LoggedExec) {
doFirst {
if (clientCertificate.parentFile.exists() == false) {
clientCertificate.parentFile.mkdirs()
}
if (clientCertificate.exists()) {
delete clientCertificate
}
}
executable = 'keytool'
args '-export',
'-alias', 'test-client',
'-keystore', clientKeyStore,
'-storepass', 'keypass',
'-file', clientCertificate
}
// Import the client certificate in the node's keystore
task importClientCertificateInNodeKeyStore(type: LoggedExec) {
dependsOn exportClientCertificate
executable = 'keytool'
args '-import',
'-alias', 'test-client',
'-keystore', nodeKeystore,
'-storepass', 'keypass',
'-file', clientCertificate,
'-noprompt'
}
forbiddenPatterns {
exclude '**/*.cert'
}
// Add keystores to test classpath: it expects it there
sourceSets.test.resources.srcDir(keystoreDir)
processTestResources.dependsOn(
createNodeKeyStore, createClientKeyStore,
importNodeCertificateInClientKeyStore, importClientCertificateInNodeKeyStore
)
ext.pluginsCount = 1 // we install xpack explicitly ext.pluginsCount = 1 // we install xpack explicitly
project.rootProject.subprojects.findAll { it.path.startsWith(':plugins:') }.each { subproj -> project.rootProject.subprojects.findAll { it.path.startsWith(':plugins:') }.each { subproj ->
@ -45,25 +145,46 @@ project.rootProject.subprojects.findAll { it.path.startsWith(':plugins:') }.each
integTest { integTest {
cluster { cluster {
systemProperty 'es.xpack.monitoring.agent.exporters.es.type', 'http' systemProperty 'es.xpack.monitoring.agent.interval', '3s'
systemProperty 'es.xpack.monitoring.agent.exporters.es.enabled', 'false' systemProperty 'es.xpack.monitoring.agent.exporters._http.type', 'http'
systemProperty 'es.xpack.monitoring.agent.exporters.es.ssl.truststore.path', keystore.name systemProperty 'es.xpack.monitoring.agent.exporters._http.enabled', 'false'
systemProperty 'es.xpack.monitoring.agent.exporters.es.ssl.truststore.password', 'keypass' systemProperty 'es.xpack.monitoring.agent.exporters._http.ssl.truststore.path', clientKeyStore.name
systemProperty 'es.xpack.monitoring.agent.exporters._http.ssl.truststore.password', 'keypass'
systemProperty 'es.xpack.monitoring.agent.exporters._http.auth.username', 'monitoring_agent'
systemProperty 'es.xpack.monitoring.agent.exporters._http.auth.password', 'changeme'
systemProperty 'es.shield.transport.ssl', 'true' systemProperty 'es.shield.transport.ssl', 'true'
systemProperty 'es.shield.http.ssl', 'true' systemProperty 'es.shield.http.ssl', 'true'
systemProperty 'es.shield.ssl.keystore.path', keystore.name systemProperty 'es.shield.ssl.keystore.path', nodeKeystore.name
systemProperty 'es.shield.ssl.keystore.password', 'keypass' systemProperty 'es.shield.ssl.keystore.password', 'keypass'
plugin 'x-pack', project(':x-plugins:elasticsearch:x-pack') plugin 'x-pack', project(':x-plugins:elasticsearch:x-pack')
// copy keystore into config/ // copy keystores into config/
extraConfigFile keystore.name, keystore extraConfigFile nodeKeystore.name, nodeKeystore
extraConfigFile clientKeyStore.name, clientKeyStore
setupCommand 'setupTestUser', setupCommand 'setupTestUser',
'bin/xpack/esusers', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin' 'bin/xpack/esusers', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin'
setupCommand 'setupMarvelUser', setupCommand 'setupMarvelUser',
'bin/xpack/esusers', 'useradd', 'marvel_export', '-p', 'changeme', '-r', 'marvel_agent' 'bin/xpack/esusers', 'useradd', 'monitoring_agent', '-p', 'changeme', '-r', 'remote_monitoring_agent'
waitCondition = { node, ant -> waitCondition = { node, ant ->
// we just return true, doing an https check is tricky here // HTTPS check is tricky to do, so we wait for the log file to indicate that the node is started
return true String waitForNodeStartProp = "waitForNodeStart${name}"
ant.waitfor(maxwait: '10', maxwaitunit: 'second', checkevery: '100', checkeveryunit: 'millisecond',
timeoutproperty: waitForNodeStartProp) {
and {
resourcecontains(resource: "${node.startLog.toString()}", substring: "bound_addresses {${node.httpUri()}")
resourcecontains(resource: "${node.startLog.toString()}", substring: 'started')
}
}
if (ant.project.getProperty(waitForNodeStartProp)) {
println "Timed out when looking for bound_addresses in log file ${node.startLog.toString()}"
return false;
}
return true;
} }
} }
} }

View File

@ -0,0 +1,134 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.smoketest;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesResponse;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.shield.transport.netty.ShieldNettyTransport;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.xpack.XPackPlugin;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
/**
* This test checks that a Monitoring's HTTP exporter correctly exports to a monitoring cluster
* protected by Shield with HTTPS/SSL.
*
* It sets up a cluster with Monitoring and Shield configured with SSL. Once started,
* an HTTP exporter is activated and it exports data locally over HTTPS/SSL. The test
* then uses a transport client to check that the data have been correctly received and
* indexed in the cluster.
*/
public class SmokeTestMonitoringWithShieldIT extends ESIntegTestCase {
private static final String USER = "test_user";
private static final String PASS = "changeme";
private static final String KEYSTORE_PASS = "keypass";
private static final String MONITORING_PATTERN = ".monitoring-*";
@Override
protected Collection<Class<? extends Plugin>> transportClientPlugins() {
return Collections.singletonList(XPackPlugin.class);
}
@Override
protected Settings externalClusterClientSettings() {
return Settings.builder()
.put("shield.user", USER + ":" + PASS)
.put(ShieldNettyTransport.TRANSPORT_SSL_SETTING, true)
.put("shield.ssl.keystore.path", clientKeyStore)
.put("shield.ssl.keystore.password", KEYSTORE_PASS)
.build();
}
@Before
public void enableExporter() throws Exception {
InetSocketAddress httpAddress = randomFrom(httpAddresses());
URI uri = new URI("https", null, httpAddress.getHostString(), httpAddress.getPort(), "/", null, null);
Settings exporterSettings = Settings.builder()
.put("xpack.monitoring.agent.exporters._http.enabled", true)
.put("xpack.monitoring.agent.exporters._http.host", uri.toString())
.build();
assertAcked(client().admin().cluster().prepareUpdateSettings().setTransientSettings(exporterSettings));
}
@After
public void disableExporter() {
Settings exporterSettings = Settings.builder()
.putNull("xpack.monitoring.agent.exporters._http.enabled")
.putNull("xpack.monitoring.agent.exporters._http.host")
.build();
assertAcked(client().admin().cluster().prepareUpdateSettings().setTransientSettings(exporterSettings));
}
public void testHTTPExporterWithSSL() throws Exception {
// Checks that the monitoring index templates have been installed
assertBusy(() -> {
GetIndexTemplatesResponse response = client().admin().indices().prepareGetTemplates(MONITORING_PATTERN).get();
assertThat(response.getIndexTemplates().size(), equalTo(2));
});
// Checks that the HTTP exporter has successfully exported some data
assertBusy(() -> {
try {
assertThat(client().prepareSearch(MONITORING_PATTERN).setSize(0).get().getHits().getTotalHits(), greaterThan(0L));
} catch (Exception e) {
fail("exception when checking for monitoring documents: " + e.getMessage());
}
});
}
private InetSocketAddress[] httpAddresses() {
NodeInfo[] nodes = client().admin().cluster().prepareNodesInfo().clear().setHttp(true).get().getNodes();
assertThat(nodes.length, greaterThan(0));
InetSocketAddress[] httpAddresses = new InetSocketAddress[nodes.length];
for (int i = 0; i < nodes.length; i++) {
httpAddresses[i] = ((InetSocketTransportAddress) nodes[i].getHttp().address().publishAddress()).address();
}
return httpAddresses;
}
static Path clientKeyStore;
@BeforeClass
public static void loadKeyStore() {
try {
clientKeyStore = PathUtils.get(SmokeTestMonitoringWithShieldIT.class.getResource("/test-client.jks").toURI());
} catch (URISyntaxException e) {
throw new ElasticsearchException("exception while reading the store", e);
}
if (!Files.exists(clientKeyStore)) {
throw new IllegalStateException("Keystore file [" + clientKeyStore + "] does not exist.");
}
}
@AfterClass
public static void clearClientKeyStore() {
clientKeyStore = null;
}
}

View File

@ -55,7 +55,7 @@ logstash:
# Monitoring user role. Assign to monitoring users. # Monitoring user role. Assign to monitoring users.
monitoring_user: monitoring_user:
indices: indices:
'.monitoring-es-*': '.monitoring-*':
privileges: read privileges: read
'.kibana': '.kibana':
privileges: indices:admin/exists, indices:admin/mappings/fields/get, indices:admin/validate/query, indices:data/read/get, indices:data/read/mget, indices:data/read/search privileges: indices:admin/exists, indices:admin/mappings/fields/get, indices:admin/validate/query, indices:data/read/get, indices:data/read/mget, indices:data/read/search
@ -65,7 +65,7 @@ monitoring_user:
remote_monitoring_agent: remote_monitoring_agent:
cluster: indices:admin/template/put, indices:admin/template/get cluster: indices:admin/template/put, indices:admin/template/get
indices: indices:
'.monitoring-es-*': '.monitoring-*':
privileges: all privileges: all
# Allows all operations required to manage ingest pipelines # Allows all operations required to manage ingest pipelines

View File

@ -54,7 +54,7 @@ public class ShieldTemplateService extends AbstractComponent implements ClusterS
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.copy(is, out); Streams.copy(is, out);
final byte[] template = out.toByteArray(); final byte[] template = out.toByteArray();
logger.info("--> putting the shield index template"); logger.debug("putting the shield index template");
PutIndexTemplateRequest putTemplateRequest = client.admin().indices() PutIndexTemplateRequest putTemplateRequest = client.admin().indices()
.preparePutTemplate(SECURITY_TEMPLATE_NAME).setSource(template).request(); .preparePutTemplate(SECURITY_TEMPLATE_NAME).setSource(template).request();
PutIndexTemplateResponse templateResponse = client.admin().indices().putTemplate(putTemplateRequest).get(); PutIndexTemplateResponse templateResponse = client.admin().indices().putTemplate(putTemplateRequest).get();