HTTP Clients with SSL

About

By default, WebClient uses the JVM’s built-in trust store (cacerts) to validate SSL certificates. This works for public APIs and standard HTTPS websites. But modern enterprise systems deal with:

  • Private APIs across data centers

  • Zero-trust networks

  • Internal services signed by private CAs

  • Mutual TLS between services

In such scenarios, SSL customization is not optional—it’s mandatory for secure and reliable communication.

Scenarios

Scenario

What’s Required

Explanation

Calling internal service signed with a self-signed certificate

Configure a TrustManager with a custom trust store that includes the internal certificate.

Internal services in closed networks often use self-signed certificates. These are not recognized by default JVM trust stores, so we must explicitly trust them by loading them into a custom trust store and wiring that into our WebClient's SSL context.

Mutual TLS (mTLS) between microservices

Provide a client certificate and private key via a KeyManager.

Mutual TLS is used when both client and server need to authenticate each other. Our WebClient must be configured with a client-side certificate and private key (from a key store) to establish the SSL handshake. This is common in secure service meshes, zero-trust networks, or regulated industries.

Certificate pinning

Implement a custom X509TrustManager that validates specific certificate fingerprints.

Instead of trusting a certificate chain or CA, our application pins one or more known public keys or cert fingerprints. This adds extra protection against CA compromises or man-in-the-middle attacks. We override the default trust logic with custom fingerprint checks.

Disabling SSL verification in CI or mock environments

Provide a permissive HostnameVerifier and trust-all TrustManager (only for non-production use).

For local development or CI pipelines where strict SSL validation is not feasible (e.g., expired certs, mock servers), we can disable checks. However, this approach must never be promoted to production due to extreme security risk. This setup is usually isolated in test-specific configs or profiles.

Different trust rules per target host or environment

Build and apply dynamic trust configurations based on the hostname or target service.

When interacting with multiple services—some public, some internal we might need different trust stores or key pairs for each. We can create per-host SSL contexts and inject the right one dynamically at runtime, either via configuration-based routing or by customizing the HttpClient logic used in WebClient.

Trust Store vs Key Store

Aspect

Trust Store

Key Store

Purpose

Holds certificates of external trusted parties (usually servers or Certificate Authorities)

Holds our application's own certificates and private keys used for identification

Used For

Verifying the identity of the remote server (e.g., during HTTPS handshake)

Proving our identity to the remote party (e.g., in mutual TLS scenarios)

Contents

Trusted certificates (e.g., .cer, .crt, or public certs from CAs)

Private key and corresponding public certificate chain (e.g., in a .p12 or .jks file)

Example Usage

Trusting a backend service’s certificate or an internal self-signed cert

Exposing our app to secure client connections or enabling mutual authentication

Format

.jks, .p12, or PEM files

.jks, .p12, or PEM + Key

Spring Configuration Example

Loaded into TrustManagerFactory and applied to SslContext

Loaded into KeyManagerFactory and used in mTLS configurations

Tools for Management

keytool, openssl

keytool, openssl

Where It's Referenced

Typically required for outgoing connections where the client must validate the server

Required for incoming connections or mTLS where the server must validate the client

How They Work Together in HTTPS / mTLS ?

  • In regular HTTPS, only the trust store is used to verify the remote (server) certificate.

  • In mutual TLS, both trust store and key store are used:

    • The client uses the trust store to validate the server.

    • The client also uses the key store to send its own certificate and prove its identity.

Think of the trust store like our personal contact list: it contains people we trust to talk to. The key store is like our own government-issued ID. We use it to prove who we are when someone asks for identification.

When Configuring in WebClient

  • If our WebClient needs to talk to a self-signed internal service, configure a trust store with the server cert.

  • If our WebClient participates in mutual TLS, we also need a key store to provide our client certificate.

How It Works Under the Hood ?

When we configure WebClient with custom SSL, here's what happens during the request:

  1. WebClient (via Netty) initiates an HTTPS connection.

  2. SSL handshake begins:

    • The server sends its certificate.

    • The client verifies it using the TrustManager.

    • If mutual TLS is enabled, the client sends its own cert from the KeyManager.

  3. The secure connection is established or rejected.

  4. HTTP request is sent securely.

Typical Approaches to Custom SSL Setup

1. Trusting Self-Signed Certificates (Custom Trust Store)

