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 aa0a2858eb..6f6a41d3c8 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 @@ -16,14 +16,15 @@ */ package org.graylog.datanode.configuration; +import com.google.common.collect.ImmutableMap; import org.graylog.datanode.Configuration; import org.graylog.datanode.configuration.variants.InSecureConfiguration; import org.graylog.datanode.configuration.variants.MongoCertSecureConfiguration; +import org.graylog.datanode.configuration.variants.OpensearchSecurityConfiguration; 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; @@ -34,6 +35,7 @@ import java.security.GeneralSecurityException; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; @Singleton public class OpensearchConfigurationProvider implements Provider { @@ -60,7 +62,7 @@ public class OpensearchConfigurationProvider implements Provider securityConfigurationTypes = List.of( @@ -75,11 +77,17 @@ public class OpensearchConfigurationProvider implements Provider new OpensearchConfigurationException("No valid option to start up OpenSearch")); try { - final Map config = chosenSecurityConfigurationVariant.configure(localConfiguration); + final OpensearchSecurityConfiguration securityConfiguration = chosenSecurityConfigurationVariant + .build() + .configure(localConfiguration); + + ImmutableMap.Builder opensearchProperties = ImmutableMap.builder(); + opensearchProperties.putAll(commonOpensearchConfig(localConfiguration)); + opensearchProperties.putAll(securityConfiguration.getProperties()); return new OpensearchConfiguration( datanodeConfiguration.opensearchDistribution().directory(), - Path.of(opensearchConfigLocation), + opensearchConfigLocation, localConfiguration.getHttpBindAddress().getHost(), localConfiguration.getHostname(), localConfiguration.getOpensearchHttpPort(), @@ -90,11 +98,31 @@ public class OpensearchConfigurationProvider implements Provider commonOpensearchConfig(final Configuration localConfiguration) { + final ImmutableMap.Builder config = ImmutableMap.builder(); + Objects.requireNonNull(localConfiguration.getConfigLocation(), "config_location setting is required!"); + localConfiguration.getOpensearchNetworkHostHost().ifPresent( + networkHost -> config.put("network.host", networkHost)); + config.put("path.data", Path.of(localConfiguration.getOpensearchDataLocation()).resolve(localConfiguration.getDatanodeNodeName()).toAbsolutePath().toString()); + config.put("path.logs", Path.of(localConfiguration.getOpensearchLogsLocation()).resolve(localConfiguration.getDatanodeNodeName()).toAbsolutePath().toString()); + if (localConfiguration.isSingleNodeOnly()) { + config.put("discovery.type", "single-node"); + } else { + config.put("cluster.initial_master_nodes", "node1"); + } + + // listen on all interfaces + config.put("network.bind_host", "0.0.0.0"); + + return config.build(); + } + } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/RootCertificateFinder.java b/data-node/src/main/java/org/graylog/datanode/configuration/RootCertificateFinder.java index 9ae6c93732..b961481eb9 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/RootCertificateFinder.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/RootCertificateFinder.java @@ -28,9 +28,9 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; -public class RootCertificateFinder { +class RootCertificateFinder { - public X509Certificate findRootCert(Path keystorePath, + X509Certificate findRootCert(Path keystorePath, char[] password, final String alias) throws IOException, GeneralSecurityException { final KeyStore keystore = loadKeystore(keystorePath, password); diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/TlsConfigurationSupplier.java b/data-node/src/main/java/org/graylog/datanode/configuration/TlsConfigurationSupplier.java deleted file mode 100644 index 2cdb64d220..0000000000 --- a/data-node/src/main/java/org/graylog/datanode/configuration/TlsConfigurationSupplier.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.datanode.configuration; - -import com.google.common.collect.ImmutableMap; -import org.graylog.datanode.configuration.certificates.CertificateMetaData; - -import static org.graylog.datanode.configuration.variants.SecureConfiguration.SSL_PREFIX; - -public class TlsConfigurationSupplier { - - static final String KEYSTORE_FORMAT = "PKCS12"; - static final String TRUSTSTORE_FORMAT = "PKCS12"; - - public static final String TRUSTSTORE_FILENAME = "datanode-truststore.p12"; - - public ImmutableMap getTransportTlsConfig(final CertificateMetaData certificateMetaData) { - final String configSubPart = SSL_PREFIX + "transport"; - return getTlsConfig(configSubPart, certificateMetaData); - } - - public ImmutableMap getHttpTlsConfig(final CertificateMetaData certificateMetaData) { - final String configSubPart = SSL_PREFIX + "http"; - return ImmutableMap.builder() - .putAll(getTlsConfig(configSubPart, certificateMetaData)) - .put(configSubPart + ".enabled", "true") - .build(); - } - - public ImmutableMap getTrustStoreTlsConfig(final String truststorePassword) { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - - String configSubPart = SSL_PREFIX + "http"; - builder.put(configSubPart + ".truststore_type", TRUSTSTORE_FORMAT); - builder.put(configSubPart + ".truststore_filepath", TRUSTSTORE_FILENAME); - builder.put(configSubPart + ".truststore_password", truststorePassword); - - configSubPart = SSL_PREFIX + "transport"; - builder.put(configSubPart + ".truststore_type", TRUSTSTORE_FORMAT); - builder.put(configSubPart + ".truststore_filepath", TRUSTSTORE_FILENAME); - builder.put(configSubPart + ".truststore_password", truststorePassword); - return builder.build(); - } - - private ImmutableMap getTlsConfig(final String configPrefix, - final CertificateMetaData certificateMetaData - ) { - return ImmutableMap.builder() - .put(configPrefix + ".keystore_type", KEYSTORE_FORMAT) - .put(configPrefix + ".keystore_filepath", certificateMetaData.keystoreFilePath()) - .put(configPrefix + ".keystore_password", certificateMetaData.passwordAsString()) - .put(configPrefix + ".keystore_alias", certificateMetaData.alias()) - .build(); - } -} diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/TruststoreCreator.java b/data-node/src/main/java/org/graylog/datanode/configuration/TruststoreCreator.java index 393cb9bda1..176c7a8970 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/TruststoreCreator.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/TruststoreCreator.java @@ -16,6 +16,7 @@ */ package org.graylog.datanode.configuration; +import org.graylog.datanode.configuration.variants.KeystoreInformation; import org.graylog.security.certutil.CertConstants; import java.io.FileOutputStream; @@ -24,23 +25,41 @@ import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.cert.X509Certificate; +import java.util.LinkedHashMap; import java.util.Map; public class TruststoreCreator { - public void createTruststore(final Map rootCerts, - final char[] truststorePassword, - final Path truststorePath) throws IOException, GeneralSecurityException { + private final Map rootCertificates; + private final RootCertificateFinder rootCertificateFinder; + + public TruststoreCreator(RootCertificateFinder rootCertificateFinder) { + this.rootCertificateFinder = rootCertificateFinder; + this.rootCertificates = new LinkedHashMap<>(); + } + + public static TruststoreCreator newTruststore() { + return new TruststoreCreator(new RootCertificateFinder()); + } + + public TruststoreCreator addRootCert(final String name, KeystoreInformation keystoreInformation, + final String alias) throws IOException, GeneralSecurityException { + final X509Certificate rootCert = rootCertificateFinder.findRootCert(keystoreInformation.location(), keystoreInformation.password(), alias); + rootCertificates.put(name, rootCert); + return this; + } + + public KeystoreInformation persist(final Path truststorePath, final char[] truststorePassword) throws IOException, GeneralSecurityException { KeyStore trustStore = KeyStore.getInstance(CertConstants.PKCS12); trustStore.load(null, null); - for (Map.Entry cert : rootCerts.entrySet()) { + for (Map.Entry cert : rootCertificates.entrySet()) { trustStore.setCertificateEntry(cert.getKey(), cert.getValue()); } try (final FileOutputStream fileOutputStream = new FileOutputStream(truststorePath.toFile())) { trustStore.store(fileOutputStream, truststorePassword); } + return new KeystoreInformation(truststorePath, truststorePassword); } - } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/InSecureConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/InSecureConfiguration.java index 796224d9f2..9159fbaaa1 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/InSecureConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/InSecureConfiguration.java @@ -16,14 +16,9 @@ */ package org.graylog.datanode.configuration.variants; -import com.google.common.collect.ImmutableMap; import org.graylog.datanode.Configuration; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; -import java.util.Map; - -import static org.graylog.datanode.configuration.variants.SecureConfiguration.SSL_PREFIX; - public class InSecureConfiguration implements SecurityConfigurationVariant { @Override @@ -34,11 +29,7 @@ public class InSecureConfiguration implements SecurityConfigurationVariant { } @Override - public ImmutableMap configure(Configuration localConfiguration) throws KeyStoreStorageException { - return ImmutableMap.builder() - .putAll(commonConfig(localConfiguration)) - .put("plugins.security.disabled", "true") - .put(SSL_PREFIX + "http.enabled", "false") - .build(); + public OpensearchSecurityConfiguration build() throws KeyStoreStorageException { + return OpensearchSecurityConfiguration.disabled(); } } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/certificates/CertificateMetaData.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/KeystoreInformation.java similarity index 55% rename from data-node/src/main/java/org/graylog/datanode/configuration/certificates/CertificateMetaData.java rename to data-node/src/main/java/org/graylog/datanode/configuration/variants/KeystoreInformation.java index 7a0f9fda20..e6c190848e 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/certificates/CertificateMetaData.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/KeystoreInformation.java @@ -14,21 +14,12 @@ * along with this program. If not, see * . */ -package org.graylog.datanode.configuration.certificates; +package org.graylog.datanode.configuration.variants; -import org.graylog.security.certutil.CertConstants; - -public record CertificateMetaData(String alias, - String keystoreFilePath, - char[] keystoreFilePassword) { - - public CertificateMetaData(String keystoreFilePath, - char[] keystoreFilePassword) { - this(CertConstants.DATANODE_KEY_ALIAS, keystoreFilePath, keystoreFilePassword); - } +import java.nio.file.Path; +public record KeystoreInformation(Path location, char[] password) { public String passwordAsString() { - return new String(keystoreFilePassword()); + return new String(password()); } - } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/MongoCertSecureConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/MongoCertSecureConfiguration.java index a87fda7d51..e1c86c082a 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/MongoCertSecureConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/MongoCertSecureConfiguration.java @@ -16,14 +16,8 @@ */ package org.graylog.datanode.configuration.variants; -import com.google.common.collect.ImmutableMap; import org.graylog.datanode.Configuration; -import org.graylog.datanode.configuration.RootCertificateFinder; -import org.graylog.datanode.configuration.TlsConfigurationSupplier; -import org.graylog.datanode.configuration.TruststoreCreator; -import org.graylog.datanode.configuration.certificates.CertificateMetaData; import org.graylog.datanode.configuration.certificates.KeystoreReEncryption; -import org.graylog.security.certutil.CertConstants; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoCollections; @@ -34,23 +28,13 @@ import org.graylog2.plugin.system.NodeId; import javax.inject.Inject; import javax.inject.Named; import java.io.IOException; -import java.nio.file.Path; import java.security.GeneralSecurityException; -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -import static org.graylog.datanode.configuration.TlsConfigurationSupplier.TRUSTSTORE_FILENAME; public final class MongoCertSecureConfiguration extends SecureConfiguration { private final KeystoreFileLocation finalTransportKeystoreLocation; private final KeystoreFileLocation finalHttpKeystoreLocation; private final KeystoreReEncryption keystoreReEncryption; - private final TlsConfigurationSupplier tlsConfigurationSupplier; - private final TruststoreCreator truststoreCreator; - private final RootCertificateFinder rootCertificateFinder; private final CertificatesService certificatesService; private final char[] secret; @@ -61,18 +45,12 @@ public final class MongoCertSecureConfiguration extends SecureConfiguration { @Inject public MongoCertSecureConfiguration(final Configuration localConfiguration, final KeystoreReEncryption keystoreReEncryption, - final TlsConfigurationSupplier tlsConfigurationSupplier, - final TruststoreCreator truststoreCreator, - final RootCertificateFinder rootCertificateFinder, final NodeId nodeId, final @Named("password_secret") String passwordSecret, final CertificatesService certificatesService ) { super(localConfiguration); this.keystoreReEncryption = keystoreReEncryption; - this.tlsConfigurationSupplier = tlsConfigurationSupplier; - this.truststoreCreator = truststoreCreator; - this.rootCertificateFinder = rootCertificateFinder; this.certificatesService = certificatesService; this.finalTransportKeystoreLocation = new KeystoreFileLocation( @@ -95,48 +73,14 @@ public final class MongoCertSecureConfiguration extends SecureConfiguration { } @Override - public ImmutableMap configure(Configuration localConfiguration) throws KeyStoreStorageException, IOException, GeneralSecurityException { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(commonSecureConfig(localConfiguration)); - Map rootCerts = new HashMap<>(); - final String truststorePassword = UUID.randomUUID().toString(); - keystoreReEncryption.reEncyptWithSecret( - mongoLocation, - mongoKeystorePassword, - finalTransportKeystoreLocation); - keystoreReEncryption.reEncyptWithSecret( - mongoLocation, - mongoKeystorePassword, - finalHttpKeystoreLocation); + public OpensearchSecurityConfiguration build() throws KeyStoreStorageException, IOException, GeneralSecurityException { + // this will take the mongodb-stored keys and persist them on a disk, in the opensearch configuration directory + keystoreReEncryption.reEncyptWithSecret(mongoLocation, mongoKeystorePassword, finalTransportKeystoreLocation); + keystoreReEncryption.reEncyptWithSecret(mongoLocation, mongoKeystorePassword, finalHttpKeystoreLocation); - configureInitialAdmin(localConfiguration, localConfiguration.getRestApiUsername(), localConfiguration.getRestApiPassword()); - - rootCerts.put("shared-chain-CA-root", rootCertificateFinder.findRootCert( - finalTransportKeystoreLocation.keystorePath(), - secret, - CertConstants.DATANODE_KEY_ALIAS)); - builder.putAll(tlsConfigurationSupplier.getTransportTlsConfig(new CertificateMetaData( - localConfiguration.getDatanodeTransportCertificate(), - secret - ) - )); - builder.putAll(tlsConfigurationSupplier.getHttpTlsConfig(new CertificateMetaData( - localConfiguration.getDatanodeHttpCertificate(), - secret - ) - )); - - if (!rootCerts.isEmpty()) { - final Path trustStorePath = opensearchConfigDir.resolve(TRUSTSTORE_FILENAME); - truststoreCreator.createTruststore(rootCerts, - truststorePassword.toCharArray(), - trustStorePath - ); - System.setProperty("javax.net.ssl.trustStore", trustStorePath.toAbsolutePath().toString()); - System.setProperty("javax.net.ssl.trustStorePassword", truststorePassword); - builder.putAll(tlsConfigurationSupplier.getTrustStoreTlsConfig(truststorePassword)); - } - - return builder.build(); + return new OpensearchSecurityConfiguration( + new KeystoreInformation(finalTransportKeystoreLocation.keystorePath().toAbsolutePath(), secret), + new KeystoreInformation(finalHttpKeystoreLocation.keystorePath().toAbsolutePath(), secret) + ); } } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/OpensearchSecurityConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/OpensearchSecurityConfiguration.java new file mode 100644 index 0000000000..92302db0bf --- /dev/null +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/OpensearchSecurityConfiguration.java @@ -0,0 +1,182 @@ +/* + * 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.datanode.configuration.variants; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.RandomStringUtils; +import org.graylog.datanode.Configuration; +import org.graylog.datanode.configuration.TruststoreCreator; +import org.graylog.security.certutil.CertConstants; +import org.graylog2.jackson.TypeReferences; +import org.graylog2.security.hashing.BCryptPasswordAlgorithm; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.util.Map; +import java.util.Objects; + +public class OpensearchSecurityConfiguration { + + private static final String KEYSTORE_FORMAT = "PKCS12"; + private static final String TRUSTSTORE_FORMAT = "PKCS12"; + private static final String TRUSTSTORE_FILENAME = "datanode-truststore.p12"; + + private final KeystoreInformation transportCertificate; + private final KeystoreInformation httpCertificate; + private KeystoreInformation truststore; + + public OpensearchSecurityConfiguration(KeystoreInformation transportCertificate, KeystoreInformation httpCertificate) { + this.transportCertificate = transportCertificate; + this.httpCertificate = httpCertificate; + } + + public static OpensearchSecurityConfiguration disabled() { + return new OpensearchSecurityConfiguration(null, null); + } + + /** + * Caution: side effects! + * + * This method will take the current security setup and apply it to the managed opensearch. It will change the + * initial set of opensearch users, it will create and persist a truststore that will be set as a system-wide + * truststore. + */ + public OpensearchSecurityConfiguration configure(Configuration localConfiguration) throws GeneralSecurityException, IOException { + if (securityEnabled()) { + final Path opensearchConfigDir = Path.of(localConfiguration.getOpensearchConfigLocation()).resolve("opensearch"); + + final Path trustStorePath = opensearchConfigDir.resolve(TRUSTSTORE_FILENAME); + final String truststorePassword = RandomStringUtils.randomAlphabetic(256); + + this.truststore = TruststoreCreator.newTruststore() + .addRootCert("transport-chain-CA-root", transportCertificate, CertConstants.DATANODE_KEY_ALIAS) + .addRootCert("http-chain-CA-root", httpCertificate, CertConstants.DATANODE_KEY_ALIAS) + .persist(trustStorePath, truststorePassword.toCharArray()); + + System.setProperty("javax.net.ssl.trustStore", trustStorePath.toAbsolutePath().toString()); + System.setProperty("javax.net.ssl.trustStorePassword", truststorePassword); + + configureInitialAdmin(localConfiguration, opensearchConfigDir, localConfiguration.getRestApiUsername(), localConfiguration.getRestApiPassword()); + } + return this; + } + + public Map getProperties() throws GeneralSecurityException, IOException { + final ImmutableMap.Builder config = ImmutableMap.builder(); + if (securityEnabled()) { + config.putAll(commonSecureConfig()); + + config.put("plugins.security.ssl.transport.keystore_type", KEYSTORE_FORMAT); + config.put("plugins.security.ssl.transport.keystore_filepath", transportCertificate.location().getFileName().toString()); // todo: this should be computed as a relative path + config.put("plugins.security.ssl.transport.keystore_password", transportCertificate.passwordAsString()); + config.put("plugins.security.ssl.transport.keystore_alias", CertConstants.DATANODE_KEY_ALIAS); + + config.put("plugins.security.ssl.transport.truststore_type", TRUSTSTORE_FORMAT); + config.put("plugins.security.ssl.transport.truststore_filepath", TRUSTSTORE_FILENAME); + config.put("plugins.security.ssl.transport.truststore_password", truststore.passwordAsString()); + + + config.put("plugins.security.ssl.http.enabled", "true"); + + config.put("plugins.security.ssl.http.keystore_type", KEYSTORE_FORMAT); + config.put("plugins.security.ssl.http.keystore_filepath", httpCertificate.location().getFileName().toString()); // todo: this should be computed as a relative path + config.put("plugins.security.ssl.http.keystore_password", httpCertificate.passwordAsString()); + config.put("plugins.security.ssl.http.keystore_alias", CertConstants.DATANODE_KEY_ALIAS); + + config.put("plugins.security.ssl.http.truststore_type", TRUSTSTORE_FORMAT); + config.put("plugins.security.ssl.http.truststore_filepath", TRUSTSTORE_FILENAME); + config.put("plugins.security.ssl.http.truststore_password", truststore.passwordAsString()); + } else { + config.put("plugins.security.disabled", "true"); + config.put("plugins.security.ssl.http.enabled", "false"); + } + return config.build(); + } + + public boolean securityEnabled() { + return !Objects.isNull(httpCertificate) && !Objects.isNull(transportCertificate); + } + + public KeystoreInformation getTransportCertificate() { + return transportCertificate; + } + + public KeystoreInformation getHttpCertificate() { + return httpCertificate; + } + + public KeystoreInformation getTruststore() { + return truststore; + } + + protected ImmutableMap commonSecureConfig() { + final ImmutableMap.Builder config = ImmutableMap.builder(); + + config.put("plugins.security.disabled", "false"); + //config.put(SSL_PREFIX + "http.enabled", "true"); + config.put("plugins.security.allow_default_init_securityindex", "true"); + //config.put("plugins.security.authcz.admin_dn", "CN=kirk,OU=client,O=client,L=test,C=de"); + + config.put("plugins.security.audit.type", "internal_opensearch"); + config.put("plugins.security.enable_snapshot_restore_privilege", "true"); + config.put("plugins.security.check_snapshot_restore_write_privileges", "true"); + config.put("plugins.security.restapi.roles_enabled", "all_access,security_rest_api_access"); + config.put("plugins.security.system_indices.enabled", "true"); + config.put("plugins.security.system_indices.indices", ".plugins-ml-model,.plugins-ml-task,.opendistro-alerting-config,.opendistro-alerting-alert*,.opendistro-anomaly-results*,.opendistro-anomaly-detector*,.opendistro-anomaly-checkpoints,.opendistro-anomaly-detection-state,.opendistro-reports-*,.opensearch-notifications-*,.opensearch-notebooks,.opensearch-observability,.opendistro-asynchronous-search-response*,.replication-metadata-store"); + config.put("node.max_local_storage_nodes", "3"); + + return config.build(); + } + + protected void configureInitialAdmin(final Configuration localConfiguration, final Path opensearchConfigDir, + final String adminUsername, + final String adminPassword) throws IOException { + final Path internalUsersFile = opensearchConfigDir.resolve("opensearch-security").resolve("internal_users.yml"); + + Objects.requireNonNull(localConfiguration.getRestApiUsername(), + "rest_api_username has to be configured the usage of secured Opensearch REST api" + ); + + Objects.requireNonNull(localConfiguration.getRestApiPassword(), + "rest_api_password has to be configured the usage of secured Opensearch REST api" + ); + + final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + final Map map = mapper.readValue(new FileInputStream(internalUsersFile.toFile()), TypeReferences.MAP_STRING_OBJECT); + final Map adminUserConfig = (Map) map.get("admin"); + + map.remove("admin"); + map.put(adminUsername, adminUserConfig); + + final BCryptPasswordAlgorithm passwordAlgorithm = new BCryptPasswordAlgorithm(12); + final String hashWithPrefix = passwordAlgorithm.hash(adminPassword); + + // remove the prefix and suffix, we need just the hash itself + final String hash = hashWithPrefix.substring("{bcrypt}".length(), hashWithPrefix.indexOf("{salt}")); + adminUserConfig.put("hash", hash); + + final FileOutputStream fos = new FileOutputStream(internalUsersFile.toFile()); + mapper.writeValue(fos, map); + } + + +} diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecureConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecureConfiguration.java index 8fe81bcf75..e07b0649af 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecureConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecureConfiguration.java @@ -16,24 +16,12 @@ */ package org.graylog.datanode.configuration.variants; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.google.common.collect.ImmutableMap; import org.graylog.datanode.Configuration; -import org.graylog2.jackson.TypeReferences; -import org.graylog2.security.hashing.BCryptPasswordAlgorithm; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; import java.nio.file.Path; -import java.util.Map; -import java.util.Objects; public sealed abstract class SecureConfiguration implements SecurityConfigurationVariant permits MongoCertSecureConfiguration, UploadedCertFilesSecureConfiguration { - public static final String SSL_PREFIX = "plugins.security.ssl."; - final Path datanodeConfigDir; final Path opensearchConfigDir; @@ -41,57 +29,4 @@ public sealed abstract class SecureConfiguration implements SecurityConfiguratio this.opensearchConfigDir = Path.of(localConfiguration.getOpensearchConfigLocation()).resolve("opensearch"); this.datanodeConfigDir = Path.of(localConfiguration.getConfigLocation()); } - - protected ImmutableMap commonSecureConfig(final Configuration localConfiguration) { - final ImmutableMap.Builder config = ImmutableMap.builder(); - config.putAll(commonConfig(localConfiguration)); - - config.put("plugins.security.disabled", "false"); - //config.put(SSL_PREFIX + "http.enabled", "true"); - config.put("plugins.security.allow_default_init_securityindex", "true"); - //config.put("plugins.security.authcz.admin_dn", "CN=kirk,OU=client,O=client,L=test,C=de"); - - config.put("plugins.security.audit.type", "internal_opensearch"); - config.put("plugins.security.enable_snapshot_restore_privilege", "true"); - config.put("plugins.security.check_snapshot_restore_write_privileges", "true"); - config.put("plugins.security.restapi.roles_enabled", "all_access,security_rest_api_access"); - config.put("plugins.security.system_indices.enabled", "true"); - config.put("plugins.security.system_indices.indices", ".plugins-ml-model,.plugins-ml-task,.opendistro-alerting-config,.opendistro-alerting-alert*,.opendistro-anomaly-results*,.opendistro-anomaly-detector*,.opendistro-anomaly-checkpoints,.opendistro-anomaly-detection-state,.opendistro-reports-*,.opensearch-notifications-*,.opensearch-notebooks,.opensearch-observability,.opendistro-asynchronous-search-response*,.replication-metadata-store"); - config.put("node.max_local_storage_nodes", "3"); - - return config.build(); - } - - protected void configureInitialAdmin(final Configuration localConfiguration, - final String adminUsername, - final String adminPassword) throws IOException { - final Path internalUsersFile = opensearchConfigDir.resolve("opensearch-security").resolve("internal_users.yml"); - - Objects.requireNonNull(localConfiguration.getRestApiUsername(), - "rest_api_username has to be configured the usage of secured Opensearch REST api" - ); - - Objects.requireNonNull(localConfiguration.getRestApiPassword(), - "rest_api_password has to be configured the usage of secured Opensearch REST api" - ); - - final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - final Map map = mapper.readValue(new FileInputStream(internalUsersFile.toFile()), TypeReferences.MAP_STRING_OBJECT); - final Map adminUserConfig = (Map) map.get("admin"); - - map.remove("admin"); - map.put(adminUsername, adminUserConfig); - - final BCryptPasswordAlgorithm passwordAlgorithm = new BCryptPasswordAlgorithm(12); - final String hashWithPrefix = passwordAlgorithm.hash(adminPassword); - - // remove the prefix and suffix, we need just the hash itself - final String hash = hashWithPrefix.substring("{bcrypt}".length(), hashWithPrefix.indexOf("{salt}")); - adminUserConfig.put("hash", hash); - - final FileOutputStream fos = new FileOutputStream(internalUsersFile.toFile()); - mapper.writeValue(fos, map); - } - - } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecurityConfigurationVariant.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecurityConfigurationVariant.java index 8eea934564..7d10291f2f 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecurityConfigurationVariant.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/SecurityConfigurationVariant.java @@ -16,38 +16,15 @@ */ package org.graylog.datanode.configuration.variants; -import com.google.common.collect.ImmutableMap; import org.graylog.datanode.Configuration; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; import java.io.IOException; -import java.nio.file.Path; import java.security.GeneralSecurityException; -import java.util.Map; -import java.util.Objects; public interface SecurityConfigurationVariant { boolean checkPrerequisites(final Configuration localConfiguration); - Map configure(final Configuration localConfiguration) throws KeyStoreStorageException, IOException, GeneralSecurityException; - - default ImmutableMap commonConfig(final Configuration localConfiguration) { - final ImmutableMap.Builder config = ImmutableMap.builder(); - Objects.requireNonNull(localConfiguration.getConfigLocation(), "config_location setting is required!"); - localConfiguration.getOpensearchNetworkHostHost().ifPresent( - networkHost -> config.put("network.host", networkHost)); - config.put("path.data", Path.of(localConfiguration.getOpensearchDataLocation()).resolve(localConfiguration.getDatanodeNodeName()).toAbsolutePath().toString()); - config.put("path.logs", Path.of(localConfiguration.getOpensearchLogsLocation()).resolve(localConfiguration.getDatanodeNodeName()).toAbsolutePath().toString()); - if (localConfiguration.isSingleNodeOnly()) { - config.put("discovery.type", "single-node"); - } else { - config.put("cluster.initial_master_nodes", "node1"); - } - - // listen on all interfaces - config.put("network.bind_host", "0.0.0.0"); - - return config.build(); - } + OpensearchSecurityConfiguration build() throws KeyStoreStorageException, IOException, GeneralSecurityException; } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/variants/UploadedCertFilesSecureConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/variants/UploadedCertFilesSecureConfiguration.java index 1ee04be96f..8d125ac11b 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/variants/UploadedCertFilesSecureConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/variants/UploadedCertFilesSecureConfiguration.java @@ -16,19 +16,13 @@ */ package org.graylog.datanode.configuration.variants; -import com.google.common.collect.ImmutableMap; import org.graylog.datanode.Configuration; import org.graylog.datanode.configuration.OpensearchConfigurationException; -import org.graylog.datanode.configuration.RootCertificateFinder; -import org.graylog.datanode.configuration.TlsConfigurationSupplier; -import org.graylog.datanode.configuration.TruststoreCreator; -import org.graylog.datanode.configuration.certificates.CertificateMetaData; import org.graylog.datanode.configuration.certificates.KeystoreReEncryption; import org.graylog.datanode.configuration.verification.ConfigProperty; import org.graylog.datanode.configuration.verification.ConfigSectionCompleteness; import org.graylog.datanode.configuration.verification.ConfigSectionCompletenessVerifier; import org.graylog.datanode.configuration.verification.ConfigSectionRequirements; -import org.graylog.security.certutil.CertConstants; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; @@ -36,16 +30,11 @@ import javax.inject.Inject; import java.io.IOException; import java.nio.file.Path; import java.security.GeneralSecurityException; -import java.security.cert.X509Certificate; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.UUID; import static org.graylog.datanode.Configuration.HTTP_CERTIFICATE_PASSWORD_PROPERTY; import static org.graylog.datanode.Configuration.TRANSPORT_CERTIFICATE_PASSWORD_PROPERTY; -import static org.graylog.datanode.configuration.TlsConfigurationSupplier.TRUSTSTORE_FILENAME; public final class UploadedCertFilesSecureConfiguration extends SecureConfiguration { @@ -56,23 +45,14 @@ public final class UploadedCertFilesSecureConfiguration extends SecureConfigurat private final String datanodeTransportCertificatePassword; private final String datanodeHttpCertificatePassword; private final KeystoreReEncryption keystoreReEncryption; - private final TlsConfigurationSupplier tlsConfigurationSupplier; - private final TruststoreCreator truststoreCreator; - private final RootCertificateFinder rootCertificateFinder; private final ConfigSectionCompletenessVerifier configSectionCompletenessVerifier; @Inject public UploadedCertFilesSecureConfiguration(final Configuration localConfiguration, final KeystoreReEncryption keystoreReEncryption, - final TlsConfigurationSupplier tlsConfigurationSupplier, - final TruststoreCreator truststoreCreator, - final RootCertificateFinder rootCertificateFinder, final ConfigSectionCompletenessVerifier configSectionCompletenessVerifier) { super(localConfiguration); this.keystoreReEncryption = keystoreReEncryption; - this.tlsConfigurationSupplier = tlsConfigurationSupplier; - this.truststoreCreator = truststoreCreator; - this.rootCertificateFinder = rootCertificateFinder; this.configSectionCompletenessVerifier = configSectionCompletenessVerifier; this.uploadedTransportKeystorePath = datanodeConfigDir .resolve(localConfiguration.getDatanodeTransportCertificate()); @@ -108,51 +88,19 @@ public final class UploadedCertFilesSecureConfiguration extends SecureConfigurat } @Override - public Map configure(Configuration localConfiguration) throws KeyStoreStorageException, IOException, GeneralSecurityException { - final ImmutableMap.Builder config = ImmutableMap.builder(); - config.putAll(commonSecureConfig(localConfiguration)); - Map rootCerts = new HashMap<>(); - final String truststorePassword = UUID.randomUUID().toString(); + public OpensearchSecurityConfiguration build() throws KeyStoreStorageException, IOException, GeneralSecurityException { + final char[] transportOTP = keystoreReEncryption.reEncyptWithOtp(new KeystoreFileLocation(uploadedTransportKeystorePath), datanodeTransportCertificatePassword.toCharArray(), finalTransportKeystoreLocation); + final char[] httpOTP = keystoreReEncryption.reEncyptWithOtp(new KeystoreFileLocation(uploadedHttpKeystorePath), datanodeHttpCertificatePassword.toCharArray(), finalHttpKeystoreLocation); - configureInitialAdmin(localConfiguration, localConfiguration.getRestApiUsername(), localConfiguration.getRestApiPassword()); - - rootCerts.put("transport-chain-CA-root", rootCertificateFinder.findRootCert( - finalTransportKeystoreLocation.keystorePath(), - transportOTP, - CertConstants.DATANODE_KEY_ALIAS)); - config.putAll(tlsConfigurationSupplier.getTransportTlsConfig(new CertificateMetaData( - localConfiguration.getDatanodeTransportCertificate(), - transportOTP - ) - )); - - rootCerts.put("http-chain-CA-root", rootCertificateFinder.findRootCert( - finalHttpKeystoreLocation.keystorePath(), - httpOTP, - CertConstants.DATANODE_KEY_ALIAS)); - config.putAll(tlsConfigurationSupplier.getHttpTlsConfig(new CertificateMetaData( - localConfiguration.getDatanodeHttpCertificate(), - httpOTP - ) - )); - - if (!rootCerts.isEmpty()) { - final Path trustStorePath = opensearchConfigDir.resolve(TRUSTSTORE_FILENAME); - truststoreCreator.createTruststore(rootCerts, - truststorePassword.toCharArray(), - trustStorePath - ); - System.setProperty("javax.net.ssl.trustStore", trustStorePath.toAbsolutePath().toString()); - System.setProperty("javax.net.ssl.trustStorePassword", truststorePassword); - config.putAll(tlsConfigurationSupplier.getTrustStoreTlsConfig(truststorePassword)); - } - - return config.build(); + return new OpensearchSecurityConfiguration( + new KeystoreInformation(finalTransportKeystoreLocation.keystorePath().toAbsolutePath(), transportOTP), + new KeystoreInformation(finalHttpKeystoreLocation.keystorePath().toAbsolutePath(), httpOTP) + ); } } diff --git a/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java b/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java index 3f5ac4c90d..fc34df37cf 100644 --- a/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java +++ b/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java @@ -21,7 +21,6 @@ import com.codahale.metrics.MetricRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; @@ -39,10 +38,11 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.ServerProperties; import org.glassfish.jersey.server.model.Resource; import org.graylog.datanode.Configuration; +import org.graylog.datanode.configuration.variants.KeystoreInformation; +import org.graylog.datanode.configuration.variants.OpensearchSecurityConfiguration; import org.graylog.datanode.management.OpensearchConfigurationChangeEvent; import org.graylog.datanode.process.OpensearchConfiguration; import org.graylog.security.certutil.CertConstants; -import org.graylog2.configuration.HttpConfiguration; import org.graylog2.configuration.TLSProtocolsConfiguration; import org.graylog2.plugin.inject.Graylog2Module; import org.graylog2.rest.MoreMediaTypes; @@ -62,7 +62,6 @@ import javax.ws.rs.ext.ExceptionMapper; import java.io.IOException; import java.net.URI; import java.nio.file.Files; -import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.util.Map; @@ -70,7 +69,6 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; -import java.util.stream.Collectors; import static com.codahale.metrics.MetricRegistry.name; import static com.google.common.base.MoreObjects.firstNonNull; @@ -78,21 +76,16 @@ import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.Objects.requireNonNull; public class JerseyService extends AbstractIdleService { - public static final String PLUGIN_PREFIX = "/plugins"; private static final Logger LOG = LoggerFactory.getLogger(JerseyService.class); private static final String RESOURCE_PACKAGE_WEB = "org.graylog2.web.resources"; private final Configuration configuration; private final Set> systemRestResources; -// private final Map>> pluginRestResources; private final Set> dynamicFeatures; -// private final Set> containerResponseFilters; private final Set> exceptionMappers; -// private final Set additionalComponents; private final ObjectMapper objectMapper; private final MetricRegistry metricRegistry; - // private final ErrorPageGenerator errorPageGenerator; private final TLSProtocolsConfiguration tlsConfiguration; private HttpServer apiHttpServer = null; @@ -100,25 +93,17 @@ public class JerseyService extends AbstractIdleService { @Inject public JerseyService(final Configuration configuration, Set> dynamicFeatures, -// Set> containerResponseFilters, Set> exceptionMappers, -// @Named("additionalJerseyComponents") final Set additionalComponents, @Named(Graylog2Module.SYSTEM_REST_RESOURCES) final Set> systemRestResources, - // final Map>> pluginRestResources, ObjectMapper objectMapper, MetricRegistry metricRegistry, -// ErrorPageGenerator errorPageGenerator, - TLSProtocolsConfiguration tlsConfiguration, EventBus eventBus) { + TLSProtocolsConfiguration tlsConfiguration, EventBus eventBus) { this.configuration = requireNonNull(configuration, "configuration"); this.dynamicFeatures = requireNonNull(dynamicFeatures, "dynamicFeatures"); - // this.containerResponseFilters = requireNonNull(containerResponseFilters, "containerResponseFilters"); this.exceptionMappers = requireNonNull(exceptionMappers, "exceptionMappers"); -// this.additionalComponents = requireNonNull(additionalComponents, "additionalComponents"); this.systemRestResources = systemRestResources; -// this.pluginRestResources = requireNonNull(pluginRestResources, "pluginResources"); this.objectMapper = requireNonNull(objectMapper, "objectMapper"); this.metricRegistry = requireNonNull(metricRegistry, "metricRegistry"); -// this.errorPageGenerator = requireNonNull(errorPageGenerator, "errorPageGenerator"); this.tlsConfiguration = requireNonNull(tlsConfiguration); eventBus.register(this); } @@ -134,15 +119,10 @@ public class JerseyService extends AbstractIdleService { * TODO: replace this map magic with proper types in OpensearchConfiguration */ private SSLEngineConfigurator extractSslConfiguration(OpensearchConfiguration config) throws GeneralSecurityException, IOException { - final Map cfgMap = config.asMap(); - if(!Boolean.parseBoolean(cfgMap.get("plugins.security.disabled"))) { + final OpensearchSecurityConfiguration securityConfiguration = config.opensearchSecurityConfiguration(); + if (securityConfiguration.securityEnabled()) { // caution, this path is relative to the opensearch config directory! - - final String keystore = cfgMap.get("plugins.security.ssl.http.keystore_filepath"); - final Path keystorePath = Path.of(configuration.getOpensearchConfigLocation()).resolve("opensearch").resolve(keystore); - final String keystoreOTP = cfgMap.get("plugins.security.ssl.http.keystore_password"); - - return buildSslEngineConfigurator(keystorePath, keystoreOTP); + return buildSslEngineConfigurator(securityConfiguration.getHttpCertificate()); } else { return null; } @@ -151,7 +131,7 @@ public class JerseyService extends AbstractIdleService { @Override protected void startUp() { - // do nothing, the actual startup will be triggered in the moment opensearch configuration is available + // do nothing, the actual startup will be triggered at the moment opensearch configuration is available } private void doStartup(SSLEngineConfigurator sslEngineConfigurator) throws Exception { @@ -193,65 +173,23 @@ public class JerseyService extends AbstractIdleService { configuration.getHttpSelectorRunnersCount(), configuration.getHttpMaxHeaderSize(), configuration.isHttpEnableGzip(), - configuration.isHttpEnableCors(), Set.of()); - // pluginResources); apiHttpServer.start(); LOG.info("Started REST API at <{}>", configuration.getHttpBindAddress()); } - private Set prefixResources(String prefix, Set> resources) { - final String pathPrefix = prefix.endsWith("/") ? prefix.substring(0, prefix.length() - 1) : prefix; - - return resources - .stream() - .map(resource -> { - final javax.ws.rs.Path pathAnnotation = Resource.getPath(resource); - final String resourcePathSuffix = Strings.nullToEmpty(pathAnnotation.value()); - final String resourcePath = resourcePathSuffix.startsWith("/") ? pathPrefix + resourcePathSuffix : pathPrefix + "/" + resourcePathSuffix; - - return Resource - .builder(resource) - .path(resourcePath) - .build(); - }) - .collect(Collectors.toSet()); - } - - - private ResourceConfig buildResourceConfig(final boolean enableCors, - final Set additionalResources) { - final Map packagePrefixes = ImmutableMap.of( - RESOURCE_PACKAGE_WEB, HttpConfiguration.PATH_WEB, - "", HttpConfiguration.PATH_API - ); - + private ResourceConfig buildResourceConfig(final Set additionalResources) { final ResourceConfig rc = new ResourceConfig() .property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true) .property(ServerProperties.WADL_FEATURE_DISABLE, true) .property(ServerProperties.MEDIA_TYPE_MAPPINGS, mediaTypeMappings()) -// .register(new PrefixAddingModelProcessor(packagePrefixes, graylogConfiguration)) .registerClasses( -/* ShiroSecurityContextFilter.class, - ShiroRequestHeadersBinder.class, - VerboseCsrfProtectionFilter.class, */ JacksonJaxbJsonProvider.class, JsonProcessingExceptionMapper.class, JsonMappingExceptionMapper.class, - JacksonPropertyExceptionMapper.class -/* AnyExceptionClassMapper.class, - MissingStreamPermissionExceptionMapper.class, - WebApplicationExceptionMapper.class, - BadRequestExceptionMapper.class, - RestAccessLogFilter.class, - NodeIdResponseFilter.class, - RequestIdFilter.class, - XHRFilter.class, - NotAuthorizedResponseFilter.class, - WebAppNotFoundResponseFilter.class, - EmbeddingControlFilter.class */) + JacksonPropertyExceptionMapper.class) // Replacing this with a lambda leads to missing subtypes - https://github.com/Graylog2/graylog2-server/pull/10617#discussion_r630236360 .register(new ContextResolver() { @Override @@ -259,24 +197,12 @@ public class JerseyService extends AbstractIdleService { return objectMapper; } }) -// .register(new UserContextBinder()) .register(MultiPartFeature.class) .registerClasses(systemRestResources) .registerResources(additionalResources); exceptionMappers.forEach(rc::registerClasses); dynamicFeatures.forEach(rc::registerClasses); -// containerResponseFilters.forEach(rc::registerClasses); -// additionalComponents.forEach(rc::registerClasses); - - if (enableCors) { - LOG.info("Enabling CORS for HTTP endpoint"); -// rc.registerClasses(CORSFilter.class); - } - - if (LOG.isDebugEnabled()) { -// rc.registerClasses(PrintModelProcessor.class); - } return rc; } @@ -296,9 +222,8 @@ public class JerseyService extends AbstractIdleService { int selectorRunnersCount, int maxHeaderSize, boolean enableGzip, - boolean enableCors, Set additionalResources) { - final ResourceConfig resourceConfig = buildResourceConfig(enableCors, additionalResources); + final ResourceConfig resourceConfig = buildResourceConfig(additionalResources); final HttpServer httpServer = GrizzlyHttpServerFactory.createHttpServer( listenUri, resourceConfig, @@ -320,8 +245,6 @@ public class JerseyService extends AbstractIdleService { // See "Selector runners count" at https://grizzly.java.net/bestpractices.html for details. listener.getTransport().setSelectorRunnersCount(selectorRunnersCount); -// listener.setDefaultErrorPageGenerator(errorPageGenerator); - if (enableGzip) { final CompressionConfig compressionConfig = listener.getCompressionConfig(); compressionConfig.setCompressionMode(CompressionConfig.CompressionMode.ON); @@ -331,17 +254,17 @@ public class JerseyService extends AbstractIdleService { return httpServer; } - private SSLEngineConfigurator buildSslEngineConfigurator(Path keystoreFile, String keyPassword) + private SSLEngineConfigurator buildSslEngineConfigurator(KeystoreInformation keystoreInformation) throws GeneralSecurityException, IOException { - if (keystoreFile == null || !Files.isRegularFile(keystoreFile) || !Files.isReadable(keystoreFile)) { - throw new IllegalArgumentException("Unreadable or missing private key: " + keystoreFile); + if (keystoreInformation == null || !Files.isRegularFile(keystoreInformation.location()) || !Files.isReadable(keystoreInformation.location())) { + throw new IllegalArgumentException("Unreadable to read private key"); } final SSLContextConfigurator sslContextConfigurator = new SSLContextConfigurator(); - final char[] password = firstNonNull(keyPassword, "").toCharArray(); + final char[] password = firstNonNull(keystoreInformation.passwordAsString(), "").toCharArray(); - final KeyStore keyStore = readKeystore(keystoreFile, password); + final KeyStore keyStore = readKeystore(keystoreInformation); sslContextConfigurator.setKeyStorePass(password); sslContextConfigurator.setKeyStoreBytes(KeyStoreUtils.getBytes(keyStore, password)); @@ -352,10 +275,10 @@ public class JerseyService extends AbstractIdleService { return sslEngineConfigurator; } - private static KeyStore readKeystore(Path keystoreFile, char[] password) { - try (var in = Files.newInputStream(keystoreFile)) { + private static KeyStore readKeystore(KeystoreInformation keystoreInformation) { + try (var in = Files.newInputStream(keystoreInformation.location())) { KeyStore caKeystore = KeyStore.getInstance(CertConstants.PKCS12); - caKeystore.load(in, password); + caKeystore.load(in, keystoreInformation.password()); return caKeystore; } catch (IOException | GeneralSecurityException ex) { throw new RuntimeException("Could not read keystore: " + ex.getMessage(), ex); @@ -375,8 +298,4 @@ public class JerseyService extends AbstractIdleService { metricRegistry, name(JerseyService.class, executorName)); } - - public void restartWithConfig(OpensearchConfiguration config) { - - } } 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 50590795f9..ee22fb2cdc 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 @@ -17,6 +17,7 @@ package org.graylog.datanode.process; import org.apache.commons.exec.OS; +import org.graylog.datanode.configuration.variants.OpensearchSecurityConfiguration; import org.graylog.datanode.management.Environment; import org.graylog.shaded.opensearch2.org.apache.http.HttpHost; @@ -35,7 +36,7 @@ public record OpensearchConfiguration( String authUsername, String authPassword, String clusterName, String nodeName, List nodeRoles, - List discoverySeedHosts, Map additionalConfiguration + List discoverySeedHosts, OpensearchSecurityConfiguration opensearchSecurityConfiguration, Map additionalConfiguration ) { public Map asMap() { diff --git a/data-node/src/test/java/org/graylog/datanode/configuration/TruststoreCreatorTest.java b/data-node/src/test/java/org/graylog/datanode/configuration/TruststoreCreatorTest.java index a4b2d183a0..3c3acd67b3 100644 --- a/data-node/src/test/java/org/graylog/datanode/configuration/TruststoreCreatorTest.java +++ b/data-node/src/test/java/org/graylog/datanode/configuration/TruststoreCreatorTest.java @@ -17,6 +17,7 @@ package org.graylog.datanode.configuration; import com.google.common.collect.ImmutableList; +import org.apache.commons.lang3.RandomStringUtils; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; @@ -24,13 +25,16 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.graylog.datanode.configuration.variants.KeystoreInformation; +import org.graylog.security.certutil.CertConstants; import org.graylog.security.certutil.keystore.storage.KeystoreContentMover; import org.graylog.security.certutil.keystore.storage.KeystoreFileStorage; import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.io.FileOutputStream; +import java.io.IOException; import java.math.BigInteger; import java.nio.file.Path; import java.security.GeneralSecurityException; @@ -41,7 +45,6 @@ import java.security.cert.X509Certificate; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; -import java.util.Map; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -51,33 +54,23 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; - public class TruststoreCreatorTest { - private TruststoreCreator toTest; - - @BeforeEach - void setUp() { - toTest = new TruststoreCreator(); - } - @Test void testTrustStoreCreation(@TempDir Path tempDir) throws Exception { - final Path trustStoreFile = tempDir.resolve("truststore.sec"); - final char[] truststorePassword = "caramba! caramba!".toCharArray(); - toTest.createTruststore( - Map.of( - "root", prepareSimpleCert("CN=ROOT", BigInteger.ONE), - "boot", prepareSimpleCert("CN=BOOT", BigInteger.TWO) - ), - truststorePassword, - trustStoreFile); + final KeystoreInformation root = createKeystore(tempDir.resolve("root.p12"), "root","CN=ROOT", BigInteger.ONE); + final KeystoreInformation boot = createKeystore(tempDir.resolve("boot.p12"), "boot","CN=BOOT", BigInteger.TWO); - assertTrue(trustStoreFile.toFile().exists()); + final KeystoreInformation truststore = TruststoreCreator.newTruststore() + .addRootCert("root", root, "root") + .addRootCert("boot", boot, "boot") + .persist(tempDir.resolve("truststore.sec"), "caramba! caramba!".toCharArray()); + + assertTrue(truststore.location().toFile().exists()); KeystoreFileStorage keystoreFileStorage = new KeystoreFileStorage(mock(KeystoreContentMover.class)); - final Optional keyStoreOptional = keystoreFileStorage.readKeyStore(new KeystoreFileLocation(trustStoreFile), truststorePassword); + final Optional keyStoreOptional = keystoreFileStorage.readKeyStore(new KeystoreFileLocation(truststore.location()), truststore.password()); assertTrue(keyStoreOptional.isPresent()); @@ -102,7 +95,7 @@ public class TruststoreCreatorTest { assertEquals(cnName, x509Certificate.getIssuerX500Principal().getName()); } - private X509Certificate prepareSimpleCert(final String cnName, final BigInteger serialNumber) throws GeneralSecurityException, OperatorCreationException { + private KeystoreInformation createKeystore(Path path, String alias, final String cnName, final BigInteger serialNumber) throws GeneralSecurityException, OperatorCreationException, IOException { KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM); java.security.KeyPair certKeyPair = keyGen.generateKeyPair(); X500Name name = new X500Name(cnName); @@ -117,8 +110,20 @@ public class TruststoreCreatorTest { ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).build(certKeyPair.getPrivate()); X509CertificateHolder certHolder = builder.build(signer); - return new JcaX509CertificateConverter().getCertificate(certHolder); + final X509Certificate signedCert = new JcaX509CertificateConverter().getCertificate(certHolder); + + KeyStore trustStore = KeyStore.getInstance(CertConstants.PKCS12); + trustStore.load(null, null); + + final char[] password = RandomStringUtils.randomAlphabetic(256).toCharArray(); + + trustStore.setKeyEntry(alias, certKeyPair.getPrivate(), password, new Certificate[]{signedCert}); + + + try (final FileOutputStream fileOutputStream = new FileOutputStream(path.toFile())) { + trustStore.store(fileOutputStream, password); + } + return new KeystoreInformation(path, password); } - } 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 4073f97bd3..8873419025 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 @@ -41,7 +41,9 @@ import org.slf4j.LoggerFactory; import java.io.FileInputStream; import java.io.IOException; +import java.net.InetAddress; import java.net.SocketException; +import java.net.UnknownHostException; import java.nio.file.Path; import java.security.KeyStore; import java.security.KeyStoreException; @@ -65,7 +67,7 @@ public class DatanodeSecuritySetupIT { private Path httpCert; @BeforeEach - void setUp() { + void setUp() throws UnknownHostException { String containerHostname = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef"); // first generate a self-signed CA