basic auth for datanode rest api if security enabled (#16446)

* basic auth for datanode rest api if security enabled

* added changelog

* adapted to master changes

* configurable rest admin username in datanode

---------

Co-authored-by: Jan Heise <jan.heise@graylog.com>
This commit is contained in:
Tomas Dvorak
2023-09-15 20:52:14 +02:00
committed by GitHub
parent a401e81b27
commit 70d6edc8a2
11 changed files with 223 additions and 87 deletions

View File

@@ -54,8 +54,6 @@ public class DatanodeClusterIT {
static Path tempDir;
private String hostnameNodeA;
private KeyStore trustStore;
private String initialAdminUsername;
private String initialAdminPassword;
private KeystoreInformation ca;
private Network network;
private MongoDBTestService mongoDBTestService;
@@ -71,8 +69,6 @@ public class DatanodeClusterIT {
hostnameNodeA = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef");
final KeystoreInformation transportNodeA = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeA);
final KeystoreInformation httpNodeA = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeA);
initialAdminUsername = RandomStringUtils.randomAlphabetic(10);
initialAdminPassword = RandomStringUtils.randomAlphabetic(10);
this.network = Network.newNetwork();
this.mongoDBTestService = MongoDBTestService.create(MongodbServer.MONGO5, network);
@@ -83,9 +79,7 @@ public class DatanodeClusterIT {
mongoDBTestService,
hostnameNodeA,
transportNodeA,
httpNodeA,
initialAdminUsername,
initialAdminPassword
httpNodeA
);
@@ -98,9 +92,7 @@ public class DatanodeClusterIT {
mongoDBTestService,
hostnameNodeB,
transportNodeB,
httpNodeB,
initialAdminUsername,
initialAdminPassword
httpNodeB
);
Stream.of(nodeA, nodeB).parallel().forEach(DatanodeContainerizedBackend::start);
@@ -130,9 +122,7 @@ public class DatanodeClusterIT {
network, mongoDBTestService,
hostnameNodeC,
transportNodeC,
httpNodeC,
initialAdminUsername,
initialAdminPassword
httpNodeC
);
nodeC.start();
@@ -146,9 +136,7 @@ public class DatanodeClusterIT {
MongoDBTestService mongodb,
String hostname,
KeystoreInformation transportKeystore,
KeystoreInformation httpKeystore,
String restUsername,
String restPassword) {
KeystoreInformation httpKeystore) {
return new DatanodeContainerizedBackend(
network,
mongodb,
@@ -171,10 +159,6 @@ public class DatanodeClusterIT {
datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_CERTIFICATE", "datanode-https-certificates.p12");
datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_CERTIFICATE_PASSWORD", httpKeystore.passwordAsString());
// configure initial admin username and password for Opensearch REST
datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_USERNAME", restUsername);
datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_PASSWORD", restPassword);
// this is the interface that we bind opensearch to. It must be 0.0.0.0 if we want
// to be able to reach opensearch from outside the container and docker network (true?)
datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_BIND_ADDRESS", "0.0.0.0");

View File

@@ -18,19 +18,13 @@ 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;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import io.restassured.RestAssured;
import io.restassured.response.ValidatableResponse;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.http.NoHttpResponseException;
import org.graylog.datanode.DatanodeRestApiWait;
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;
import org.junit.jupiter.api.BeforeEach;
@@ -39,18 +33,16 @@ import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
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;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static io.restassured.RestAssured.given;
import static org.graylog.datanode.testinfra.DatanodeContainerizedBackend.IMAGE_WORKING_DIR;
import static org.graylog.testing.completebackend.ContainerizedGraylogBackend.ROOT_PASSWORD_PLAINTEXT;
public class DatanodeSecuritySetupIT {
private static final Logger LOG = LoggerFactory.getLogger(DatanodeSecuritySetupIT.class);
@@ -58,14 +50,15 @@ public class DatanodeSecuritySetupIT {
@TempDir
static Path tempDir;
private DatanodeContainerizedBackend backend;
private String httpUsername;
private String httpPassword;
private KeyStore trustStore;
private String containerHostname;
private String restAdminUsername;
@BeforeEach
void setUp() throws IOException, GeneralSecurityException {
restAdminUsername = RandomStringUtils.randomAlphanumeric(10);
containerHostname = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef");
// first generate a self-signed CA
KeystoreInformation ca = DatanodeSecurityTestUtils.generateCa(tempDir);
@@ -76,9 +69,6 @@ public class DatanodeSecuritySetupIT {
// use the CA to generate HTTP certificate keystore
final KeystoreInformation httpCert = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, containerHostname, Tools.getLocalCanonicalHostname());
httpUsername = RandomStringUtils.randomAlphabetic(10);
httpPassword = RandomStringUtils.randomAlphabetic(10);
backend = new DatanodeContainerizedBackend(containerHostname, datanodeContainer -> {
// provide the keystore files to the docker container
datanodeContainer.withFileSystemBind(transportCert.location().toAbsolutePath().toString(), IMAGE_WORKING_DIR + "/config/datanode-transport-certificates.p12");
@@ -88,16 +78,11 @@ 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");
datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_CERTIFICATE_PASSWORD", httpCert.passwordAsString());
// configure initial admin username and password for Opensearch REST
datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_USERNAME", httpUsername);
datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_PASSWORD", httpPassword);
// this is the interface that we bind opensearch to. It must be 0.0.0.0 if we want
// to be able to reach opensearch from outside the container and docker network (true?)
datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_BIND_ADDRESS", "0.0.0.0");
@@ -108,6 +93,8 @@ public class DatanodeSecuritySetupIT {
datanodeContainer.withEnv("GRAYLOG_DATANODE_HOSTNAME", containerHostname);
datanodeContainer.withEnv("GRAYLOG_DATANODE_SINGLE_NODE_ONLY", "true");
datanodeContainer.withEnv("GRAYLOG_DATANODE_ROOT_USERNAME", restAdminUsername);
}).start();
}
@@ -120,7 +107,7 @@ public class DatanodeSecuritySetupIT {
void testSecuredSetup() throws ExecutionException, RetryException {
final String jwtAuthToken = IndexerJwtAuthTokenProvider.createToken(DatanodeContainerizedBackend.SIGNING_SECRET.getBytes(StandardCharsets.UTF_8), Duration.seconds(120));
waitForOpensearchAvailableStatus(backend.getDatanodeRestPort(), backend.getOpensearchRestPort(), trustStore);
waitForOpensearchAvailableStatus(backend.getDatanodeRestPort(), trustStore, restAdminUsername, ROOT_PASSWORD_PLAINTEXT);
try {
given().header( "Authorization", "Bearer " + jwtAuthToken)
@@ -135,27 +122,13 @@ public class DatanodeSecuritySetupIT {
}
}
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))
.retryIfException(input -> input instanceof NoHttpResponseException)
.retryIfException(input -> input instanceof SocketException)
.retryIfException(input -> input instanceof SSLHandshakeException) // may happen before SSL is configured properly
.retryIfResult(input -> !input.extract().contentType().startsWith("application/json"))
.retryIfResult(input -> !input.extract().body().path("opensearch.node.state").equals("AVAILABLE"))
.build();
private ValidatableResponse waitForOpensearchAvailableStatus(final Integer datanodeRestPort, final KeyStore trustStore, String authUsername, String authPassword) throws ExecutionException, RetryException {
try {
var hostname = Tools.getLocalCanonicalHostname();
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(rest)
.then());
return DatanodeRestApiWait.onPort(datanodeRestPort)
.withTruststore(trustStore)
.withBasicAuth(authUsername, authPassword)
.waitForAvailableStatus();
} catch (Exception ex) {
LOG.error("Error starting the DataNode, showing logs:\n" + backend.getLogs());
throw ex;