In production, services typically use certificates issued by trusted Certificate Authorities (CAs) like DigiCert or Let's Encrypt. But in many real-world enterprise scenarios—especially in internal or non-production environments we may:

  • Use self-signed certificates

  • Use an internal certificate authority (CA)

  • Need to test secure HTTPS endpoints without buying certificates from a public CA

By default, the Java Virtual Machine (JVM) uses a default trust store (cacerts) that doesn't trust self-signed or internal CA certificates unless explicitly added.

This leads to common errors like:

  • javax.net.ssl.SSLHandshakeException: PKIX path building failed

  • unable to find valid certification path to requested target

To resolve this, we configure our application to load a custom trust store (e.g., a .jks or .p12 file) that contains the required self-signed certificate or the root/intermediate certificate of our internal CA.

Steps to Create a Trust Store

  1. Export the certificate from the remote server (using browser or openssl).

  2. Import it into a .jks trust store using keytool:

keytool -import -alias my-service-cert \
        -file my-service.crt \
        -keystore truststore.jks \
        -storepass changeit

Code: WebClient with Custom Trust Store (Reactor Netty)

@Configuration
public class SecureWebClientConfig {

    @Bean
    public WebClient webClientWithCustomTrustStore() throws Exception {
        // Load truststore
        KeyStore trustStore = KeyStore.getInstance("JKS");
        try (FileInputStream trustStoreFile = new FileInputStream("truststore.jks")) {
            trustStore.load(trustStoreFile, "changeit".toCharArray()); // password for the .jks
        }

        // Initialize TrustManager with custom truststore
        TrustManagerFactory trustManagerFactory =
                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);

        // Create SSL context
        SslContext sslContext = SslContextBuilder.forClient()
                .trustManager(trustManagerFactory)
                .build();

        // Configure Netty client with custom SSL
        HttpClient httpClient = HttpClient.create()
                .secure(sslSpec -> sslSpec.sslContext(sslContext));

        // Create and return WebClient
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}

Structure of the Trust Store

  • Format: Typically .jks, but .p12 is also supported (PKCS12).

  • Contains: Certificates of the services we call.

  • Secured with a password (e.g., "changeit").

We can place the file in src/main/resources and load it with classpath if preferred:

try (InputStream trustStream = getClass().getResourceAsStream("/truststore.jks")) {
    ...
}

Using This WebClient

@Autowired
private WebClient webClientWithCustomTrustStore;

public Mono<String> callSecureService() {
    return webClientWithCustomTrustStore
        .get()
        .uri("https://internal-secure-service/api/secure-data")
        .retrieve()
        .bodyToMono(String.class);
}

2. Mutual TLS (Using Key Store and Trust Store)

Mutual TLS (mTLS) is a two-way SSL authentication mechanism where both client and server authenticate each other using certificates.

  • The server presents its certificate to the client.

  • The client presents its certificate to the server.

This is commonly used in enterprise systems or zero-trust architectures, particularly for internal service-to-service communication where strong authentication and encryption are required.

When Do We Need It ?

  • Secure communication between microservices inside private networks

  • API Gateways or BFFs validating backend services

  • Ensuring only trusted applications talk to each other

  • Regulatory compliance (e.g., PCI-DSS, HIPAA)

Prerequisites

We need:

  • A trust store: Contains certificates of the services we are talking to.

  • A key store: Contains our own certificate + private key to present to the server.

Use keytool or openssl to:

  • Generate keys and certificates

  • Sign and import them into .jks or .p12 files

Code

MutualTLSWebClientConfig.java

package com.example.config;

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.security.KeyStore;

@Configuration
public class MutualTLSWebClientConfig {

    @Bean
    public WebClient mutualTlsWebClient() throws Exception {
        // Load TrustStore
        KeyStore trustStore = KeyStore.getInstance("JKS");
        try (FileInputStream trustStoreFile = new FileInputStream("truststore.jks")) {
            trustStore.load(trustStoreFile, "trustpass".toCharArray());
        }
        TrustManagerFactory trustManagerFactory =
                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);

        // Load KeyStore (client certificate + private key)
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream keyStoreFile = new FileInputStream("keystore.p12")) {
            keyStore.load(keyStoreFile, "keypass".toCharArray());
        }
        KeyManagerFactory keyManagerFactory =
                KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, "keypass".toCharArray());

        // Create SSL Context with both KeyManager and TrustManager
        SslContext sslContext = SslContextBuilder.forClient()
                .keyManager(keyManagerFactory)
                .trustManager(trustManagerFactory)
                .build();

        // Configure WebClient with custom SSL
        HttpClient httpClient = HttpClient.create()
                .secure(sslSpec -> sslSpec.sslContext(sslContext));

        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}
