Skip to content

Commit

Permalink
Merge branch 'master' into close-investigation-drawer-in-security-per…
Browse files Browse the repository at this point in the history
…spective
  • Loading branch information
simonychuang authored Jul 15, 2024
2 parents a37f057 + 096e903 commit b830ebb
Show file tree
Hide file tree
Showing 23 changed files with 770 additions and 223 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/pr-19463.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type="a"
message="Add option to select all fields for messages export widget"

pulls = ["19463"]
issues=["Graylog2/graylog-plugin-enterprise#5616"]
4 changes: 4 additions & 0 deletions changelog/unreleased/pr-19879.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type = "c"
message = "Run remote reindex connection checks from datanodes, aggregate results."

pulls = ["19879"]
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.common.util.concurrent.Service;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
import org.graylog.datanode.configuration.DatanodeTrustManagerProvider;
import org.graylog.datanode.configuration.OpensearchConfigurationService;
import org.graylog.datanode.metrics.ConfigureMetricsIndexSettings;
import org.graylog.datanode.opensearch.OpensearchProcess;
Expand All @@ -45,6 +46,8 @@ protected void configure() {
serviceBinder.addBinding().to(OpensearchConfigurationService.class).asEagerSingleton();
serviceBinder.addBinding().to(OpensearchProcessService.class).asEagerSingleton();

bind(DatanodeTrustManagerProvider.class);

// tracer
Multibinder<StateMachineTracer> tracerBinder = Multibinder.newSetBinder(binder(), StateMachineTracer.class);
tracerBinder.addBinding().to(ClusterNodeStateTracer.class).asEagerSingleton();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.datanode.configuration;

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.graylog.datanode.configuration.variants.OpensearchSecurityConfiguration;
import org.graylog.datanode.opensearch.OpensearchConfigurationChangeEvent;
import org.graylog2.security.CustomCAX509TrustManager;
import org.graylog2.security.TrustManagerAggregator;

import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.Optional;

public class DatanodeTrustManagerProvider implements Provider<X509TrustManager> {

private final CustomCAX509TrustManager customCAX509TrustManager;
private volatile KeyStore datanodeTruststore;

@Inject
public DatanodeTrustManagerProvider(CustomCAX509TrustManager CustomCAX509TrustManager, EventBus eventBus) {
customCAX509TrustManager = CustomCAX509TrustManager;
eventBus.register(this);
}

@Subscribe
public void onOpensearchConfigurationChange(OpensearchConfigurationChangeEvent e) {
Optional.ofNullable(e.config().opensearchSecurityConfiguration())
.flatMap(OpensearchSecurityConfiguration::getTruststore)
.map(t -> {
try {
return t.loadKeystore();
} catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
})
.ifPresent(this::setTruststore);
}

private void setTruststore(KeyStore keyStore) {
this.datanodeTruststore = keyStore;
}


@Override
public X509TrustManager get() {
final X509TrustManager datanodeTrustManager = TrustManagerAggregator.trustManagerFromKeystore(this.datanodeTruststore);
return new TrustManagerAggregator(List.of(datanodeTrustManager, customCAX509TrustManager));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.datanode.rest;

import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.graylog.datanode.configuration.DatanodeTrustManagerProvider;
import org.graylog.storage.opensearch2.ConnectionCheckRequest;
import org.graylog.storage.opensearch2.ConnectionCheckResponse;
import org.graylog2.security.TrustAllX509TrustManager;
import org.graylog2.security.untrusted.UntrustedCertificateExtractor;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

@Path("/connection-check")
@Produces(MediaType.APPLICATION_JSON)
public class OpensearchConnectionCheckController {

public static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10);
public static final Duration WRITE_TIMEOUT = Duration.ofSeconds(10);
public static final Duration READ_TIMEOUT = Duration.ofSeconds(10);
private final DatanodeTrustManagerProvider datanodeTrustManagerProvider;

private final OkHttpClient httpClient;

@Inject
public OpensearchConnectionCheckController(DatanodeTrustManagerProvider datanodeTrustManagerProvider) {
this.datanodeTrustManagerProvider = datanodeTrustManagerProvider;
this.httpClient = new OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.connectTimeout(CONNECT_TIMEOUT)
.writeTimeout(WRITE_TIMEOUT)
.readTimeout(READ_TIMEOUT)
.build();
}

@POST
@Path("opensearch")
public ConnectionCheckResponse status(ConnectionCheckRequest request) {
final List<X509Certificate> unknownCertificates = new LinkedList<>();
try {
unknownCertificates.addAll(extractUnknownCertificates(request.host()));
final List<String> indices = getAllIndicesFrom(request.host(), request.username(), request.password(), request.trustUnknownCerts());
return ConnectionCheckResponse.success(indices, unknownCertificates);
} catch (Exception e) {
return ConnectionCheckResponse.error(e, unknownCertificates);
}
}

List<String> getAllIndicesFrom(final String host, final String username, final String password, boolean trustUnknownCerts) {
var url = (host.endsWith("/") ? host : host + "/") + "_cat/indices?h=index";
try (var response = getClient(trustUnknownCerts).newCall(new Request.Builder().url(url).header("Authorization", Credentials.basic(username, password)).build()).execute()) {
if (response.isSuccessful() && response.body() != null) {
// filtering all indices that start with "." as they indicate a system index - we don't want to reindex those
return new BufferedReader(new StringReader(response.body().string())).lines().filter(i -> !i.startsWith(".")).sorted().toList();
} else {
String message = String.format(Locale.ROOT, "Could not read list of indices from %s. Code=%d, message=%s", host, response.code(), response.message());
throw new RuntimeException(message);
}
} catch (IOException e) {
throw new RuntimeException("Could not read list of indices from " + host + ", " + e.getMessage(), e);
}
}


private OkHttpClient getClient(boolean trustUnknownCerts) {
try {
final SSLContext ctx = SSLContext.getInstance("TLS");
final X509TrustManager trustManager = getTrustManager(trustUnknownCerts);
ctx.init(null, new TrustManager[]{trustManager}, new SecureRandom());
return httpClient.newBuilder().sslSocketFactory(ctx.getSocketFactory(), trustManager).build();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);

}
}

@Nonnull
private X509TrustManager getTrustManager(boolean trustUnknownCerts) {
if (trustUnknownCerts) {
return new TrustAllX509TrustManager();
} else {
return datanodeTrustManagerProvider.get();
}
}

@Nonnull
private List<X509Certificate> extractUnknownCertificates(String host) {
final UntrustedCertificateExtractor extractor = new UntrustedCertificateExtractor(httpClient);
try {
return extractor.extractUntrustedCerts(host);
} catch (NoSuchAlgorithmException | IOException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ protected void configure() {
addSystemRestResource(LogsController.class);
addSystemRestResource(ManagementController.class);
addSystemRestResource(IndicesDirectoryController.class);
addSystemRestResource(OpensearchConnectionCheckController.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.storage.opensearch2;

import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.openssl.PEMParser;

import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.StringReader;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

public record AggregatedConnectionResponse(Map<String, ConnectionCheckResponse> responses) {
public List<String> indices() {
return responses.values().stream().flatMap(v -> v.indices().stream()).sorted(Comparator.naturalOrder()).collect(Collectors.toList());
}

public String error() {

final String errorMessage = responses.entrySet().stream().filter(e -> e.getValue().error() != null).map(e -> e.getKey() + ": " + e.getValue().error()).collect(Collectors.joining(";"));

if (errorMessage.isEmpty()) {
return null;
}

final StringBuilder errorBuilder = new StringBuilder();
errorBuilder.append(errorMessage);

if (!certificates().isEmpty()) {
errorBuilder.append("\n").append("Unknown certificates: \n").append(certificates().stream().map(AggregatedConnectionResponse::decode).map(this::info).collect(Collectors.joining("\n\n")));
}
return errorBuilder.toString();
}

private String info(X509Certificate certificate) {
return """
Issued to: %s,
Issued by: %s,
Serial number: %s,
Issued on: %s,
Expires on: %s,
SHA-256 fingerprint: %s,
SHA-1 Fingerprint: %s
""".formatted(
certificate.getSubjectX500Principal().getName(),
certificate.getIssuerX500Principal().getName(),
certificate.getSerialNumber(),
certificate.getNotBefore(),
certificate.getNotAfter(),
getfingerprint(certificate, "SHA-256"),
getfingerprint(certificate, "SHA-1")
);
}

public List<String> certificates() {
return responses.values().stream()
.filter(v -> v.certificates() != null)
.flatMap(v -> v.certificates().stream()).distinct().collect(Collectors.toList());
}

private static X509Certificate decode(String pemEncodedCert) {
final PEMParser pemParser = new PEMParser(new StringReader(pemEncodedCert));
try {
Object parsed = pemParser.readObject();
if (parsed instanceof X509CertificateHolder certificate) {
return new JcaX509CertificateConverter().getCertificate(certificate);
} else {
throw new IllegalArgumentException("Couldn't parse x509 certificate from provided string, unknown type");
}
} catch (IOException | CertificateException e) {
throw new RuntimeException(e);
}
}

private static String getfingerprint(X509Certificate cert, String type) {
try {
MessageDigest md = MessageDigest.getInstance(type);
byte[] der = cert.getEncoded();
md.update(der);
byte[] digest = md.digest();
String digestHex = DatatypeConverter.printHexBinary(digest);
return digestHex.toLowerCase(Locale.ROOT);
} catch (CertificateEncodingException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.storage.opensearch2;

import java.net.URI;

public record ConnectionCheckRequest(String host, String username, String password, boolean trustUnknownCerts) {
}
Loading

0 comments on commit b830ebb

Please sign in to comment.