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

@@ -16,11 +16,14 @@
*/
package org.graylog.datanode.integration;
import com.github.joschi.jadconfig.util.Duration;
import com.github.rholder.retry.RetryException;
import org.apache.commons.lang3.RandomStringUtils;
import org.graylog.datanode.DatanodeOpensearchWait;
import org.graylog.datanode.configuration.variants.KeystoreInformation;
import org.graylog.datanode.testinfra.DatanodeContainerizedBackend;
import org.graylog2.security.IndexerJwtAuthTokenProvider;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -30,8 +33,11 @@ import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import javax.net.ssl.SSLHandshakeException;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
@@ -144,6 +150,7 @@ public class DatanodeClusterIT {
mongodb,
hostname,
datanodeContainer -> {
datanodeContainer.withEnv("GRAYLOG_DATANODE_PASSWORD_SECRET", DatanodeContainerizedBackend.SIGNING_SECRET);
datanodeContainer.withEnv("GRAYLOG_DATANODE_CLUSTER_INITIAL_MANAGER_NODES", hostnameNodeA);
datanodeContainer.withEnv("GRAYLOG_DATANODE_OPENSEARCH_DISCOVERY_SEED_HOSTS", hostnameNodeA + ":9300");
@@ -174,11 +181,13 @@ public class DatanodeClusterIT {
});
}
private void waitForNodesCount(int countOfNodes) throws ExecutionException, RetryException {
private void waitForNodesCount(final int countOfNodes) throws ExecutionException, RetryException {
final String jwtToken = IndexerJwtAuthTokenProvider.createToken(DatanodeContainerizedBackend.SIGNING_SECRET.getBytes(StandardCharsets.UTF_8), Duration.seconds(120));
try {
DatanodeOpensearchWait.onPort(nodeA.getOpensearchRestPort())
.withTruststore(trustStore)
.withBasicAuth(initialAdminUsername, initialAdminPassword)
.withJwtAuth(jwtToken)
.waitForNodesCount(countOfNodes);
} catch (Exception retryException) {
LOG.error("DataNode Container logs from nodeA follow:\n" + nodeA.getLogs());

View File

@@ -16,6 +16,7 @@
*/
package org.graylog.datanode.integration;
import com.github.joschi.jadconfig.util.Duration;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
@@ -28,6 +29,7 @@ import org.apache.http.NoHttpResponseException;
import org.graylog.datanode.configuration.variants.KeystoreInformation;
import org.graylog.datanode.testinfra.DatanodeContainerizedBackend;
import org.graylog2.plugin.Tools;
import org.graylog2.security.IndexerJwtAuthTokenProvider;
import org.graylog2.shared.utilities.StringUtils;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
@@ -40,6 +42,7 @@ import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
@@ -86,6 +89,7 @@ public class DatanodeSecuritySetupIT {
datanodeContainer.withEnv("GRAYLOG_DATANODE_TRANSPORT_CERTIFICATE", "datanode-transport-certificates.p12");
datanodeContainer.withEnv("GRAYLOG_DATANODE_TRANSPORT_CERTIFICATE_PASSWORD", transportCert.passwordAsString());
datanodeContainer.withEnv("GRAYLOG_DATANODE_INSECURE_STARTUP", "false");
datanodeContainer.withEnv("GRAYLOG_DATANODE_PASSWORD_SECRET", DatanodeContainerizedBackend.SIGNING_SECRET);
// configure http security
datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_CERTIFICATE", "datanode-https-certificates.p12");
@@ -115,24 +119,24 @@ public class DatanodeSecuritySetupIT {
@Test
void testSecuredSetup() throws ExecutionException, RetryException {
final String jwtAuthToken = IndexerJwtAuthTokenProvider.createToken(DatanodeContainerizedBackend.SIGNING_SECRET.getBytes(StandardCharsets.UTF_8), Duration.seconds(120));
waitForOpensearchAvailableStatus(backend.getDatanodeRestPort(), trustStore);
waitForOpensearchAvailableStatus(backend.getDatanodeRestPort(), backend.getOpensearchRestPort(), trustStore);
try {
given()
.auth().basic(httpUsername, httpPassword)
.trustStore(trustStore)
.get("https://localhost:" + backend.getOpensearchRestPort())
.then().assertThat()
.body("name", Matchers.equalTo(containerHostname))
.body("cluster_name", Matchers.equalTo("datanode-cluster"));
given().header( "Authorization", "Bearer " + jwtAuthToken)
.trustStore(trustStore)
.get("https://localhost:" + backend.getOpensearchRestPort())
.then().assertThat()
.body("name", Matchers.equalTo(containerHostname))
.body("cluster_name", Matchers.equalTo("datanode-cluster"));
} catch (Exception ex) {
LOG.error("Error connecting to OpenSearch in the DataNode, showing logs:\n{}", backend.getLogs());
throw ex;
}
}
private void waitForOpensearchAvailableStatus(Integer datanodeRestPort, KeyStore trustStore) throws ExecutionException, RetryException {
private void waitForOpensearchAvailableStatus(final Integer datanodeRestPort, final Integer opensearchPort, final KeyStore trustStore) throws ExecutionException, RetryException {
final Retryer<ValidatableResponse> retryer = RetryerBuilder.<ValidatableResponse>newBuilder()
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(120))
@@ -145,11 +149,13 @@ public class DatanodeSecuritySetupIT {
try {
var hostname = Tools.getLocalCanonicalHostname();
var url = StringUtils.f("https://%s:%d", hostname, datanodeRestPort);
LOG.info("Trying to connect to: {}", url);
var rest = StringUtils.f("https://%s:%d", hostname, datanodeRestPort);
var os = StringUtils.f("https://%s:%d", hostname, opensearchPort);
LOG.info("Trying to connect to REST API at: {}, OpenSearch is at: {}", rest, os);
retryer.call(() -> RestAssured.given()
.auth().basic(httpUsername, httpPassword)
.trustStore(trustStore)
.get(url)
.get(rest)
.then());
} catch (Exception ex) {
LOG.error("Error starting the DataNode, showing logs:\n" + backend.getLogs());