Component
Purpose

trustStore.jks

Trusts the server's certificate (or the CA that signed it)

keystore.p12

Stores client cert and private key to authenticate itself

KeyManagerFactory

Used to present client credentials

TrustManagerFactory

Used to validate the server’s certificate

Use Example

@Autowired
private WebClient mutualTlsWebClient;

public Mono<String> fetchSecureData() {
    return mutualTlsWebClient.get()
            .uri("https://secure.internal.service/api/data")
            .retrieve()
            .bodyToMono(String.class);
}

3. Disabling SSL Validation (For CI or Local Mocks ONLY)

Disabling SSL validation allows an HTTP client to:

  • Trust all SSL certificates, even self-signed or expired ones

  • Bypass hostname verification, accepting mismatched hostnames

This is extremely dangerous in production and should only be used in controlled test environments like:

  • Local development

  • CI pipelines using mock/stub services

  • Testing internal services before certs are provisioned

Why Would We Do This ?

Scenario
Reason

CI pipelines with stubbed services over HTTPS

Mock servers use self-signed or untrusted certificates

Developer is testing internal microservices locally

No production-like certs available

Legacy systems using expired certs for testing

Bypassing SSL issues temporarily

Risks

  • Leaves us vulnerable to Man-in-the-Middle (MitM) attacks

  • Completely bypasses SSL/TLS verification, making the communication untrustworthy

  • Can mask certificate misconfiguration issues

Never commit or ship code with SSL disabled to production environments.

Code to Trust All Certificates + Disable Hostname Verification

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.SslProvider;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;

import javax.net.ssl.SSLException;

@Configuration
public class InsecureWebClientConfig {

    @Bean
    public WebClient insecureWebClient() throws SSLException {
        // Trust all certificates
        SslContext sslContext = SslContextBuilder.forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .build();

        // Disable hostname verification
        HttpClient httpClient = HttpClient.create()
                .secure(sslContextSpec -> sslContextSpec
                        .sslContext(sslContext)
                        .handlerConfigurator(sslHandler -> 
                            sslHandler.setHostnameVerifier((hostname, session) -> true)
                        )
                );

        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}
Component
Role

InsecureTrustManagerFactory.INSTANCE

Accepts all certificates

setHostnameVerifier(... -> true)

Disables hostname checks

SslContextBuilder.forClient()

Builds the client SSL context

HttpClient.create().secure(...)

Applies custom SSL context to Reactor Netty HTTP client

Best Practices for Safer Testing

  • Use a local CA and trusted certs for mocks (e.g., mkcert)

  • Always wrap this logic in a conditional bean or Spring profile

    @Profile("dev")
    @Bean
    public WebClient insecureWebClient() { ... }

We can also make the insecure client conditional on an environment property like:

@Value("${webclient.insecure:false}")
private boolean insecure;

@Bean
public WebClient webClient() {
    return insecure ? insecureWebClient() : secureWebClient();
}

This ensures we don't accidentally deploy insecure code into production.

4. Certificate Pinning

Certificate pinning is a security technique where our application trusts only a specific certificate or public key even if it is otherwise valid and trusted by the system’s trust store.

Instead of trusting any certificate issued by a recognized Certificate Authority (CA), we “pin” the application to a known, fixed certificate or public key fingerprint.

Why Use Certificate Pinning ?

Reason
Description

Prevent MitM attacks

Ensures attackers cannot impersonate the server even with a valid certificate

Extra layer of trust

Protects against CA compromises

Ideal for sensitive systems

Financial services, authentication providers, and internal microservices benefit the most

How It Works

When the WebClient makes an HTTPS call, a custom X509TrustManager intercepts the certificate and compares it against pre-approved fingerprints or public keys.

If the certificate matches, the request proceeds. Otherwise, it's rejected—even if the cert is valid.

Risks and Trade-Offs

Risk
Explanation

Certificate rotation issues

If certs change and app isn’t updated, connections will fail

Maintenance burden

We must manually maintain valid pins

Breaks flexibility

Can’t rely on usual CA trust hierarchy

How to Implement Certificate Pinning in WebClient

This implementation assumes pinning via SHA-256 fingerprint of the certificate.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;

import javax.net.ssl.*;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.security.MessageDigest;

@Configuration
public class PinnedCertWebClientConfig {

