diff --git a/data-node/src/main/java/org/graylog/datanode/Configuration.java b/data-node/src/main/java/org/graylog/datanode/Configuration.java index 31a14601e0..ebf262e618 100644 --- a/data-node/src/main/java/org/graylog/datanode/Configuration.java +++ b/data-node/src/main/java/org/graylog/datanode/Configuration.java @@ -334,7 +334,10 @@ public class Configuration extends BaseConfiguration { public static final String PATH_API = "api/"; @Parameter(value = "http_bind_address", required = true) - private HostAndPort httpBindAddress = HostAndPort.fromParts("127.0.0.1", GRAYLOG_DEFAULT_PORT); + private HostAndPort httpBindAddress = HostAndPort.fromParts("0.0.0.0", GRAYLOG_DEFAULT_PORT); + + @Parameter(value = "hostname", required = true) + private String hostname = Tools.getLocalCanonicalHostname(); @Parameter(value = "http_publish_uri", validator = URIAbsoluteValidator.class) private URI httpPublishUri; @@ -550,4 +553,8 @@ public class Configuration extends BaseConfiguration { private boolean isRegularFileAndReadable(Path path) { return path != null && Files.isRegularFile(path) && Files.isReadable(path); } + + public String getHostname() { + return hostname; + } } diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java index ea250a6deb..22ebdff96b 100644 --- a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodePreflightGeneratePeriodical.java @@ -17,7 +17,6 @@ package org.graylog.datanode.bootstrap.preflight; import org.bouncycastle.operator.OperatorException; -import org.graylog.datanode.Configuration; import org.graylog.security.certutil.CertConstants; import org.graylog.security.certutil.cert.CertificateChain; import org.graylog.security.certutil.cert.storage.CertChainMongoStorage; @@ -58,7 +57,6 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { private final CsrGenerator csrGenerator; private final CertChainStorage certMongoStorage; private final CertificateAndPrivateKeyMerger certificateAndPrivateKeyMerger; - private final Configuration configuration; private final SmartKeystoreStorage keystoreStorage; private final char[] passwordSecret; @@ -70,7 +68,6 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { final CsrGenerator csrGenerator, final CertChainMongoStorage certMongoStorage, final CertificateAndPrivateKeyMerger certificateAndPrivateKeyMerger, - final Configuration configuration, final SmartKeystoreStorage keystoreStorage, final @Named("password_secret") String passwordSecret) { this.nodePreflightConfigService = nodePreflightConfigService; @@ -80,7 +77,6 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { this.csrGenerator = csrGenerator; this.certMongoStorage = certMongoStorage; this.certificateAndPrivateKeyMerger = certificateAndPrivateKeyMerger; - this.configuration = configuration; this.keystoreStorage = keystoreStorage; // TODO: merge with real storage this.privateKeyEncryptedStorage = new PrivateKeyEncryptedFileStorage("privateKeyFilename.cert"); @@ -130,12 +126,6 @@ public class DataNodePreflightGeneratePeriodical extends Periodical { LOG.error("Config entry in signed state, but wrong certificate data present in Mongo"); } } - - // write certificate to local truststore - // configure SSL - // start DataNode - // set state to State.CONNECTED - // nodePreflightConfigService.save(cfg.toBuilder().state(NodePreflightConfig.State.CONNECTED).build()); } } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/OpensearchConfigurationProvider.java b/data-node/src/main/java/org/graylog/datanode/configuration/OpensearchConfigurationProvider.java index 75421f5d7c..aa0a2858eb 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/OpensearchConfigurationProvider.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/OpensearchConfigurationProvider.java @@ -23,6 +23,7 @@ import org.graylog.datanode.configuration.variants.SecurityConfigurationVariant; import org.graylog.datanode.configuration.variants.UploadedCertFilesSecureConfiguration; import org.graylog.datanode.process.OpensearchConfiguration; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; +import org.graylog2.plugin.Tools; import javax.inject.Inject; import javax.inject.Provider; @@ -79,6 +80,8 @@ public class OpensearchConfigurationProvider implements Provider new URIBuilder() + .setHost(httpHost.getHostName()) + .setPort(httpHost.getPort()) + .setScheme(httpHost.getSchemeName()) + .setUserInfo(configuration.get().authUsername(), configuration.get().authPassword()).toString()) .orElse(""); // TODO: will this cause problems in the nodeservice? return URI.create(baseUrl); } diff --git a/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java b/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java index 06de3d8f6b..50590795f9 100644 --- a/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java @@ -28,6 +28,8 @@ import java.util.Map; public record OpensearchConfiguration( Path opensearchDir, Path opensearchConfigDir, + String bindAddress, + String hostname, int httpPort, int transportPort, String authUsername, @@ -45,6 +47,9 @@ public record OpensearchConfiguration( config.put("bootstrap.system_call_filter", "false"); } + if (bindAddress != null && !bindAddress.isBlank()) { + config.put("network.host", bindAddress); + } config.put("http.port", String.valueOf(httpPort)); config.put("transport.port", String.valueOf(transportPort)); if (clusterName != null && !clusterName.isBlank()) { @@ -75,6 +80,6 @@ public record OpensearchConfiguration( public HttpHost getRestBaseUrl() { final boolean sslEnabled = Boolean.parseBoolean(asMap().getOrDefault("plugins.security.ssl.http.enabled", "false")); - return new HttpHost("localhost", httpPort(), sslEnabled ? "https" : "http"); + return new HttpHost(hostname(), httpPort(), sslEnabled ? "https" : "http"); } } diff --git a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java index c3c57d68a1..2948997569 100644 --- a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java +++ b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java @@ -29,10 +29,13 @@ import org.graylog.security.certutil.CertutilCa; import org.graylog.security.certutil.CertutilCert; import org.graylog.security.certutil.CertutilHttp; import org.graylog.security.certutil.console.TestableConsole; +import org.graylog2.plugin.Tools; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.FileInputStream; import java.io.IOException; @@ -52,6 +55,8 @@ import static org.graylog.datanode.testinfra.DatanodeContainerizedBackend.IMAGE_ public class DatanodeSecuritySetupIT { + private static final Logger LOG = LoggerFactory.getLogger(DatanodeSecuritySetupIT.class); + public static final String HOSTNAME = "graylog-datanode-host"; @TempDir static Path tempDir; @@ -84,6 +89,15 @@ public class DatanodeSecuritySetupIT { // configure initial admin username and password for Opensearch REST datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_USERNAME", "admin"); datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_PASSWORD", "admin"); + + // this is the interface that we bind opensearch to. It must be 0.0.0.0 if we want + // to be able to reach opensearch from outside the container and docker network (true?) + datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_BIND_ADDRESS", "0.0.0.0"); + + // HOSTNAME is used to generate the SSL certificates and to communicate inside the + // container and docker network, where we do the hostname validation. + datanodeContainer.withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withName(HOSTNAME)); + datanodeContainer.withEnv("GRAYLOG_DATANODE_HOSTNAME", HOSTNAME); }).start(); } @@ -92,14 +106,18 @@ public class DatanodeSecuritySetupIT { waitForOpensearchAvailableStatus(backend.getDatanodeRestPort()); - given() + try { + given() .auth().basic("admin", "admin") .trustStore(buildTruststore(httpCert, "password")) .get("https://localhost:" + backend.getOpensearchRestPort()) .then().assertThat() .body("name", Matchers.equalTo("node1")) .body("cluster_name", Matchers.equalTo("datanode-cluster")); - + } catch (Exception ex) { + LOG.error("Error connecting to OpenSearch in the DataNode, showing logs:\n{}", backend.getLogs()); + throw ex; + } } private void waitForOpensearchAvailableStatus(Integer datanodeRestPort) throws ExecutionException, RetryException { @@ -111,9 +129,17 @@ public class DatanodeSecuritySetupIT { .retryIfResult(input -> !input.extract().body().path("opensearch.node.state").equals("AVAILABLE")) .build(); - retryer.call(() -> RestAssured.given() - .get("http://localhost:" + datanodeRestPort) - .then()); + try { + var hostname = Tools.getLocalCanonicalHostname(); + var url = "http://" + hostname + ":" + datanodeRestPort; + LOG.info("Trying to connect to: {}", url); + retryer.call(() -> RestAssured.given() + .get(url) + .then()); + } catch (Exception ex) { + LOG.error("Error starting the DataNode, showing logs:\n" + backend.getLogs()); + throw ex; + } } /** @@ -167,7 +193,7 @@ public class DatanodeSecuritySetupIT { .register("Do you want to use your own certificate authority? Respond with y/n?", "n") .register("Enter CA password", "password") .register("Enter certificate validity in days", "90") - .register("Enter alternative names (addresses) of this node [comma separated]", "example.com") + .register("Enter alternative names (addresses) of this node [comma separated]", HOSTNAME) .register("Enter HTTP certificate password", "password"); CertutilHttp certutilCert = new CertutilHttp( caPath.toAbsolutePath().toString(), diff --git a/graylog-storage-opensearch2/src/main/java/org/graylog/storage/opensearch2/RestHighLevelClientProvider.java b/graylog-storage-opensearch2/src/main/java/org/graylog/storage/opensearch2/RestHighLevelClientProvider.java index 1e87151a25..ff68a99912 100644 --- a/graylog-storage-opensearch2/src/main/java/org/graylog/storage/opensearch2/RestHighLevelClientProvider.java +++ b/graylog-storage-opensearch2/src/main/java/org/graylog/storage/opensearch2/RestHighLevelClientProvider.java @@ -20,15 +20,15 @@ import com.github.joschi.jadconfig.util.Duration; import com.google.common.base.Suppliers; import org.graylog.shaded.opensearch2.org.apache.http.HttpHost; import org.graylog.shaded.opensearch2.org.apache.http.client.CredentialsProvider; -import org.graylog.shaded.opensearch2.org.opensearch.client.Node; import org.graylog.shaded.opensearch2.org.opensearch.client.RestClient; import org.graylog.shaded.opensearch2.org.opensearch.client.RestClientBuilder; import org.graylog.shaded.opensearch2.org.opensearch.client.RestHighLevelClient; -import org.graylog2.configuration.IndexerHosts; -import org.graylog2.system.shutdown.GracefulShutdownService; -import org.graylog.shaded.opensearch2.org.opensearch.client.sniff.NodesSniffer; import org.graylog.shaded.opensearch2.org.opensearch.client.sniff.OpenSearchNodesSniffer; -import org.graylog.shaded.opensearch2.org.opensearch.client.sniff.Sniffer; +import org.graylog2.configuration.IndexerHosts; +import org.graylog2.security.TrustManagerAndSocketFactoryProvider; +import org.graylog2.system.shutdown.GracefulShutdownService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import javax.inject.Inject; @@ -40,11 +40,12 @@ import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import java.util.stream.Collectors; @Singleton public class RestHighLevelClientProvider implements Provider { + private static final Logger LOG = LoggerFactory.getLogger(RestHighLevelClientProvider.class); private final Supplier clientSupplier; + private final TrustManagerAndSocketFactoryProvider trustManagerAndSocketFactoryProvider; @SuppressWarnings("unused") @Inject @@ -64,7 +65,10 @@ public class RestHighLevelClientProvider implements Provider { final RestHighLevelClient client = buildClient(hosts, @@ -119,7 +123,6 @@ public class RestHighLevelClientProvider implements Provider new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme())).toArray(HttpHost[]::new); - final RestClientBuilder restClientBuilder = RestClient.builder(esHosts) .setRequestConfigCallback(requestConfig -> requestConfig .setConnectTimeout(Math.toIntExact(connectTimeout.toMilliseconds())) @@ -137,6 +140,10 @@ public class RestHighLevelClientProvider implements Provider host.getScheme().equalsIgnoreCase("https"))) { + httpClientConfig.setSSLContext(trustManagerAndSocketFactoryProvider.getSslContext()); + } + return httpClientConfig; }); diff --git a/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/DatanodeInstance.java b/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/DatanodeInstance.java index 356be42d88..80abb58e2a 100644 --- a/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/DatanodeInstance.java +++ b/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/DatanodeInstance.java @@ -90,7 +90,8 @@ public class DatanodeInstance extends TestableSearchServerInstance { "http", false, false, - new BasicCredentialsProvider()) + new BasicCredentialsProvider(), + null) .get(); } diff --git a/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/OpenSearchInstance.java b/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/OpenSearchInstance.java index bdbf080894..054dcfba05 100644 --- a/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/OpenSearchInstance.java +++ b/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/OpenSearchInstance.java @@ -88,7 +88,8 @@ public class OpenSearchInstance extends TestableSearchServerInstance { "http", false, false, - new BasicCredentialsProvider()) + new BasicCredentialsProvider(), + null) .get(); } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CaService.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CaService.java index 880069aa20..4d10b75e65 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CaService.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CaService.java @@ -16,123 +16,23 @@ */ package org.graylog.security.certutil; -import org.glassfish.jersey.media.multipart.BodyPart; import org.glassfish.jersey.media.multipart.FormDataBodyPart; -import org.graylog.security.certutil.ca.CACreator; -import org.graylog.security.certutil.ca.PemCaReader; import org.graylog.security.certutil.ca.exceptions.CACreationException; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; -import org.graylog.security.certutil.keystore.storage.SmartKeystoreStorage; -import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; -import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoCollections; -import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoLocation; -import org.graylog2.Configuration; import org.graylog2.bootstrap.preflight.web.resources.model.CA; -import org.graylog2.bootstrap.preflight.web.resources.model.CAType; -import org.graylog2.plugin.system.NodeId; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.time.Duration; import java.util.List; import java.util.Optional; -import static org.graylog.security.certutil.CertConstants.PKCS12; +public interface CaService { + int DEFAULT_VALIDITY = 10 * 365; - -@Singleton -public class CaService { - private static final Logger LOG = LoggerFactory.getLogger(CaService.class); - public static int DEFAULT_VALIDITY = 10 * 365; - private final SmartKeystoreStorage keystoreStorage; - private final KeystoreMongoLocation mongoDbCaLocation; - private final KeystoreFileLocation manuallyProvidedCALocation; - private final NodeId nodeId; - private final CACreator caCreator; - private final PemCaReader pemCaReader; - private final CaConfiguration configuration; - private final String passwordSecret; - - @Inject - public CaService(final Configuration configuration, - final SmartKeystoreStorage keystoreStorage, - final NodeId nodeId, - final CACreator caCreator, - final PemCaReader pemCaReader, - final @Named("password_secret") String passwordSecret) { - this.keystoreStorage = keystoreStorage; - this.nodeId = nodeId; - this.caCreator = caCreator; - this.pemCaReader = pemCaReader; - this.configuration = configuration; - this.passwordSecret = configuration.getCaPassword() != null ? configuration.getCaPassword() : passwordSecret; - this.mongoDbCaLocation = new KeystoreMongoLocation(nodeId.getNodeId(), KeystoreMongoCollections.GRAYLOG_CA_KEYSTORE_COLLECTION); - this.manuallyProvidedCALocation = new KeystoreFileLocation(configuration.getCaKeystoreFile()); - } - - public CA get() throws KeyStoreStorageException { - if(configuration.configuredCaExists()) { - return new CA("local CA", CAType.LOCAL); - } else { - var keystore = keystoreStorage.readKeyStore(mongoDbCaLocation, passwordSecret.toCharArray()); - return keystore.map(c -> new CA(nodeId.getNodeId(), CAType.GENERATED)).orElse(null); - } - } - - public void create(final Integer daysValid, char[] password) throws CACreationException, KeyStoreStorageException { - final Duration certificateValidity = Duration.ofDays(daysValid == null || daysValid == 0 ? DEFAULT_VALIDITY: daysValid); - KeyStore keyStore = caCreator.createCA(passwordSecret.toCharArray(), certificateValidity); - keystoreStorage.writeKeyStore(mongoDbCaLocation, keyStore, passwordSecret.toCharArray(), password); - LOG.debug("Generated a new CA."); - } - - public void upload(String pass, List parts) throws CACreationException { - final var password = pass == null ? null : pass.toCharArray(); - // TODO: if the upload consists of more than one file, handle accordingly - // or: decide that it's always only one file containing all certificates - try { - KeyStore keyStore = KeyStore.getInstance(PKCS12); - for(BodyPart part : parts) { - InputStream is = part.getEntityAs(InputStream.class); - byte[] bytes = is.readAllBytes(); - String pem = new String(bytes, StandardCharsets.UTF_8); - // Test, if upload is PEM file, must contain at least a certificate - if (pem.contains("-----BEGIN CERTIFICATE")) { - pemCaReader.readCA(keyStore, password, pem); - } else { - ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - keyStore.load(bais, password); - } - } - keystoreStorage.writeKeyStore(mongoDbCaLocation, keyStore, password, passwordSecret.toCharArray()); - } catch (IOException | KeyStoreStorageException | NoSuchAlgorithmException | CertificateException | - KeyStoreException ex) { - LOG.error("Could not write CA: " + ex.getMessage(), ex); - throw new CACreationException("Could not write CA: " + ex.getMessage(), ex); - } - } - - public void startOver() { - - } - - public Optional loadKeyStore() throws KeyStoreException, KeyStoreStorageException, NoSuchAlgorithmException { - if(configuration.configuredCaExists()) { - return keystoreStorage.readKeyStore(manuallyProvidedCALocation, configuration.getCaPassword().toCharArray()); - } else { - return keystoreStorage.readKeyStore(mongoDbCaLocation, passwordSecret.toCharArray()); - } - } + CA get() throws KeyStoreStorageException; + void create(final Integer daysValid, char[] password) throws CACreationException, KeyStoreStorageException; + void upload(String pass, List parts) throws CACreationException; + void startOver(); + Optional loadKeyStore() throws KeyStoreException, KeyStoreStorageException, NoSuchAlgorithmException; } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CaServiceImpl.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CaServiceImpl.java new file mode 100644 index 0000000000..1770feff74 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CaServiceImpl.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.security.certutil; + +import org.glassfish.jersey.media.multipart.BodyPart; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.graylog.security.certutil.ca.CACreator; +import org.graylog.security.certutil.ca.PemCaReader; +import org.graylog.security.certutil.ca.exceptions.CACreationException; +import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; +import org.graylog.security.certutil.keystore.storage.SmartKeystoreStorage; +import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; +import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoCollections; +import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoLocation; +import org.graylog2.Configuration; +import org.graylog2.bootstrap.preflight.web.resources.model.CA; +import org.graylog2.bootstrap.preflight.web.resources.model.CAType; +import org.graylog2.plugin.system.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import static org.graylog.security.certutil.CertConstants.PKCS12; + +@Singleton +public class CaServiceImpl implements CaService { + private static final Logger LOG = LoggerFactory.getLogger(CaServiceImpl.class); + private final SmartKeystoreStorage keystoreStorage; + private final KeystoreMongoLocation mongoDbCaLocation; + private final KeystoreFileLocation manuallyProvidedCALocation; + private final NodeId nodeId; + private final CACreator caCreator; + private final PemCaReader pemCaReader; + private final CaConfiguration configuration; + private final String passwordSecret; + + @Inject + public CaServiceImpl(final Configuration configuration, + final SmartKeystoreStorage keystoreStorage, + final NodeId nodeId, + final CACreator caCreator, + final PemCaReader pemCaReader, + final @Named("password_secret") String passwordSecret) { + this.keystoreStorage = keystoreStorage; + this.nodeId = nodeId; + this.caCreator = caCreator; + this.pemCaReader = pemCaReader; + this.configuration = configuration; + this.passwordSecret = configuration.getCaPassword() != null ? configuration.getCaPassword() : passwordSecret; + this.mongoDbCaLocation = new KeystoreMongoLocation(nodeId.getNodeId(), KeystoreMongoCollections.GRAYLOG_CA_KEYSTORE_COLLECTION); + this.manuallyProvidedCALocation = new KeystoreFileLocation(configuration.getCaKeystoreFile()); + } + + @Override + public CA get() throws KeyStoreStorageException { + if(configuration.configuredCaExists()) { + return new CA("local CA", CAType.LOCAL); + } else { + var keystore = keystoreStorage.readKeyStore(mongoDbCaLocation, passwordSecret.toCharArray()); + return keystore.map(c -> new CA(nodeId.getNodeId(), CAType.GENERATED)).orElse(null); + } + } + + @Override + public void create(final Integer daysValid, char[] password) throws CACreationException, KeyStoreStorageException { + final Duration certificateValidity = Duration.ofDays(daysValid == null || daysValid == 0 ? DEFAULT_VALIDITY: daysValid); + KeyStore keyStore = caCreator.createCA(passwordSecret.toCharArray(), certificateValidity); + keystoreStorage.writeKeyStore(mongoDbCaLocation, keyStore, passwordSecret.toCharArray(), password); + LOG.debug("Generated a new CA."); + } + + @Override + public void upload(String pass, List parts) throws CACreationException { + final var password = pass == null ? null : pass.toCharArray(); + // TODO: if the upload consists of more than one file, handle accordingly + // or: decide that it's always only one file containing all certificates + try { + KeyStore keyStore = KeyStore.getInstance(PKCS12); + for(BodyPart part : parts) { + InputStream is = part.getEntityAs(InputStream.class); + byte[] bytes = is.readAllBytes(); + String pem = new String(bytes, StandardCharsets.UTF_8); + // Test, if upload is PEM file, must contain at least a certificate + if (pem.contains("-----BEGIN CERTIFICATE")) { + pemCaReader.readCA(keyStore, password, pem); + } else { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + keyStore.load(bais, password); + } + } + keystoreStorage.writeKeyStore(mongoDbCaLocation, keyStore, password, passwordSecret.toCharArray()); + } catch (IOException | KeyStoreStorageException | NoSuchAlgorithmException | CertificateException | + KeyStoreException ex) { + LOG.error("Could not write CA: " + ex.getMessage(), ex); + throw new CACreationException("Could not write CA: " + ex.getMessage(), ex); + } + } + + @Override + public void startOver() { + + } + + @Override + public Optional loadKeyStore() throws KeyStoreException, KeyStoreStorageException, NoSuchAlgorithmException { + if(configuration.configuredCaExists()) { + return keystoreStorage.readKeyStore(manuallyProvidedCALocation, configuration.getCaPassword().toCharArray()); + } else { + return keystoreStorage.readKeyStore(mongoDbCaLocation, passwordSecret.toCharArray()); + } + } +} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCert.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCert.java index acdffb80f9..f20b43a8df 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCert.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCert.java @@ -88,8 +88,9 @@ public class CertutilCert implements CliCommand { console.printLine("Generating private key and certificate for this datanode"); - final CertRequest req = CertRequest.signed("localhost", intermediateCA) + final CertRequest req = CertRequest.signed(Tools.getLocalCanonicalHostname(), intermediateCA) .withSubjectAlternativeName("localhost") + .withSubjectAlternativeName("graylog-datanode-host") .withSubjectAlternativeName(Tools.getLocalHostname()) .withSubjectAlternativeName(String.valueOf(InetAddress.getLocalHost())) .withSubjectAlternativeName("127.0.0.1") diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsrSign.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsrSign.java index 9e8be2b5f7..ff81a08138 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsrSign.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilCsrSign.java @@ -89,7 +89,7 @@ public class CertutilCsrSign implements CliCommand { final var csr = csrStorage.readCsr(); final int validityDays = console.readInt("Enter certificate validity in days: "); - final var cert = CsrSigner.sign(caPrivateKey, caCertificate, csr, validityDays); + final var cert = new CsrSigner().sign(caPrivateKey, caCertificate, csr, validityDays); final Path certPath = Path.of(certFilename); writePem(certPath, cert); diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilHttp.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilHttp.java index 0c0d51b463..054250c54a 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilHttp.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertutilHttp.java @@ -123,11 +123,12 @@ public class CertutilHttp implements CliCommand { final int validityDays = console.readInt("Enter certificate validity in days: "); - final String cnName = "localhost"; + final String cnName = Tools.getLocalCanonicalHostname(); CertRequest certificateRequest = CertRequest.signed(cnName, caKeyPair) - .withSubjectAlternativeName(cnName) + .withSubjectAlternativeName("localhost") .withSubjectAlternativeName(Tools.getLocalHostname()) + .withSubjectAlternativeName("graylog-datanode-host") .withSubjectAlternativeName(String.valueOf(InetAddress.getLocalHost())) .withSubjectAlternativeName("127.0.0.1") .withSubjectAlternativeName("ip6-localhost") diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java index a3d0f9f6ec..79b3a40f67 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java @@ -33,6 +33,7 @@ import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedStorage; import javax.security.auth.x500.X500Principal; import java.security.KeyPairGenerator; +import java.util.ArrayList; import java.util.List; import static org.graylog.security.certutil.CertConstants.KEY_GENERATION_ALGORITHM; @@ -64,23 +65,23 @@ public class CsrGenerator { certKeyPair.getPublic() ); - if (altNames != null && !altNames.isEmpty()) { - Extension subjectAltNames = new Extension(Extension.subjectAlternativeName, false, - new DEROctetString( - new GeneralNames( - altNames.stream() - .map(alternativeName -> new GeneralName( - new X500Name("CN=" + alternativeName)) - ) - .toArray(GeneralName[]::new) - ) - ) - ); - p10Builder.addAttribute( - PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, - new Extensions(subjectAltNames)); + var names = new ArrayList<>(List.of(principalName)); + if(altNames != null) { + names.addAll(altNames); } + Extension subjectAltNames = new Extension(Extension.subjectAlternativeName, false, + new DEROctetString( + new GeneralNames( + names.stream() + .map(alternativeName -> new GeneralName(GeneralName.dNSName, alternativeName)) + .toArray(GeneralName[]::new) + ) + ) + ); + p10Builder.addAttribute( + PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, + new Extensions(subjectAltNames)); JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(SIGNING_ALGORITHM); ContentSigner signer = csBuilder.build(certKeyPair.getPrivate()); diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrSigner.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrSigner.java index 7d936f31f4..e23aaf730b 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrSigner.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrSigner.java @@ -16,7 +16,13 @@ */ package org.graylog.security.certutil.csr; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; @@ -29,12 +35,24 @@ import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import static org.bouncycastle.asn1.x509.GeneralName.dNSName; +import static org.bouncycastle.asn1.x509.GeneralName.iPAddress; +import static org.bouncycastle.asn1.x509.GeneralName.rfc822Name; import static org.graylog.security.certutil.CertConstants.SIGNING_ALGORITHM; public class CsrSigner { - public static X509Certificate sign(PrivateKey caPrivateKey, X509Certificate caCertificate, PKCS10CertificationRequest csr, int validityDays) throws Exception { + private boolean isValidName(final int name) { + return switch (name) { + case dNSName, iPAddress, rfc822Name -> true; + default -> false; + }; + } + + public X509Certificate sign(PrivateKey caPrivateKey, X509Certificate caCertificate, PKCS10CertificationRequest csr, int validityDays) throws Exception { // TODO: cert serial number? BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis()); Instant validFrom = Instant.now(); @@ -50,6 +68,23 @@ public class CsrSigner { Date.from(validFrom), Date.from(validUntil), csr.getSubject(), csr.getSubjectPublicKeyInfo()); + var certAttributes = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest); + if (certAttributes != null && certAttributes.length > 0) { + ArrayList altNames = new ArrayList<>(); + + for (Attribute attribute : certAttributes) { + Extensions extensions = Extensions.getInstance(attribute.getAttrValues().getObjectAt(0)); + GeneralNames gns = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName); + if (gns != null && gns.getNames() != null) { + Arrays.stream(gns.getNames()).filter(n -> isValidName(n.getTagNo())).forEach(altNames::add); + } + } + if (!altNames.isEmpty()) { + builder.addExtension(Extension.subjectAlternativeName, false, + new GeneralNames(altNames.toArray(new GeneralName[altNames.size()]))); + } + } + ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).build(issuerKey); X509CertificateHolder certHolder = builder.build(signer); X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder); diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogPreflightGeneratePeriodical.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogPreflightGeneratePeriodical.java index ce88ce656c..3992b5bb57 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogPreflightGeneratePeriodical.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogPreflightGeneratePeriodical.java @@ -16,13 +16,12 @@ */ package org.graylog2.bootstrap.preflight; -import okhttp3.Authenticator; +import com.google.common.base.Splitter; import okhttp3.Call; import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import okhttp3.Route; import org.graylog.security.certutil.CaConfiguration; import org.graylog.security.certutil.CaService; import org.graylog.security.certutil.cert.CertificateChain; @@ -36,6 +35,7 @@ import org.graylog2.cluster.NodeService; import org.graylog2.cluster.preflight.NodePreflightConfig; import org.graylog2.cluster.preflight.NodePreflightConfigService; import org.graylog2.plugin.periodical.Periodical; +import org.graylog2.security.CustomCAX509TrustManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,17 +44,20 @@ import javax.inject.Named; import javax.inject.Singleton; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.X509Certificate; import java.util.List; import java.util.Optional; +import static com.google.common.base.Strings.isNullOrEmpty; import static org.graylog.security.certutil.CaService.DEFAULT_VALIDITY; @Singleton @@ -68,9 +71,9 @@ public class GraylogPreflightGeneratePeriodical extends Periodical { private final CsrMongoStorage csrStorage; private final CertChainStorage certMongoStorage; private final CaService caService; + private final CsrSigner csrSigner; private final String passwordSecret; - private final OkHttpClient okHttpClient; - private OkHttpClient tmpClient; + private Optional okHttpClient = Optional.empty(); @Inject public GraylogPreflightGeneratePeriodical(final NodePreflightConfigService nodePreflightConfigService, @@ -79,7 +82,7 @@ public class GraylogPreflightGeneratePeriodical extends Periodical { final CaService caService, final Configuration configuration, final NodeService nodeService, - final OkHttpClient okHttpClient, + final CsrSigner csrSigner, final @Named("password_secret") String passwordSecret) { this.nodePreflightConfigService = nodePreflightConfigService; this.csrStorage = csrStorage; @@ -88,47 +91,26 @@ public class GraylogPreflightGeneratePeriodical extends Periodical { this.passwordSecret = passwordSecret; this.configuration = configuration; this.nodeService = nodeService; - this.okHttpClient = okHttpClient; - buildTempHttpClient(); + this.csrSigner = csrSigner; } - // building a non checking httpclient - private void buildTempHttpClient() { + // building a httpclient to check the connectivity to OpenSearch - TODO: maybe replace it with a VersionProbe already? + private Optional buildTempHttpClient() { try { - TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - @Override - public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) { - } - - @Override - public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) { - } - - @Override - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new java.security.cert.X509Certificate[]{}; - } - } - }; - - SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); - - OkHttpClient.Builder newBuilder = new OkHttpClient.Builder(); - newBuilder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]); - newBuilder.hostnameVerifier((hostname, session) -> true); - newBuilder.authenticator(new Authenticator() { - public Request authenticate(Route route, Response response) throws IOException { - String credential = Credentials.basic("admin", "admin"); - return response.request().newBuilder().header("Authorization", credential).build(); - } - }); - - tmpClient = newBuilder.build(); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + try { + var sslContext = SSLContext.getInstance("TLS"); + var tm = new CustomCAX509TrustManager(caService); + sslContext.init(null, new TrustManager[]{tm}, new SecureRandom()); + clientBuilder.sslSocketFactory(sslContext.getSocketFactory(), tm); + } catch (NoSuchAlgorithmException ex) { + LOG.error("Could not set Graylog CA trustmanager: {}", ex.getMessage(), ex); + } + return Optional.of(clientBuilder.build()); } catch (Exception ex) { - LOG.error("Could not create tmp okhttpclient " + ex.getMessage(), ex); + LOG.error("Could not create temporary okhttpclient " + ex.getMessage(), ex); } + return Optional.empty(); } @Override @@ -143,6 +125,10 @@ public class GraylogPreflightGeneratePeriodical extends Periodical { return; } + if(okHttpClient.isEmpty()) { + okHttpClient = buildTempHttpClient(); + } + KeyStore caKeystore = optKey.get(); var caPrivateKey = (PrivateKey) caKeystore.getKey("ca", password); var caCertificate = (X509Certificate) caKeystore.getCertificate("ca"); @@ -161,7 +147,7 @@ public class GraylogPreflightGeneratePeriodical extends Periodical { .errorMsg("Node in CSR state, but no CSR present") .build()); } else { - var cert = CsrSigner.sign(caPrivateKey, caCertificate, csr.get(), c.validFor() != null ? c.validFor() : DEFAULT_VALIDITY); + var cert = csrSigner.sign(caPrivateKey, caCertificate, csr.get(), c.validFor() != null ? c.validFor() : DEFAULT_VALIDITY); //TODO: assumptions about the chain, to contain 2 CAs, named "ca" and "root"... final List caCertificates = List.of(caCertificate, rootCertificate); certMongoStorage.writeCertChain(new CertificateChain(cert, caCertificates), c.nodeId()); @@ -194,11 +180,28 @@ public class GraylogPreflightGeneratePeriodical extends Periodical { private boolean checkConnectivity(final String nodeId) throws NodeNotFoundException, IOException { final var node = nodeService.byNodeId(nodeId); Request request = new Request.Builder().url(node.getTransportAddress()).build(); - Call call = tmpClient.newCall(request); - try(Response response = call.execute()) { - return response.isSuccessful(); + if(okHttpClient.isPresent()) { + OkHttpClient.Builder builder = okHttpClient.get().newBuilder(); + try { + URI uri = new URI(node.getTransportAddress()); + if (!isNullOrEmpty(uri.getUserInfo())) { + var list = Splitter.on(":").limit(2).splitToList(uri.getUserInfo()); + builder.authenticator((route, response) -> { + String credential = Credentials.basic(list.get(0), list.get(1)); + return response.request().newBuilder().header("Authorization", credential).build(); + }); + } + } catch (URISyntaxException ex) { + return false; + } + Call call = builder.build().newCall(request); + try(Response response = call.execute()) { + return response.isSuccessful(); + } + } else { + return false; } - } + } @Override protected Logger getLogger() { diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightConfigServiceImpl.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightConfigServiceImpl.java index 9ed6dc149f..b8e30b4242 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightConfigServiceImpl.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightConfigServiceImpl.java @@ -43,7 +43,7 @@ public class PreflightConfigServiceImpl extends PersistedServiceImpl implements @Override public PreflightConfig saveConfiguration() throws ValidationException { - final ImmutableMap fields = ImmutableMap.of("finished", true); + final ImmutableMap fields = ImmutableMap.of("result", PreflightConfigResult.FINISHED); final PreflightConfigImpl config = new PreflightConfigImpl(fields); final String id = save(config); return getPersistedConfig().orElseThrow(() -> new IllegalStateException("Failed to obtain configuration that was just stored")); diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightWebModule.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightWebModule.java index 6c7e66fd04..6bf169d81e 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightWebModule.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightWebModule.java @@ -21,6 +21,8 @@ import com.google.common.util.concurrent.ServiceManager; import com.google.inject.TypeLiteral; import com.google.inject.multibindings.MapBinder; import com.google.inject.multibindings.Multibinder; +import org.graylog.security.certutil.CaService; +import org.graylog.security.certutil.CaServiceImpl; import org.graylog.security.certutil.keystore.storage.KeystoreContentMover; import org.graylog.security.certutil.keystore.storage.SinglePasswordKeystoreContentMover; import org.graylog2.bindings.providers.MongoConnectionProvider; @@ -51,6 +53,7 @@ public class PreflightWebModule extends Graylog2Module { bind(NodeService.class).to(NodeServiceImpl.class); bind(KeystoreContentMover.class).to(SinglePasswordKeystoreContentMover.class).asEagerSingleton(); bind(ClusterConfigService.class).to(ClusterConfigServiceImpl.class); + bind(CaService.class).to(CaServiceImpl.class); bind(PreflightConfigService.class).to(PreflightConfigServiceImpl.class); bind(PreflightBoot.class).asEagerSingleton(); diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/ServerPreflightChecksModule.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/ServerPreflightChecksModule.java index 816c9cac9d..ce399d4d5c 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/ServerPreflightChecksModule.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/ServerPreflightChecksModule.java @@ -16,18 +16,36 @@ */ package org.graylog2.bootstrap.preflight; -import com.google.inject.Scopes; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import okhttp3.OkHttpClient; +import org.graylog.security.certutil.CaService; +import org.graylog.security.certutil.CaServiceImpl; +import org.graylog.security.certutil.keystore.storage.KeystoreContentMover; +import org.graylog.security.certutil.keystore.storage.SinglePasswordKeystoreContentMover; import org.graylog2.audit.AuditEventSender; import org.graylog2.audit.NullAuditEventSender; import org.graylog2.plugin.inject.Graylog2Module; import org.graylog2.plugin.system.FilePersistedNodeIdProvider; import org.graylog2.plugin.system.NodeId; +import org.graylog2.security.CustomCAX509TrustManager; +import org.graylog2.security.TrustManagerProvider; +import org.graylog2.shared.bindings.providers.OkHttpClientProvider; import org.graylog2.storage.providers.ElasticsearchVersionProvider; +import javax.net.ssl.TrustManager; + public class ServerPreflightChecksModule extends Graylog2Module { @Override protected void configure() { + bind(CaService.class).to(CaServiceImpl.class); + + install(new FactoryModuleBuilder() + .implement(TrustManager.class, CustomCAX509TrustManager.class) + .build(TrustManagerProvider.class)); + + bind(KeystoreContentMover.class).to(SinglePasswordKeystoreContentMover.class).asEagerSingleton(); + bind(OkHttpClient.class).toProvider(OkHttpClientProvider.class).asEagerSingleton(); bind(ElasticsearchVersionProvider.class).asEagerSingleton(); bind(NodeId.class).toProvider(FilePersistedNodeIdProvider.class).asEagerSingleton(); bind(AuditEventSender.class).to(NullAuditEventSender.class); diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java index c4bd43b7c2..f1f502459f 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java @@ -16,8 +16,8 @@ */ package org.graylog2.bootstrap.preflight.web.resources; +import org.apache.http.client.utils.URIBuilder; import org.glassfish.jersey.media.multipart.FormDataBodyPart; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.FormDataParam; import org.graylog.security.certutil.CaService; import org.graylog.security.certutil.ca.exceptions.CACreationException; @@ -27,23 +27,23 @@ import org.graylog2.bootstrap.preflight.PreflightConstants; import org.graylog2.bootstrap.preflight.web.resources.model.CA; import org.graylog2.bootstrap.preflight.web.resources.model.CertParameters; import org.graylog2.cluster.Node; +import org.graylog2.cluster.NodeService; import org.graylog2.cluster.preflight.NodePreflightConfig; import org.graylog2.cluster.preflight.NodePreflightConfigService; -import org.graylog2.cluster.NodeService; import javax.inject.Inject; import javax.inject.Named; import javax.validation.constraints.NotNull; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; -import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import java.util.ArrayList; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -79,10 +79,19 @@ public class PreflightResource { return activeDataNodes.values().stream().map(n -> { final var preflight = preflightDataNodes.get(n.getNodeId()); - return new DataNode(n.getNodeId(), n.getType(), n.getTransportAddress(), preflight != null ? preflight.state() : null, preflight != null ? preflight.errorMsg() : null, n.getHostname(), n.getShortNodeId()); + return new DataNode(n.getNodeId(), n.getType(), removeCredentialsFromTransportAddress(n.getTransportAddress()), preflight != null ? preflight.state() : null, preflight != null ? preflight.errorMsg() : null, n.getHostname(), n.getShortNodeId()); }).collect(Collectors.toList()); } + private String removeCredentialsFromTransportAddress(final String orig) { + try { + final var uri = new URI(orig); + return new URIBuilder().setScheme(uri.getScheme()).setHost(uri.getHost()).setPort(uri.getPort()).toString(); + } catch (URISyntaxException ex) { + return "Invalid URI"; + } + } + @GET @Path("/ca") public CA get() throws KeyStoreStorageException { diff --git a/graylog2-server/src/main/java/org/graylog2/security/CustomCAX509TrustManager.java b/graylog2-server/src/main/java/org/graylog2/security/CustomCAX509TrustManager.java new file mode 100644 index 0000000000..37731e1846 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog2/security/CustomCAX509TrustManager.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.security; + +import com.google.common.collect.Iterables; +import org.graylog.security.certutil.CaService; +import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class CustomCAX509TrustManager implements X509TrustManager { + private static final Logger LOG = LoggerFactory.getLogger(CustomCAX509TrustManager.class); + private final List trustManagers = new ArrayList<>(); + + @Inject + public CustomCAX509TrustManager(CaService caService) { + try { + trustManagers.add(getDefaultTrustManager()); + caService.loadKeyStore().ifPresent(keystore -> trustManagers.add(getTrustManager(keystore))); + } catch (KeyStoreException | KeyStoreStorageException | NoSuchAlgorithmException k) { + LOG.error("Could not add Graylog CA to TrustManagers: {}", k.getMessage(), k); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + for (X509TrustManager trustManager : trustManagers) { + try { + trustManager.checkClientTrusted(chain, authType); + return; + } catch (CertificateException e) {} + } + throw new CertificateException("None of the TrustManagers trust this certificate chain."); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + for (X509TrustManager trustManager : trustManagers) { + try { + trustManager.checkServerTrusted(chain, authType); + return; + } catch (CertificateException e) {} + } + throw new CertificateException("None of the TrustManagers trust this certificate chain."); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + final var certificates = new ArrayList(); + trustManagers.forEach(tm -> certificates.addAll(Arrays.asList(tm.getAcceptedIssuers()))); + return certificates.toArray(new X509Certificate[0]); + } + + private X509TrustManager getDefaultTrustManager() throws NoSuchAlgorithmException, KeyStoreException { + return getTrustManager(null); + } + + private X509TrustManager getTrustManager(KeyStore keystore) { + try { + TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init(keystore); + return Iterables.getFirst(Iterables.filter(Arrays.asList(factory.getTrustManagers()), X509TrustManager.class), null); + } catch (NoSuchAlgorithmException | KeyStoreException e) { + LOG.error("Could not create TrustManager: {}", e.getMessage(), e); + } + return null; + } +} diff --git a/graylog2-server/src/main/java/org/graylog2/security/TrustManagerAndSocketFactoryProvider.java b/graylog2-server/src/main/java/org/graylog2/security/TrustManagerAndSocketFactoryProvider.java new file mode 100644 index 0000000000..e00fc27cc7 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog2/security/TrustManagerAndSocketFactoryProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.security; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +@Singleton +public class TrustManagerAndSocketFactoryProvider { + private final SSLSocketFactory sslSocketFactory; + private final X509TrustManager trustManager; + private final SSLContext sslContext; + + @Inject + public TrustManagerAndSocketFactoryProvider(CustomCAX509TrustManager trustManager) throws NoSuchAlgorithmException, KeyManagementException { + this.sslContext = SSLContext.getInstance("TLS"); + this.trustManager = trustManager; + sslContext.init(null, new TrustManager[]{this.trustManager}, new SecureRandom()); + this.sslSocketFactory = sslContext.getSocketFactory(); + } + + public SSLSocketFactory getSslSocketFactory() { + return this.sslSocketFactory; + } + + public X509TrustManager getTrustManager() { + return trustManager; + } + + public SSLContext getSslContext() { + return sslContext; + } +} diff --git a/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/OkHttpClientProvider.java b/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/OkHttpClientProvider.java index 375dd91c41..52bae535e6 100644 --- a/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/OkHttpClientProvider.java +++ b/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/OkHttpClientProvider.java @@ -27,6 +27,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.Route; +import org.graylog2.security.TrustManagerAndSocketFactoryProvider; import org.graylog2.utilities.ProxyHostsPattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,20 +71,24 @@ public class OkHttpClientProvider implements Provider { protected final Duration writeTimeout; protected final URI httpProxyUri; protected final ProxyHostsPattern nonProxyHostsPattern; + private final TrustManagerAndSocketFactoryProvider trustManagerAndSocketFactoryProvider; @Inject public OkHttpClientProvider(@Named("http_connect_timeout") Duration connectTimeout, @Named("http_read_timeout") Duration readTimeout, @Named("http_write_timeout") Duration writeTimeout, @Named("http_proxy_uri") @Nullable URI httpProxyUri, - @Named("http_non_proxy_hosts") @Nullable ProxyHostsPattern nonProxyHostsPattern) { + @Named("http_non_proxy_hosts") @Nullable ProxyHostsPattern nonProxyHostsPattern, + TrustManagerAndSocketFactoryProvider trustManagerAndSocketFactoryProvider) { this.connectTimeout = requireNonNull(connectTimeout); this.readTimeout = requireNonNull(readTimeout); this.writeTimeout = requireNonNull(writeTimeout); this.httpProxyUri = httpProxyUri; this.nonProxyHostsPattern = nonProxyHostsPattern; + this.trustManagerAndSocketFactoryProvider = trustManagerAndSocketFactoryProvider; } + @Override public OkHttpClient get() { final OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() @@ -92,6 +97,11 @@ public class OkHttpClientProvider implements Provider { .writeTimeout(writeTimeout.getQuantity(), writeTimeout.getUnit()) .readTimeout(readTimeout.getQuantity(), readTimeout.getUnit()); + if(trustManagerAndSocketFactoryProvider != null) { + // always set our own CA, might be overriden in later code + clientBuilder.sslSocketFactory(trustManagerAndSocketFactoryProvider.getSslSocketFactory(), trustManagerAndSocketFactoryProvider.getTrustManager()); + } + if (httpProxyUri != null) { final ProxySelector proxySelector = new ProxySelector() { @Override diff --git a/graylog2-server/src/main/java/org/graylog2/shared/security/SecurityBindings.java b/graylog2-server/src/main/java/org/graylog2/shared/security/SecurityBindings.java index 00b8e0826f..cf8aea7848 100644 --- a/graylog2-server/src/main/java/org/graylog2/shared/security/SecurityBindings.java +++ b/graylog2-server/src/main/java/org/graylog2/shared/security/SecurityBindings.java @@ -19,10 +19,16 @@ package org.graylog2.shared.security; import com.google.inject.Scopes; import com.google.inject.assistedinject.FactoryModuleBuilder; import com.google.inject.multibindings.OptionalBinder; +import org.graylog.security.certutil.CaService; +import org.graylog.security.certutil.CaServiceImpl; +import org.graylog.security.certutil.keystore.storage.KeystoreContentMover; +import org.graylog.security.certutil.keystore.storage.SinglePasswordKeystoreContentMover; import org.graylog2.plugin.PluginModule; import org.graylog2.rest.models.system.sessions.responses.DefaultSessionResponseFactory; import org.graylog2.rest.models.system.sessions.responses.SessionResponseFactory; +import org.graylog2.security.CustomCAX509TrustManager; import org.graylog2.security.DefaultX509TrustManager; +import org.graylog2.security.TrustManagerAndSocketFactoryProvider; import org.graylog2.security.TrustManagerProvider; import org.graylog2.security.UserSessionTerminationService; import org.graylog2.security.encryption.EncryptedValueService; @@ -38,9 +44,12 @@ public class SecurityBindings extends PluginModule { addPermissions(RestPermissions.class); addInitializer(UserSessionTerminationService.class); + bind(KeystoreContentMover.class).to(SinglePasswordKeystoreContentMover.class).asEagerSingleton(); install(new FactoryModuleBuilder() .implement(TrustManager.class, DefaultX509TrustManager.class) .build(TrustManagerProvider.class)); + bind(CustomCAX509TrustManager.class).asEagerSingleton(); + bind(CaService.class).to(CaServiceImpl.class); OptionalBinder.newOptionalBinder(binder(), ActorAwareAuthenticationTokenFactory.class) .setDefault().to(ActorAwareUsernamePasswordTokenFactory.class); diff --git a/graylog2-server/src/main/java/org/graylog2/storage/versionprobe/VersionProbe.java b/graylog2-server/src/main/java/org/graylog2/storage/versionprobe/VersionProbe.java index 031d56d610..2b0ab1e95e 100644 --- a/graylog2-server/src/main/java/org/graylog2/storage/versionprobe/VersionProbe.java +++ b/graylog2-server/src/main/java/org/graylog2/storage/versionprobe/VersionProbe.java @@ -29,7 +29,6 @@ import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.ResponseBody; -import org.graylog2.plugin.Version; import org.graylog2.shared.utilities.ExceptionUtils; import org.graylog2.storage.SearchVersion; import org.slf4j.Logger; diff --git a/graylog2-server/src/test/java/org/graylog2/security/CustomCAX509TrustManagerTest.java b/graylog2-server/src/test/java/org/graylog2/security/CustomCAX509TrustManagerTest.java new file mode 100644 index 0000000000..36ca0e1ea8 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/security/CustomCAX509TrustManagerTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.security; + +import org.assertj.core.api.Assertions; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.graylog.security.certutil.CaService; +import org.graylog.security.certutil.CertutilCa; +import org.graylog.security.certutil.CertutilCert; +import org.graylog.security.certutil.ca.exceptions.CACreationException; +import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; +import org.graylog.security.certutil.console.TestableConsole; +import org.graylog2.bootstrap.preflight.web.resources.model.CA; +import org.graylog2.plugin.Tools; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Optional; + +public class CustomCAX509TrustManagerTest { + @TempDir + static Path tempDir; + + static class DummyCaService implements CaService { + private final Optional keyStore; + public DummyCaService(KeyStore keyStore) { + this.keyStore = Optional.ofNullable(keyStore); + } + + @Override + public CA get() throws KeyStoreStorageException { + return null; + } + + @Override + public void create(Integer daysValid, char[] password) throws CACreationException, KeyStoreStorageException {} + + @Override + public void upload(String pass, List parts) throws CACreationException {} + + @Override + public void startOver() {} + + @Override + public Optional loadKeyStore() throws KeyStoreException, KeyStoreStorageException, NoSuchAlgorithmException { + return this.keyStore; + } + } + + @Test + public void testCA() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException { + // create certs (root, ca, datanode) copy of CertUtilTest + final Path caPath = tempDir.resolve("test-ca.p12"); + final Path nodePath = tempDir.resolve("test-node.p12"); + + final TestableConsole inputCa = TestableConsole.empty() + .register("Enter CA password", "password"); + final CertutilCa certutilCa = new CertutilCa(caPath.toAbsolutePath().toString(), inputCa); + certutilCa.run(); + + // now we have a ROOT + CA keypair in the keystore, let's use it to generate node keypair + + TestableConsole inputCert = TestableConsole.empty() + .register("Enter CA password", "password") + .register("Enter datanode certificate password", "changeme"); + + CertutilCert certutilCert = new CertutilCert( + caPath.toAbsolutePath().toString(), + nodePath.toAbsolutePath().toString(), + inputCert); + certutilCert.run(); + + KeyStore caKeyStore = KeyStore.getInstance("PKCS12"); + caKeyStore.load(new FileInputStream(caPath.toFile()), "password".toCharArray()); + + KeyStore nodeKeyStore = KeyStore.getInstance("PKCS12"); + nodeKeyStore.load(new FileInputStream(nodePath.toFile()), "changeme".toCharArray()); + final Key nodeKey = nodeKeyStore.getKey(CertutilCert.DATANODE_KEY_ALIAS, "changeme".toCharArray()); + Assertions.assertThat(nodeKey).isNotNull(); + + Assertions.assertThatCode(() -> nodeKeyStore.getCertificate(CertutilCert.DATANODE_KEY_ALIAS).verify(caKeyStore.getCertificate("ca").getPublicKey())) + .doesNotThrowAnyException(); + + var hostname = Tools.getLocalCanonicalHostname(); + final Certificate[] certificateChain = nodeKeyStore.getCertificateChain(CertutilCert.DATANODE_KEY_ALIAS); + Assertions.assertThat(certificateChain) + .hasSize(3) + .extracting(c ->(X509Certificate)c) + .extracting(c -> c.getSubjectX500Principal().getName()) + .contains("CN=root", "CN=ca", "CN=" + hostname); + + // additional Tests + final var noAdditionalKeystore = new DummyCaService(null); + final var additionalKeystore = new DummyCaService(caKeyStore); + + final var defaultTM = new CustomCAX509TrustManager(noAdditionalKeystore); + final var customTM = new CustomCAX509TrustManager(additionalKeystore); + + final var default_issuers = defaultTM.getAcceptedIssuers().length; + Assertions.assertThat(customTM.getAcceptedIssuers().length).isEqualTo(default_issuers + 2); + + final var cert = (X509Certificate)nodeKeyStore.getCertificate(CertutilCert.DATANODE_KEY_ALIAS); + + Assertions.assertThatCode(() -> { + try { + defaultTM.checkClientTrusted(new X509Certificate[]{cert}, "ANY"); + throw new Exception("Should not get here"); + } catch (CertificateException e) { + // expected + } + }).doesNotThrowAnyException(); + + Assertions.assertThatCode(() -> + customTM.checkClientTrusted(new X509Certificate[]{cert}, "ANY") + ).doesNotThrowAnyException(); + } +} diff --git a/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilCertTest.java b/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilCertTest.java index 6007875844..8ba3e624c8 100644 --- a/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilCertTest.java +++ b/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilCertTest.java @@ -20,6 +20,7 @@ import org.assertj.core.api.Assertions; import org.graylog.security.certutil.CertutilCa; import org.graylog.security.certutil.CertutilCert; import org.graylog.security.certutil.console.TestableConsole; +import org.graylog2.plugin.Tools; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -79,11 +80,12 @@ class CertutilCertTest { Assertions.assertThatCode(() -> nodeKeyStore.getCertificate(CertutilCert.DATANODE_KEY_ALIAS).verify(caKeyStore.getCertificate("ca").getPublicKey())) .doesNotThrowAnyException(); + var hostname = Tools.getLocalCanonicalHostname(); final Certificate[] certificateChain = nodeKeyStore.getCertificateChain(CertutilCert.DATANODE_KEY_ALIAS); Assertions.assertThat(certificateChain) .hasSize(3) .extracting(c ->(X509Certificate)c) .extracting(c -> c.getSubjectX500Principal().getName()) - .contains("CN=root", "CN=ca", "CN=localhost"); + .contains("CN=root", "CN=ca", "CN=" + hostname); } } diff --git a/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilHttpTest.java b/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilHttpTest.java index b2f8cb6371..2f063de7db 100644 --- a/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilHttpTest.java +++ b/graylog2-server/src/test/java/org/graylog2/security/certutil/CertutilHttpTest.java @@ -21,6 +21,7 @@ import org.graylog.security.certutil.CertutilCa; import org.graylog.security.certutil.CertutilCert; import org.graylog.security.certutil.CertutilHttp; import org.graylog.security.certutil.console.TestableConsole; +import org.graylog2.plugin.Tools; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -92,11 +93,12 @@ class CertutilHttpTest { .extracting(item -> (String) item.get(1)) .contains("localhost", "example.com"); + var hostname = Tools.getLocalCanonicalHostname(); final Certificate[] certificateChain = nodeKeyStore.getCertificateChain(CertutilCert.DATANODE_KEY_ALIAS); Assertions.assertThat(certificateChain) .hasSize(3) .extracting(c ->(X509Certificate)c) .extracting(c -> c.getSubjectX500Principal().getName()) - .contains("CN=root", "CN=ca", "CN=localhost"); + .contains("CN=root", "CN=ca", "CN=" + hostname); } } diff --git a/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/OkHttpClientProviderTest.java b/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/OkHttpClientProviderTest.java index 7189efa69c..04bd5cbdf7 100644 --- a/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/OkHttpClientProviderTest.java +++ b/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/OkHttpClientProviderTest.java @@ -293,7 +293,7 @@ public class OkHttpClientProviderTest { Duration.milliseconds(100L), Duration.milliseconds(100L), server.url("/").uri(), - null); + null, null); OkHttpClientProvider spyClientProvider = Mockito.spy(provider); @@ -336,7 +336,7 @@ public class OkHttpClientProviderTest { Duration.milliseconds(100L), Duration.milliseconds(100L), proxyURI, - null); + null, null); return provider.get(); } diff --git a/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/ParameterizedHttpClientProviderTest.java b/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/ParameterizedHttpClientProviderTest.java index 4623a51feb..9dea0422ed 100644 --- a/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/ParameterizedHttpClientProviderTest.java +++ b/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/ParameterizedHttpClientProviderTest.java @@ -146,7 +146,7 @@ public class ParameterizedHttpClientProviderTest { Duration.milliseconds(500L), Duration.milliseconds(500L), proxyURI, - null); + null, null); return provider; }