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:
Jan Heise
2023-09-12 11:48:44 +02:00
committed by GitHub
parent 862520d455
commit cc52dcb5b0
35 changed files with 558 additions and 259 deletions

View File

@@ -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);
}

View File

@@ -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
) {
}

View File

@@ -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
);
}

View File

@@ -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(),

View File

@@ -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())) {

View File

@@ -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);
}

View File

@@ -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;
});

View File

@@ -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,