    // Expected SHA-256 certificate fingerprint (Base64 encoded)
    private static final String PINNED_FINGERPRINT = "QWxhZGRpbjpPcGVuU2VzYW1l"; // example

    @Bean
    public WebClient pinnedCertWebClient() throws Exception {
        TrustManager[] trustManagers = new TrustManager[]{
            new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) { }

                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    if (chain == null || chain.length == 0) {
                        throw new CertificateException("Certificate chain is invalid");
                    }

                    X509Certificate serverCert = chain[0];
                    String actualFingerprint = getFingerprint(serverCert);

                    if (!PINNED_FINGERPRINT.equals(actualFingerprint)) {
                        throw new CertificateException("Certificate pinning failed. Fingerprint mismatch.");
                    }
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }

                private String getFingerprint(X509Certificate cert) throws CertificateException {
                    try {
                        MessageDigest digest = MessageDigest.getInstance("SHA-256");
                        byte[] encoded = cert.getEncoded();
                        byte[] hash = digest.digest(encoded);
                        return Base64.getEncoder().encodeToString(hash);
                    } catch (Exception e) {
                        throw new CertificateException("Could not calculate certificate fingerprint", e);
                    }
                }
            }
        };

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, new java.security.SecureRandom());

        HttpClient httpClient = HttpClient.create()
            .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));

        return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
    }
}

How to Get the Fingerprint

Run the following to extract fingerprint from a certificate file:

openssl x509 -in server.crt -noout -fingerprint -sha256

Then Base64-encode the hex value or directly compare raw hex (change code accordingly).

When to Use Certificate Pinning

Environment
Reason

Production with strict zero-trust policy

Extra layer of identity assurance

High-security applications

Financial APIs, healthcare, internal auth

Direct device-to-service communication

IoT, mobile apps calling backend

When Not to Use It

Situation
Reason

Certificate rotation is automated

Pins will break without manual updates

Using public APIs with rotating certs

We will likely break connectivity

Maintaining multiple environments

Pinning adds complexity in staging, QA, etc.

Best Practices

  • Use a fallback trust manager if pinning fails (optional)

  • Keep the pinned value in a centralized config for easier rotation

  • Pin public key or subject public key info (SPKI) instead of full cert if we expect cert changes but key stability

5. Multiple WebClients With Different SSL Contexts

In real-world enterprise applications, we might need to interact with multiple external systems—each with different SSL requirements. One might use a self-signed certificate, another might require mutual TLS, and yet another might use standard CA-signed certificates.

Using a single WebClient bean doesn’t work well in this scenario because SSL configuration (Trust Store, Key Store) is tightly coupled with the HTTP client.

Hence, we need multiple WebClient instances, each with its own SSL context.

How to Set Up Multiple WebClients with Custom SSL Contexts

Each WebClient is configured with its own ReactorClientHttpConnector, which is backed by a different HttpClient using a separate SSLContext.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;

import javax.net.ssl.SSLContext;
import java.security.KeyStore;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.KeyManagerFactory;

@Configuration
public class MultiSSLWebClientConfig {

    @Bean
    public WebClient selfSignedClient() throws Exception {
        SSLContext sslContext = createSSLContext("classpath:selfsigned-truststore.jks", "password", null, null);
        HttpClient httpClient = HttpClient.create()
            .secure(t -> t.sslContext(sslContext));
        return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
    }

    @Bean
    public WebClient mutualTlsClient() throws Exception {
        SSLContext sslContext = createSSLContext(
            "classpath:mutual-tls-truststore.jks", "trustpass",
            "classpath:mutual-tls-keystore.jks", "keypass"
        );
        HttpClient httpClient = HttpClient.create()
            .secure(t -> t.sslContext(sslContext));
        return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
    }

    private SSLContext createSSLContext(
        String trustStorePath, String trustStorePassword,
        String keyStorePath, String keyStorePassword
    ) throws Exception {

        // Load trust store
        KeyStore trustStore = KeyStore.getInstance("JKS");
        trustStore.load(
            getClass().getResourceAsStream(trustStorePath),
            trustStorePassword.toCharArray()
        );
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);

        // Load key store (optional)
        KeyManagerFactory kmf = null;
        if (keyStorePath != null) {
            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(
                getClass().getResourceAsStream(keyStorePath),
                keyStorePassword.toCharArray()
            );
            kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            kmf.init(keyStore, keyStorePassword.toCharArray());
        }

