mirror of
https://github.com/Graylog2/graylog2-server.git
synced 2026-03-13 09:32:21 +08:00
DataNode: using JWT for authentication/authorisation (#16334)
* jwt implementation, adding preflight + versionprobe * adding JWT auth to anomaly detection client, some minor refactorings * added missing license * added provider, refactorings * fixing small bugs * set smaller default expiration * using the correct OpenSearch distro for the erchitecture * added config options * small refactorings, fixing tests, adding dependencies for running tests from inside IntelliJ * fixing import to correct Duration class * removing unnecessary debug output * fixing tests * fixing tests * adding JWT to Cluster test * fixing path * fixing path problems * fix architecture logic for DatanodeContainerizedBackend * fixing path problems * fixing JWT settings in tests * fixing JWT settings in tests * fixing cluster test * fixing cluster test * fixing cluster test * adding missing jwt token * small refactoring to force thread-safety on creation * the datanode uses mongodb - so the closing sequence needs to be adjusted * moving config changes into config file, keep only change that has to be done by code * adding breaking change info for JWT and clock synchronization. * renaming config settings * renaming config settings * removed debug settings that are no longer in use * renamed provider/annotation to be more precise * lowering log level * smaller refactorings/renamings * keep config change in code until PR is merged, remove code in a follow-up PR * moved changelog file to correct folder * clock synchronsation Co-authored-by: Dennis Oelkers <dennis@graylog.com> * added jwt auth into changes after merge * upped the timeout * log curl connection information in DatanodeOpensearchWait * replacing with jwt auth * changing wait strategy * fixing imports * remove uname/pwd from opensearch rest base url * propagate jwt token provider to http rest client callback, obtain fresh token every time * inject IndexerJwtAuthTokenProvider in DatanodeConfigurationProvider * remove rest uname/pwd from datanode configuration --------- Co-authored-by: Tomas Dvorak <tomas.dvorak@graylog.com> Co-authored-by: Dennis Oelkers <dennis@graylog.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import com.github.joschi.jadconfig.Validator;
|
||||
import com.github.joschi.jadconfig.ValidatorMethod;
|
||||
import com.github.joschi.jadconfig.converters.IntegerConverter;
|
||||
import com.github.joschi.jadconfig.converters.StringListConverter;
|
||||
import com.github.joschi.jadconfig.util.Duration;
|
||||
import com.github.joschi.jadconfig.validators.PositiveIntegerValidator;
|
||||
import com.github.joschi.jadconfig.validators.StringNotBlankValidator;
|
||||
import com.github.joschi.jadconfig.validators.URIAbsoluteValidator;
|
||||
@@ -190,12 +191,27 @@ public class Configuration extends BaseConfiguration {
|
||||
return opensearchProcessLogsBufferSize;
|
||||
}
|
||||
|
||||
@Parameter(value = "rest_api_username")
|
||||
private String restApiUsername;
|
||||
|
||||
@Parameter(value = "password_secret", required = true, validators = StringNotBlankValidator.class)
|
||||
private String passwordSecret;
|
||||
|
||||
public String getPasswordSecret() {
|
||||
return passwordSecret;
|
||||
}
|
||||
|
||||
@Parameter(value = "indexer_jwt_auth_token_caching_duration")
|
||||
Duration indexerJwtAuthTokenCachingDuration = Duration.seconds(60);
|
||||
|
||||
public Duration getIndexerJwtAuthTokenCachingDuration() {
|
||||
return indexerJwtAuthTokenCachingDuration;
|
||||
}
|
||||
|
||||
@Parameter(value = "indexer_jwt_auth_token_expiration_duration")
|
||||
Duration indexerJwtAuthTokenExpirationDuration = Duration.seconds(180);
|
||||
|
||||
public Duration getIndexerJwtAuthTokenExpirationDuration() {
|
||||
return indexerJwtAuthTokenExpirationDuration;
|
||||
}
|
||||
|
||||
@ValidatorMethod
|
||||
@SuppressWarnings("unused")
|
||||
public void validatePasswordSecret() throws ValidationException {
|
||||
@@ -204,9 +220,6 @@ public class Configuration extends BaseConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
@Parameter(value = "rest_api_password")
|
||||
private String restApiPassword;
|
||||
|
||||
@Parameter(value = "node_id_file", validators = NodeIdFileValidator.class)
|
||||
private String nodeIdFile = "data/node-id";
|
||||
|
||||
@@ -274,16 +287,6 @@ public class Configuration extends BaseConfiguration {
|
||||
return datanodeHttpCertificatePassword;
|
||||
}
|
||||
|
||||
|
||||
public String getRestApiUsername() {
|
||||
return restApiUsername;
|
||||
}
|
||||
|
||||
public String getRestApiPassword() {
|
||||
return restApiPassword;
|
||||
}
|
||||
|
||||
|
||||
public Optional<String> getOpensearchNetworkHostHost() {
|
||||
return Optional.ofNullable(opensearchNetworkHostHost);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.graylog.datanode.configuration;
|
||||
|
||||
import org.graylog.datanode.OpensearchDistribution;
|
||||
import org.graylog2.security.IndexerJwtAuthTokenProvider;
|
||||
|
||||
/**
|
||||
* DatanodeConfiguration holds the static configuration as parsed during the datanode startup, either from the
|
||||
@@ -25,6 +26,7 @@ import org.graylog.datanode.OpensearchDistribution;
|
||||
public record DatanodeConfiguration(
|
||||
OpensearchDistribution opensearchDistribution,
|
||||
String nodeName,
|
||||
int processLogsBufferSize
|
||||
int processLogsBufferSize,
|
||||
IndexerJwtAuthTokenProvider indexerJwtAuthTokenProvider
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.graylog.datanode.configuration;
|
||||
|
||||
import org.graylog.datanode.Configuration;
|
||||
import org.graylog.datanode.OpensearchDistribution;
|
||||
import org.graylog2.security.IndexerJwtAuthTokenProvider;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
@@ -31,12 +32,13 @@ public class DatanodeConfigurationProvider implements Provider<DatanodeConfigura
|
||||
private final DatanodeConfiguration datanodeConfiguration;
|
||||
|
||||
@Inject
|
||||
public DatanodeConfigurationProvider(final Configuration localConfiguration) throws IOException {
|
||||
public DatanodeConfigurationProvider(final Configuration localConfiguration, IndexerJwtAuthTokenProvider jwtTokenProvider) throws IOException {
|
||||
final OpensearchDistribution opensearchDistribution = detectOpensearchDistribution(localConfiguration);
|
||||
datanodeConfiguration = new DatanodeConfiguration(
|
||||
opensearchDistribution,
|
||||
localConfiguration.getDatanodeNodeName(),
|
||||
localConfiguration.getProcessLogsBufferSize()
|
||||
localConfiguration.getProcessLogsBufferSize(),
|
||||
jwtTokenProvider
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,11 @@ import org.graylog.datanode.process.OpensearchConfiguration;
|
||||
import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Collections;
|
||||
@@ -40,24 +42,26 @@ import java.util.Optional;
|
||||
@Singleton
|
||||
public class OpensearchConfigurationProvider implements Provider<OpensearchConfiguration> {
|
||||
|
||||
|
||||
private final Configuration localConfiguration;
|
||||
private final UploadedCertFilesSecureConfiguration uploadedCertFilesSecureConfiguration;
|
||||
private final MongoCertSecureConfiguration mongoCertSecureConfiguration;
|
||||
private final InSecureConfiguration inSecureConfiguration;
|
||||
private final DatanodeConfiguration datanodeConfiguration;
|
||||
private final byte[] signingKey;
|
||||
|
||||
@Inject
|
||||
public OpensearchConfigurationProvider(Configuration localConfiguration,
|
||||
DatanodeConfiguration datanodeConfiguration,
|
||||
UploadedCertFilesSecureConfiguration uploadedCertFilesSecureConfiguration,
|
||||
MongoCertSecureConfiguration mongoCertSecureConfiguration,
|
||||
InSecureConfiguration inSecureConfiguration) {
|
||||
InSecureConfiguration inSecureConfiguration,
|
||||
final @Named("password_secret") String passwordSecret) {
|
||||
this.localConfiguration = localConfiguration;
|
||||
this.datanodeConfiguration = datanodeConfiguration;
|
||||
this.uploadedCertFilesSecureConfiguration = uploadedCertFilesSecureConfiguration;
|
||||
this.mongoCertSecureConfiguration = mongoCertSecureConfiguration;
|
||||
this.inSecureConfiguration = inSecureConfiguration;
|
||||
this.signingKey = passwordSecret.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,7 +86,7 @@ public class OpensearchConfigurationProvider implements Provider<OpensearchConfi
|
||||
if (chosenSecurityConfigurationVariant.isPresent()) {
|
||||
securityConfiguration = chosenSecurityConfigurationVariant.get()
|
||||
.build()
|
||||
.configure(localConfiguration);
|
||||
.configure(localConfiguration, signingKey);
|
||||
opensearchProperties.putAll(securityConfiguration.getProperties());
|
||||
}
|
||||
|
||||
@@ -93,8 +97,6 @@ public class OpensearchConfigurationProvider implements Provider<OpensearchConfi
|
||||
localConfiguration.getHostname(),
|
||||
localConfiguration.getOpensearchHttpPort(),
|
||||
localConfiguration.getOpensearchTransportPort(),
|
||||
localConfiguration.getRestApiUsername(),
|
||||
localConfiguration.getRestApiPassword(),
|
||||
"datanode-cluster",
|
||||
localConfiguration.getDatanodeNodeName(),
|
||||
Collections.emptyList(),
|
||||
|
||||
@@ -16,20 +16,19 @@
|
||||
*/
|
||||
package org.graylog.datanode.configuration.variants;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
@@ -39,7 +38,9 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -72,7 +73,7 @@ public class OpensearchSecurityConfiguration {
|
||||
* 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 {
|
||||
public OpensearchSecurityConfiguration configure(Configuration localConfiguration, byte[] signingKey) throws GeneralSecurityException, IOException {
|
||||
if (securityEnabled()) {
|
||||
|
||||
logCertificateInformation("transport certificate", transportCertificate);
|
||||
@@ -91,7 +92,7 @@ public class OpensearchSecurityConfiguration {
|
||||
System.setProperty("javax.net.ssl.trustStore", trustStorePath.toAbsolutePath().toString());
|
||||
System.setProperty("javax.net.ssl.trustStorePassword", truststorePassword);
|
||||
|
||||
configureInitialAdmin(localConfiguration, opensearchConfigDir, localConfiguration.getRestApiUsername(), localConfiguration.getRestApiPassword());
|
||||
enableJwtAuthenticationInConfig(opensearchConfigDir, signingKey);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -115,7 +116,6 @@ public class OpensearchSecurityConfiguration {
|
||||
// configured node names.
|
||||
config.put("plugins.security.ssl.transport.enforce_hostname_verification", "false");
|
||||
|
||||
|
||||
config.put("plugins.security.ssl.http.enabled", "true");
|
||||
|
||||
config.put("plugins.security.ssl.http.keystore_type", KEYSTORE_FORMAT);
|
||||
@@ -133,6 +133,37 @@ public class OpensearchSecurityConfiguration {
|
||||
return config.build();
|
||||
}
|
||||
|
||||
private Map<String, Object> filterConfigurationMap(final Map<String, Object> map, final String... keys) {
|
||||
Map<String, Object> result = map;
|
||||
for(final String key: List.of(keys)) {
|
||||
result = (Map<String, Object>)result.get(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void enableJwtAuthenticationInConfig(final Path opensearchConfigDir, final byte[] signingKey) throws IOException {
|
||||
final ObjectMapper objectMapper = new YAMLMapper();
|
||||
final File file = opensearchConfigDir.resolve(Path.of("opensearch-security", "config.yml")).toFile();
|
||||
Map<String, Object> contents = objectMapper.readValue(file, new TypeReference<>() {});
|
||||
|
||||
/* TODO: the following two lines should be active, once #16334 is merged and there are nightly builds of master available to build a compatible 5.2-dev DataNode Docker image
|
||||
Map<String, Object> config = filterConfigurationMap(contents, "config", "dynamic", "authc", "jwt_auth_domain", "http_authenticator", "config");
|
||||
config.put("signing_key", Base64.getEncoder().encodeToString(signingKey));
|
||||
*/
|
||||
|
||||
// TODO: remove the following block, once #16334 is merged and there are nightly builds of master available to build a compatible 5.2-dev DataNode Docker image
|
||||
Map<String, Object> jwt_auth_domain = filterConfigurationMap(contents, "config", "dynamic", "authc", "jwt_auth_domain");
|
||||
jwt_auth_domain.put("http_enabled", true);
|
||||
jwt_auth_domain.put("transport_enabled", true);
|
||||
|
||||
Map<String, Object> config = filterConfigurationMap(jwt_auth_domain, "http_authenticator", "config");
|
||||
config.put("signing_key", Base64.getEncoder().encodeToString(signingKey));
|
||||
config.put("roles_key", "os_roles");
|
||||
// TODO: remove up until here
|
||||
|
||||
objectMapper.writeValue(file, contents);
|
||||
}
|
||||
|
||||
public boolean securityEnabled() {
|
||||
return !Objects.isNull(httpCertificate) && !Objects.isNull(transportCertificate);
|
||||
}
|
||||
@@ -169,37 +200,6 @@ public class OpensearchSecurityConfiguration {
|
||||
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<String, Object> map = mapper.readValue(new FileInputStream(internalUsersFile.toFile()), TypeReferences.MAP_STRING_OBJECT);
|
||||
final Map<String, Object> 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);
|
||||
}
|
||||
|
||||
private void logCertificateInformation(String certificateType, KeystoreInformation keystore) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {
|
||||
final KeyStore instance = KeyStore.getInstance(KEYSTORE_FORMAT);
|
||||
try (final FileInputStream is = new FileInputStream(keystore.location().toFile())) {
|
||||
|
||||
@@ -71,7 +71,7 @@ class OpensearchProcessImpl implements OpensearchProcess, ProcessListener {
|
||||
}
|
||||
|
||||
private RestHighLevelClient createRestClient(OpensearchConfiguration configuration) {
|
||||
return OpensearchRestClient.build(configuration, trustManager);
|
||||
return OpensearchRestClient.build(configuration, datanodeConfiguration, trustManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,8 +98,7 @@ class OpensearchProcessImpl implements OpensearchProcess, ProcessListener {
|
||||
.map(httpHost -> new URIBuilder()
|
||||
.setHost(httpHost.getHostName())
|
||||
.setPort(httpHost.getPort())
|
||||
.setScheme(httpHost.getSchemeName())
|
||||
.setUserInfo(configuration.get().authUsername(), configuration.get().authPassword()).toString())
|
||||
.setScheme(httpHost.getSchemeName()).toString())
|
||||
.orElse(""); // TODO: will this cause problems in the nodeservice?
|
||||
return URI.create(baseUrl);
|
||||
}
|
||||
|
||||
@@ -16,19 +16,15 @@
|
||||
*/
|
||||
package org.graylog.datanode.management;
|
||||
|
||||
import org.graylog.datanode.configuration.DatanodeConfiguration;
|
||||
import org.graylog.datanode.process.OpensearchConfiguration;
|
||||
import org.graylog.security.certutil.CaService;
|
||||
import org.graylog.shaded.opensearch2.org.apache.http.HttpHost;
|
||||
import org.graylog.shaded.opensearch2.org.apache.http.auth.AuthScope;
|
||||
import org.graylog.shaded.opensearch2.org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.graylog.shaded.opensearch2.org.apache.http.client.CredentialsProvider;
|
||||
import org.graylog.shaded.opensearch2.org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.graylog.shaded.opensearch2.org.apache.http.HttpRequestInterceptor;
|
||||
import org.graylog.shaded.opensearch2.org.opensearch.client.RestHighLevelClient;
|
||||
import org.graylog2.security.CustomCAX509TrustManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import java.security.KeyManagementException;
|
||||
@@ -38,23 +34,21 @@ import java.security.SecureRandom;
|
||||
public class OpensearchRestClient {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OpensearchRestClient.class);
|
||||
|
||||
public static RestHighLevelClient build(final OpensearchConfiguration configuration, final CustomCAX509TrustManager tm) {
|
||||
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||
|
||||
public static RestHighLevelClient build(final OpensearchConfiguration configuration, final DatanodeConfiguration datanodeConfiguration, final CustomCAX509TrustManager tm) {
|
||||
final HttpHost host = configuration.getRestBaseUrl();
|
||||
|
||||
org.graylog.shaded.opensearch2.org.opensearch.client.RestClientBuilder builder = org.graylog.shaded.opensearch2.org.opensearch.client.RestClient.builder(host);
|
||||
if ("https".equals(host.getSchemeName())) {
|
||||
if (configuration.authUsername() != null && configuration.authPassword() != null) {
|
||||
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(configuration.authUsername(), configuration.authPassword()));
|
||||
}
|
||||
|
||||
try {
|
||||
final var sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, new TrustManager[]{tm}, new SecureRandom());
|
||||
|
||||
builder.setHttpClientConfigCallback(httpClientBuilder -> {
|
||||
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
|
||||
httpClientBuilder.addInterceptorLast((HttpRequestInterceptor) (request, context) -> {
|
||||
final String jwtToken = datanodeConfiguration.indexerJwtAuthTokenProvider().get();
|
||||
request.addHeader("Authorization", jwtToken);
|
||||
});
|
||||
httpClientBuilder.setSSLContext(sslContext);
|
||||
return httpClientBuilder;
|
||||
});
|
||||
|
||||
@@ -33,8 +33,6 @@ public record OpensearchConfiguration(
|
||||
String hostname,
|
||||
int httpPort,
|
||||
int transportPort,
|
||||
String authUsername,
|
||||
String authPassword,
|
||||
String clusterName, String nodeName, List<String> nodeRoles,
|
||||
List<String> discoverySeedHosts,
|
||||
OpensearchSecurityConfiguration opensearchSecurityConfiguration,
|
||||
|
||||
Reference in New Issue
Block a user