Skip to content

Commit

Permalink
Fix Kafka SSL scenarios on FIPS by regenerating certs
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik authored and mjurc committed Apr 29, 2024
1 parent 754cd0c commit 10575b9
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,13 @@
@QuarkusScenario
public class StrimziKafkaWithDefaultSaslSslMessagingIT {

private final static String SASL_USERNAME_VALUE = "client";
private final static String SASL_PASSWORD_VALUE = "client-secret";
private static final String TRUSTSTORE_FILE = "strimzi-server-ssl-truststore.p12";

@KafkaContainer(vendor = KafkaVendor.STRIMZI, protocol = KafkaProtocol.SASL_SSL, kafkaConfigResources = TRUSTSTORE_FILE)
@KafkaContainer(vendor = KafkaVendor.STRIMZI, protocol = KafkaProtocol.SASL_SSL)
static final KafkaService kafka = new KafkaService();

@QuarkusApplication
static final RestService app = new RestService()
.withProperty("kafka.bootstrap.servers", kafka::getBootstrapUrl)
.withProperty("kafka.security.protocol", "SASL_SSL")
.withProperty("kafka.ssl.truststore.location", TRUSTSTORE_FILE)
.withProperty("kafka.ssl.truststore.password", "top-secret")
.withProperty("kafka.ssl.truststore.type", "PKCS12")
.withProperty("kafka.sasl.mechanism", "PLAIN")
.withProperty("kafka.sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required "
+ "username=\"" + SASL_USERNAME_VALUE + "\" "
+ "password=\"" + SASL_PASSWORD_VALUE + "\";");
.withProperties(kafka::getSslProperties)
.withProperty("kafka.bootstrap.servers", kafka::getBootstrapUrl);

@Test
public void checkUserResourceByNormalUser() {
Expand Down
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
<infinispan.image>docker.io/infinispan/server:14.0</infinispan.image>
<infinispan-legacy.image>docker.io/infinispan/server:13.0</infinispan-legacy.image>
<reruns>2</reruns>
<flaky-run-reporter.version>0.1.2.Beta1</flaky-run-reporter.version>
<certificate-generator.version>0.5.0</certificate-generator.version>
</properties>
<distributionManagement>
<snapshotRepository>
Expand Down Expand Up @@ -204,6 +206,11 @@
<artifactId>quarkus-test-maven</artifactId>
<version>${quarkus.platform.version}</version>
</dependency>
<dependency>
<groupId>me.escoffier.certs</groupId>
<artifactId>certificate-generator</artifactId>
<version>${certificate-generator.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,11 @@

String[] command() default {};

/**
* If true, forwards Docker ports from localhost to Docker host on Windows.
* This works around issue when certificates are only generated for localhost.
*/
boolean portDockerHostToLocalhost() default false;

Class<? extends ManagedResourceBuilder> builder() default ContainerManagedResourceBuilder.class;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Optional;
import java.util.ServiceLoader;

import io.quarkus.test.bootstrap.LocalhostManagedResource;
import io.quarkus.test.bootstrap.ManagedResource;
import io.quarkus.test.bootstrap.ManagedResourceBuilder;
import io.quarkus.test.bootstrap.ServiceContext;
Expand All @@ -20,6 +21,7 @@ public class ContainerManagedResourceBuilder implements ManagedResourceBuilder {
private String expectedLog;
private String[] command;
private Integer port;
private boolean portDockerHostToLocalhost;

protected String getImage() {
return image;
Expand Down Expand Up @@ -48,17 +50,24 @@ public void init(Annotation annotation) {
this.command = metadata.command();
this.expectedLog = PropertiesUtils.resolveProperty(metadata.expectedLog());
this.port = metadata.port();
this.portDockerHostToLocalhost = metadata.portDockerHostToLocalhost();
}

@Override
public ManagedResource build(ServiceContext context) {
this.context = context;
for (ContainerManagedResourceBinding binding : managedResourceBindingsRegistry) {
if (binding.appliesFor(context)) {
if (portDockerHostToLocalhost) {
return new LocalhostManagedResource(binding.init(this));
}
return binding.init(this);
}
}

if (portDockerHostToLocalhost) {
return new LocalhostManagedResource(new GenericDockerContainerManagedResource(this));
}
return new GenericDockerContainerManagedResource(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ public T withProperties(String... propertiesFiles) {
return (T) this;
}

/**
* The runtime configuration property to be used if the built artifact is
* configured to be run.
*
* NOTE: unlike other {@link this::withProperties}, here we add new properties and keep the old ones
*/
public T withProperties(Supplier<Map<String, String>> newProperties) {
futureProperties.add(() -> properties.putAll(newProperties.get()));
return (T) this;
}

/**
* The runtime configuration property to be used if the built artifact is
* configured to be run.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package io.quarkus.test.bootstrap;

import java.io.IOException;
import java.util.List;

import org.junit.jupiter.api.condition.OS;

import io.quarkus.test.services.URILike;
import io.quarkus.test.utils.Command;

/**
* Forward Docker ports from localhost to Docker host on Windows. This works around issue when
* certificates are only generated for localhost.
*/
public final class LocalhostManagedResource implements ManagedResource {

/**
* Our Linux bare-metal instances use Docker on localhost.
*/
private static final boolean FORWARD_PORT = OS.current() == OS.WINDOWS;
private final ManagedResource delegate;

public LocalhostManagedResource(ManagedResource delegate) {
this.delegate = delegate;
}

@Override
public String getDisplayName() {
return delegate.getDisplayName();
}

@Override
public void stop() {
if (FORWARD_PORT) {
try {
// stop port proxy
new Command("netsh", "interface", "portproxy", "delete", "v4tov4",
"listenport=" + getExposedPort(), "listenaddress=127.0.0.1").runAndWait();
} catch (IOException | InterruptedException e) {
throw new RuntimeException(
"Failed delete port proxy for Kafka container port " + getExposedPort(), e);
}
}
delegate.stop();
}

@Override
public void start() {
delegate.start();
if (FORWARD_PORT) {
try {
// forward localhost:somePort to dockerIp:somePort
new Command("netsh", "interface", "portproxy", "add", "v4tov4", "listenport=" + getExposedPort(),
"listenaddress=127.0.0.1", "connectport=" + getExposedPort(),
"connectaddress=" + getDockerHost()).runAndWait();
} catch (IOException | InterruptedException e) {
throw new RuntimeException(
"Failed to setup forwarding for Kafka container port " + getExposedPort(), e);
}
}
}

@Override
public URILike getURI(Protocol protocol) {
var uriLike = delegate.getURI(protocol);
if (FORWARD_PORT) {
// replace Docker IP with local host
uriLike = new URILike(uriLike.getScheme(), "localhost", uriLike.getPort(), uriLike.getPath());
}
return uriLike;
}

private String getDockerHost() {
return delegate.getURI(Protocol.NONE).getHost();
}

private int getExposedPort() {
return delegate.getURI(Protocol.NONE).getPort();
}

@Override
public boolean isRunning() {
return delegate.isRunning();
}

@Override
public boolean isFailed() {
return delegate.isFailed();
}

@Override
public List<String> logs() {
return delegate.logs();
}

@Override
public void restart() {
delegate.restart();
}

@Override
public void validate() {
delegate.validate();
}

@Override
public void afterStart() {
delegate.afterStart();
}

@Override
public URILike createURI(String scheme, String host, int port) {
return delegate.createURI(scheme, host, port);
}
}
4 changes: 4 additions & 0 deletions quarkus-test-service-kafka/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,9 @@
<optional>true</optional>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.escoffier.certs</groupId>
<artifactId>certificate-generator</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package io.quarkus.test.bootstrap;

import static java.util.Objects.requireNonNull;

import java.util.Map;

public class KafkaService extends BaseService<KafkaService> {

public static final String KAFKA_REGISTRY_URL_PROPERTY = "ts.kafka.registry.url";
public static final String KAFKA_SSL_PROPERTIES = "ts.kafka.ssl.properties";

public String getBootstrapUrl() {
var host = getURI();
Expand All @@ -12,4 +17,8 @@ public String getBootstrapUrl() {
public String getRegistryUrl() {
return getPropertyFromContext(KAFKA_REGISTRY_URL_PROPERTY);
}

public Map<String, String> getSslProperties() {
return requireNonNull(getPropertyFromContext(KAFKA_SSL_PROPERTIES));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.test.services.containers;

import java.io.File;

import org.apache.commons.lang3.StringUtils;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
Expand Down Expand Up @@ -65,6 +67,10 @@ protected int getKafkaRegistryPort() {
return model.getVendor().getRegistry().getPort();
}

protected String getResourceTargetName(String resource) {
return resource;
}

@Override
protected GenericContainer<?> initContainer() {
GenericContainer<?> kafkaContainer = initKafkaContainer();
Expand All @@ -78,7 +84,15 @@ protected GenericContainer<?> initContainer() {
}

for (String resource : getKafkaConfigResources()) {
kafkaContainer.withCopyFileToContainer(MountableFile.forClasspathResource(resource), kafkaConfigPath + resource);
if (resource.contains(File.separator)) {
// file in the target directory
String fileName = resource.substring(resource.lastIndexOf(File.separator) + 1);
kafkaContainer.withCopyFileToContainer(MountableFile.forHostPath(resource), kafkaConfigPath + fileName);
} else {
// resource
kafkaContainer.withCopyFileToContainer(MountableFile.forClasspathResource(resource),
kafkaConfigPath + getResourceTargetName(resource));
}
}

if (model.isWithRegistry()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.lang.annotation.Annotation;
import java.util.ServiceLoader;

import io.quarkus.test.bootstrap.LocalhostManagedResource;
import io.quarkus.test.bootstrap.ManagedResource;
import io.quarkus.test.bootstrap.ManagedResourceBuilder;
import io.quarkus.test.bootstrap.ServiceContext;
Expand Down Expand Up @@ -115,7 +116,7 @@ public ManagedResource build(ServiceContext context) {
}

if (vendor == KafkaVendor.STRIMZI) {
return new StrimziKafkaContainerManagedResource(this);
return new LocalhostManagedResource(new StrimziKafkaContainerManagedResource(this));
}

return new ConfluentKafkaContainerManagedResource(this);
Expand Down

0 comments on commit 10575b9

Please sign in to comment.