        // Build SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(
            kmf != null ? kmf.getKeyManagers() : null,
            tmf.getTrustManagers(),
            null
        );
        return sslContext;
    }
}

Usage Example in Service Layer

@Service
public class PaymentService {

    private final WebClient selfSignedClient;
    private final WebClient mutualTlsClient;

    public PaymentService(
        @Qualifier("selfSignedClient") WebClient selfSignedClient,
        @Qualifier("mutualTlsClient") WebClient mutualTlsClient
    ) {
        this.selfSignedClient = selfSignedClient;
        this.mutualTlsClient = mutualTlsClient;
    }

    public Mono<String> callServiceA() {
        return selfSignedClient.get()
            .uri("https://self-signed-service/api/data")
            .retrieve()
            .bodyToMono(String.class);
    }

    public Mono<String> callServiceB() {
        return mutualTlsClient.post()
            .uri("https://secure-service/api/submit")
            .retrieve()
            .bodyToMono(String.class);
    }
}

Testing Custom SSL Setup

Custom SSL configurations (like self-signed certs, mutual TLS, certificate pinning) are notoriously error-prone. If misconfigured, they don’t fail gracefully the client simply fails to connect, often with obscure error messages. That’s why robust testing is essential.

We should validate:

  • The trust and key stores are correctly loaded.

  • The SSL handshake completes successfully.

  • Our WebClient communicates securely as intended.

  • Errors are surfaced clearly (e.g., untrusted cert, wrong alias, expired cert, hostname mismatch).

Testing Scenarios

Scenario
Goal

Integration test with mock HTTPS server

Verify end-to-end behavior of SSL context

CI pipeline SSL validation

Fail builds early if certs are expired or invalid

Negative test with wrong trust store

Ensure application fails securely

Mutual TLS setup test

Confirm handshake using client cert/key

Certificate pinning check

Block unknown certs intentionally

1. Integration Testing with a Local HTTPS Server

We can use tools like WireMock, MockServer, or Testcontainers with NGINX configured with HTTPS to simulate remote services.

Example: WireMock with HTTPS

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import org.junit.jupiter.api.*;
import org.springframework.web.reactive.function.client.WebClient;

class SslWebClientTest {

    static WireMockServer wireMockServer;

    WebClient client;

    @BeforeAll
    static void startServer() {
        wireMockServer = new WireMockServer(WireMockConfiguration.options()
            .httpsPort(8443)
            .keystorePath("src/test/resources/test-keystore.jks")
            .keystorePassword("testpass"));
        wireMockServer.start();
    }

    @AfterAll
    static void stopServer() {
        wireMockServer.stop();
    }

    @BeforeEach
    void setupClient() throws Exception {
        // Create WebClient using truststore that trusts test-keystore cert
        SSLContext sslContext = SslUtils.createSSLContext(
            "src/test/resources/test-truststore.jks", "testpass",
            null, null
        );

        HttpClient httpClient = HttpClient.create().secure(spec -> spec.sslContext(sslContext));
        client = WebClient.builder()
            .baseUrl("https://localhost:8443")
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
    }

    @Test
    void shouldConnectWithValidTrustStore() {
        wireMockServer.stubFor(get(urlEqualTo("/hello"))
            .willReturn(aResponse().withBody("Secure Hello")));

        String body = client.get().uri("/hello")
            .retrieve()
            .bodyToMono(String.class)
            .block();

        Assertions.assertEquals("Secure Hello", body);
    }
}

2. Negative Testing With Wrong Trust Store

Try running a test where our WebClient uses a trust store that does not include the server’s certificate.

Expectation: An SSLHandshakeException should be thrown.

This helps validate that our certificate validation logic is strict and correctly enforced.

3. Testing Mutual TLS Setup

Simulate an endpoint requiring client authentication using:

  • NGINX configured with ssl_verify_client on;

  • A custom Spring Boot server with server.ssl.client-auth=need

Then test our WebClient with correct and incorrect keystore configuration to ensure:

  • Handshake succeeds with proper keystore

  • Handshake fails with missing/invalid client cert

4. Testcontainers for Isolated SSL Testing

We can use Testcontainers with Dockerized services that have:

  • Different cert setups (self-signed, CA-signed)

  • TLS-only ports

  • Simulated network failures

This helps test realistic environments in our integration pipeline.

Last updated