Skip to content

Commit

Permalink
Test TLS registry manual trigger of cert reloading
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik authored and fedinskiy committed Oct 10, 2024
1 parent 700c52a commit 29fbd81
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -1,47 +1,39 @@
package io.quarkus.ts.messaging.infinispan.grpc.kafka.providers;

import java.io.File;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.KafkaAdminClient;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.config.SslConfigs;
import org.eclipse.microprofile.config.inject.ConfigProperty;

public class SaslSslKafkaProvider extends KafkaProviders {

private final static String SASL_USERNAME_VALUE = "client";
private final static String SASL_PASSWORD_VALUE = "client-secret12345678912345678912";

@ConfigProperty(name = "kafka.ssl.truststore.location")
Optional<String> trustStoreFile;

@ConfigProperty(name = "kafka.ssl.truststore.password")
Optional<String> trustStorePassword;
import io.smallrye.common.annotation.Identifier;

@ConfigProperty(name = "kafka.ssl.truststore.type")
Optional<String> trustStoreType;
public class SaslSslKafkaProvider extends KafkaProviders {

@ConfigProperty(name = "kafka-client-sasl-ssl.bootstrap.servers")
Optional<String> saslSslKafkaBootStrap;

@Inject
@Identifier("default-kafka-broker")
Map<String, Object> defaultKafkaBrokerConfig;

@ApplicationScoped
@Produces
@Named("kafka-consumer-sasl-ssl")
KafkaConsumer<String, String> getSaslSslConsumer() {
Properties props = setupConsumerProperties(saslSslKafkaBootStrap.get());
saslSetup(props);
sslSetup(props);
saslSslSetup(props);
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList("test-ssl-consumer"));
return consumer;
Expand All @@ -52,8 +44,7 @@ KafkaConsumer<String, String> getSaslSslConsumer() {
@Named("kafka-producer-sasl-ssl")
KafkaProducer<String, String> getSaslSslProducer() {
Properties props = setupProducerProperties(saslSslKafkaBootStrap.get());
saslSetup(props);
sslSetup(props);
saslSslSetup(props);
return new KafkaProducer<>(props);
}

Expand All @@ -62,24 +53,12 @@ KafkaProducer<String, String> getSaslSslProducer() {
@Named("kafka-admin-sasl-ssl")
AdminClient getSaslSslAdmin() {
Properties props = setupConsumerProperties(saslSslKafkaBootStrap.get());
saslSetup(props);
sslSetup(props);
saslSslSetup(props);
return KafkaAdminClient.create(props);
}

private static void saslSetup(Properties props) {
props.setProperty(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");
props.setProperty(SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-512");
props.setProperty(SaslConfigs.SASL_JAAS_CONFIG,
"org.apache.kafka.common.security.scram.ScramLoginModule required username=\"%s\" password=\"%s\";"
.formatted(SASL_USERNAME_VALUE, SASL_PASSWORD_VALUE));
}

protected void sslSetup(Properties props) {
File tsFile = new File(trustStoreFile.get());
props.setProperty(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, tsFile.getPath());
props.setProperty(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, trustStorePassword.get());
props.setProperty(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, trustStoreType.get());
protected void saslSslSetup(Properties props) {
defaultKafkaBrokerConfig.forEach(props::putIfAbsent);
props.setProperty(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.quarkus.test.services.containers.model.KafkaProtocol;
import io.quarkus.test.services.containers.model.KafkaVendor;

@Tag("QUARKUS-4592")
@Tag("QUARKUS-2036")
@QuarkusScenario
public class InfinispanKafkaSaslSslIT {
Expand All @@ -29,7 +30,7 @@ public class InfinispanKafkaSaslSslIT {
.withSecretFiles(CertUtils.KEYSTORE)
.onPreStart((action) -> CertUtils.prepareCerts());

@KafkaContainer(vendor = KafkaVendor.STRIMZI, protocol = KafkaProtocol.SASL_SSL)
@KafkaContainer(vendor = KafkaVendor.STRIMZI, protocol = KafkaProtocol.SASL_SSL, tlsRegistryEnabled = true, tlsConfigName = "kafka-sasl-ssl")
static final KafkaService kafkaSaslSsl = new KafkaService();

@QuarkusApplication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.awaitility.Awaitility.await;

import org.hamcrest.core.StringContains;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import io.quarkus.test.bootstrap.KafkaService;
Expand All @@ -15,6 +16,7 @@
import io.quarkus.test.services.containers.model.KafkaProtocol;
import io.quarkus.test.services.containers.model.KafkaVendor;

@Tag("QUARKUS-4592")
@QuarkusScenario
public class KafkaSaslSslTlsRegistryIT {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.awaitility.Awaitility.await;

import org.hamcrest.core.StringContains;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import io.quarkus.test.bootstrap.KafkaService;
Expand All @@ -15,6 +16,7 @@
import io.quarkus.test.services.containers.model.KafkaProtocol;
import io.quarkus.test.services.containers.model.KafkaVendor;

@Tag("QUARKUS-4592")
@QuarkusScenario
public class KafkaSslTlsRegistryIT {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.ts.security.https;

import jakarta.enterprise.event.Event;
import jakarta.enterprise.event.Observes;

import io.quarkus.tls.CertificateUpdatedEvent;
import io.quarkus.tls.TlsConfiguration;
import io.quarkus.tls.TlsConfigurationRegistry;
import io.vertx.core.Vertx;
import io.vertx.ext.web.Router;

public class CertificateReloader {

private static final String MTLS_CONFIG_NAME = "mtls-http";

void setupCertificateReloadingTriggerRoute(@Observes Router router, Event<CertificateUpdatedEvent> certUpdatedEvent,
TlsConfigurationRegistry registry, Vertx vertx) {
router.route("/reload-mtls-certificates").handler(ctx -> {
TlsConfiguration config = registry.get(MTLS_CONFIG_NAME).orElseThrow();
vertx
.executeBlocking(() -> {
boolean reloaded = config.reload();
if (reloaded) {
certUpdatedEvent.fire(new CertificateUpdatedEvent(MTLS_CONFIG_NAME, config));
}
return reloaded;
})
.onSuccess(reloaded -> {
if (reloaded) {
ctx.response().end("Certificates reloaded.");
} else {
ctx.fail(500, new RuntimeException("Could not reload mTLS certificates."));
}
})
.onFailure(ctx::fail);
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,20 @@ public class SecuredResource {
@Inject
SecurityIdentity identity;

@GET // requires 'user' role
@Produces(MediaType.TEXT_PLAIN)
public String getHttps() {
return getResponse();
}

@Path("mtls") // requires authentication
@GET
@Produces(MediaType.TEXT_PLAIN)
public String get() {
public String getMtls() {
return getResponse();
}

private String getResponse() {
X509Certificate certificate = identity.getCredential(CertificateCredential.class).getCertificate();
return "Client certificate: " + certificate.getSubjectX500Principal().getName();
}
Expand Down
3 changes: 3 additions & 0 deletions security/https/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ quarkus.http.auth.policy.users-only.roles-allowed=user
quarkus.http.auth.permission.secured-urls.paths=/secured/*
quarkus.http.auth.permission.secured-urls.policy=users-only

quarkus.http.auth.permission.mtls.paths=/secured/mtls
quarkus.http.auth.permission.mtls.policy=authenticated

quarkus.http.auth.certificate-role-properties=role-mappings.txt
quarkus.native.additional-build-args=-H:IncludeResources=.*\\.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.quarkus.ts.security.https.secured;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.security.certificate.CertificateBuilder;
import io.quarkus.test.security.certificate.ClientCertificateRequest;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.Certificate.ClientCertificate;
import io.quarkus.test.services.QuarkusApplication;
import io.quarkus.test.utils.AwaitilityUtils;

@Tag("QUARKUS-4592")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@QuarkusScenario
public class MutualTlsSecurityIT {

private static final String MTLS_PATH = "/secured/mtls";
private static final String CERT_PREFIX = "qe-test";
private static final String CLIENT = "client";
private static final String GUEST_CLIENT = "guest-client";

@QuarkusApplication(ssl = true, certificates = @Certificate(prefix = CERT_PREFIX, clientCertificates = {
@ClientCertificate(cnAttribute = CLIENT),
@ClientCertificate(cnAttribute = GUEST_CLIENT, unknownToServer = true)
}, configureKeystore = true, configureTruststore = true, tlsConfigName = "mtls-http", configureHttpServer = true))
static final RestService app = new RestService()
.withProperty("quarkus.http.auth.proactive", "false")
.withProperty("quarkus.http.ssl.client-auth", "required")
.withProperty("quarkus.http.insecure-requests", "disabled");

@Order(1)
@Test
public void testMutualTlsForHttpServer() {
var client1 = app.mutinyHttps(CLIENT);
var httpResponse = client1.get(MTLS_PATH).sendAndAwait();
assertEquals(HttpStatus.SC_OK, httpResponse.statusCode());
assertEquals("Client certificate: CN=" + CLIENT, httpResponse.bodyAsString());

callSecuredEndpointAndExpectFailure(GUEST_CLIENT);
}

@Order(2)
@Test
public void testCertificateReloading() {
// important to load before certificates are reloaded because otherwise
// we would have new invalid certificate on test side and expected different certs on the server side
var clientBeforeReload = app.mutinyHttps(CLIENT);

app
.<CertificateBuilder> getPropertyFromContext(CertificateBuilder.INSTANCE_KEY)
.regenerateCertificate(CERT_PREFIX, certRequest -> {
// regenerate client certificates
// make the first client invalid
var clientOneCert = new ClientCertificateRequest(CLIENT, true);
// make the second client valid
var clientTwoCert = new ClientCertificateRequest(GUEST_CLIENT, false);
certRequest.withClientRequests(clientOneCert, clientTwoCert);
});

var response = clientBeforeReload.get("/reload-mtls-certificates").sendAndAwait();
assertEquals(HttpStatus.SC_OK, response.statusCode());
assertEquals("Certificates reloaded.", response.bodyAsString());

// now we expect opposite from what we tested in step one: client 1 must fail and client 2 must succeed
AwaitilityUtils.untilAsserted(() -> {
var httpResponse = app.mutinyHttps(GUEST_CLIENT).get(MTLS_PATH).sendAndAwait();
assertEquals(HttpStatus.SC_OK, httpResponse.statusCode());
assertEquals("Client certificate: CN=" + GUEST_CLIENT, httpResponse.bodyAsString());

callSecuredEndpointAndExpectFailure(CLIENT);
});
}

private static void callSecuredEndpointAndExpectFailure(String clientCn) {
// this client certs are not in the server truststore, therefore they cannot be trusted
try {
app.mutinyHttps(clientCn).get(MTLS_PATH).sendAndAwait();
// this must never happen, basically as SSL handshake must throw exception
Assertions.fail("SSL handshake didn't fail even though certificate host is unknown");
} catch (Exception e) {
// failure is expected
assertTrue(e.getMessage().contains("Received fatal alert: bad_certificate"),
"Expected failure over bad certificate, but got: " + e.getMessage());
}
}
}

0 comments on commit 29fbd81

Please sign in to comment.