Skip to content

Commit dcf6573

Browse files
authored
feat: Add plugin API for selecting TLS client credentials (kroxylicious#3218)
* feat: Add TLS credential supplier plugin API for server connections - Define TLS credential supplier plugin API - Extend TargetCluster configuration for TLS credential suppliers - Implement TLS credential supplier runtime integration - Add comprehensive tests for TLS credential supplier plugin API - Document TLS credential supplier configuration - Address PR review feedback and CI failures - Align tlsCredentials() API with design proposal (JDK types) - Fix security and correctness issues in TLS credential supplier See https://github.com/kroxylicious/design/blob/main/proposals/011-plugin-api-to-select-tls-credentials-for-server-connection.md Assisted-by: AI (Pi/Claude Sonnet 4.6) <noreply@pi.dev> Assisted-by: AI (Pi/GPT-5.5) <noreply@pi.dev> Signed-off-by: Paco Viramontes <kidpollo@gmail.com> Signed-off-by: Paco Viramontes <paco.viramontes@shopify.com>
1 parent 996425b commit dcf6573

44 files changed

Lines changed: 5174 additions & 46 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/tls/Tls.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,43 @@
1818
* @param trust specifies a trust provider used by this peer to determine whether to trust the peer. If omitted platform trust is used instead.
1919
* @param cipherSuites specifies a custom object which contains details of allowed and denied cipher suites
2020
* @param protocols specifies a custom object which contains details of allowed and denied tls protocols
21+
* @param credentialSupplier specifies a dynamic TLS credential supplier for per-client certificate selection (optional)
2122
*/
2223
public record Tls(@Nullable KeyProvider key,
2324
@Nullable TrustProvider trust,
2425
@Nullable AllowDeny<String> cipherSuites,
25-
@Nullable AllowDeny<String> protocols) {
26+
@Nullable AllowDeny<String> protocols,
27+
@Nullable TlsCredentialSupplierConfig credentialSupplier) {
28+
29+
/**
30+
* Compact constructor with validation.
31+
*/
32+
public Tls {
33+
if (key != null && credentialSupplier != null) {
34+
throw new IllegalArgumentException(
35+
"Cannot configure both 'key' and 'credentialSupplier' - they are mutually exclusive. " +
36+
"Use 'key' for static TLS credentials or 'credentialSupplier' for dynamic credential selection.");
37+
}
38+
}
39+
40+
/**
41+
* Creates a Tls configuration without a TLS credential supplier.
42+
* This constructor is provided for backward compatibility with v0.18.0.
43+
* <p>
44+
* For configurations that require dynamic TLS credential selection, use the
45+
* 5-parameter constructor that includes {@code credentialSupplier}.
46+
*
47+
* @param key specifies a key provider that provides the certificate/key used to identify this peer.
48+
* @param trust specifies a trust provider used by this peer to determine whether to trust the peer.
49+
* @param cipherSuites specifies allowed and denied cipher suites
50+
* @param protocols specifies allowed and denied tls protocols
51+
*/
52+
public Tls(@Nullable KeyProvider key,
53+
@Nullable TrustProvider trust,
54+
@Nullable AllowDeny<String> cipherSuites,
55+
@Nullable AllowDeny<String> protocols) {
56+
this(key, trust, cipherSuites, protocols, null);
57+
}
2658

2759
public static final String PEM = "PEM";
2860

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
package io.kroxylicious.proxy.config.tls;
8+
9+
import java.util.Objects;
10+
11+
import com.fasterxml.jackson.annotation.JsonCreator;
12+
import com.fasterxml.jackson.annotation.JsonProperty;
13+
14+
import io.kroxylicious.proxy.plugin.PluginImplConfig;
15+
import io.kroxylicious.proxy.plugin.PluginImplName;
16+
import io.kroxylicious.proxy.tls.ServerTlsCredentialSupplierFactory;
17+
18+
import edu.umd.cs.findbugs.annotations.Nullable;
19+
20+
/**
21+
* Configuration for a TLS credential supplier that dynamically provides TLS credentials.
22+
* <p>
23+
* This follows the same pattern as filter configuration, with a type specifying the
24+
* {@link ServerTlsCredentialSupplierFactory} implementation and an optional config object
25+
* for supplier-specific configuration.
26+
* </p>
27+
*
28+
* @param type The type of TLS credential supplier (references a {@link ServerTlsCredentialSupplierFactory} implementation)
29+
* @param config Supplier-specific configuration (optional)
30+
*/
31+
public record TlsCredentialSupplierConfig(
32+
@PluginImplName(ServerTlsCredentialSupplierFactory.class) @JsonProperty(required = true) String type,
33+
@Nullable @PluginImplConfig(implNameProperty = "type") Object config) {
34+
35+
@JsonCreator
36+
public TlsCredentialSupplierConfig {
37+
Objects.requireNonNull(type, "TLS credential supplier type must not be null");
38+
}
39+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
package io.kroxylicious.proxy.tls;
8+
9+
import java.util.concurrent.CompletionStage;
10+
11+
/**
12+
* <p>Supplies TLS credentials for proxy-to-server (upstream) TLS connections.</p>
13+
*
14+
* <p>Instances of this interface are created by {@link ServerTlsCredentialSupplierFactory}
15+
* and are responsible for providing TLS credentials (private keys and certificate chains)
16+
* that the proxy uses when connecting to the target Kafka cluster.</p>
17+
*
18+
* <p>The supplier supports asynchronous credential retrieval, allowing implementations
19+
* to load credentials from remote sources, perform cryptographic operations, or
20+
* interact with external services without blocking the proxy runtime.</p>
21+
*
22+
* <h2>Thread Safety</h2>
23+
* <p>Implementations must be thread-safe as the {@link #tlsCredentials(ServerTlsCredentialSupplierContext)}
24+
* method may be called concurrently from multiple threads.</p>
25+
*
26+
* <h2>Non-Blocking Requirement</h2>
27+
* <p>Implementations must not block the calling thread or perform heavy I/O operations synchronously.
28+
* Long-running work such as network calls, file I/O, or key generation should be performed
29+
* asynchronously, returning a {@link CompletionStage} that completes when the work is done.</p>
30+
*
31+
* <h2>Error Handling</h2>
32+
* <p>If credential retrieval fails, implementations should return a {@link CompletionStage}
33+
* that completes exceptionally. The runtime will handle the exception appropriately,
34+
* typically by rejecting the connection attempt.</p>
35+
*
36+
* <h2>Usage Example: File-Based Credential Loading</h2>
37+
* <pre>{@code
38+
* public class FileBasedCredentialSupplier implements ServerTlsCredentialSupplier {
39+
* private final PrivateKey key;
40+
* private final X509Certificate[] chain;
41+
*
42+
* public FileBasedCredentialSupplier(PrivateKey key, X509Certificate[] chain) {
43+
* this.key = key;
44+
* this.chain = chain;
45+
* }
46+
*
47+
* @Override
48+
* public CompletionStage<TlsCredentials> tlsCredentials(ServerTlsCredentialSupplierContext context) {
49+
* // Plugin has already parsed the key and certificate chain (from PEM, PKCS12, etc.)
50+
* // Use context factory method to create validated TlsCredentials
51+
* TlsCredentials creds = context.tlsCredentials(key, chain);
52+
* return CompletableFuture.completedFuture(creds);
53+
* }
54+
* }
55+
* }</pre>
56+
*
57+
* <h2>Usage Example: Client-Specific Credentials</h2>
58+
* <pre>{@code
59+
* public class ClientSpecificSupplier implements ServerTlsCredentialSupplier {
60+
* private final Map<String, PrivateKey> clientKeys;
61+
* private final Map<String, X509Certificate[]> clientChains;
62+
* private final PrivateKey defaultKey;
63+
* private final X509Certificate[] defaultChain;
64+
*
65+
* public ClientSpecificSupplier(Map<String, PrivateKey> clientKeys,
66+
* Map<String, X509Certificate[]> clientChains,
67+
* PrivateKey defaultKey,
68+
* X509Certificate[] defaultChain) {
69+
* this.clientKeys = clientKeys;
70+
* this.clientChains = clientChains;
71+
* this.defaultKey = defaultKey;
72+
* this.defaultChain = defaultChain;
73+
* }
74+
*
75+
* @Override
76+
* public CompletionStage<TlsCredentials> tlsCredentials(ServerTlsCredentialSupplierContext context) {
77+
* Optional<ClientTlsContext> clientContext = context.clientTlsContext();
78+
*
79+
* if (clientContext.isPresent() && clientContext.get().clientCertificate().isPresent()) {
80+
* String clientId = clientContext.get().clientCertificate().get()
81+
* .getSubjectX500Principal().getName();
82+
*
83+
* PrivateKey key = clientKeys.get(clientId);
84+
* X509Certificate[] chain = clientChains.get(clientId);
85+
*
86+
* if (key != null && chain != null) {
87+
* TlsCredentials creds = context.tlsCredentials(key, chain);
88+
* return CompletableFuture.completedFuture(creds);
89+
* }
90+
* }
91+
*
92+
* // Fall back to shared default credentials
93+
* TlsCredentials creds = context.tlsCredentials(defaultKey, defaultChain);
94+
* return CompletableFuture.completedFuture(creds);
95+
* }
96+
* }
97+
* }</pre>
98+
*
99+
* @see ServerTlsCredentialSupplierFactory
100+
* @see TlsCredentials
101+
*/
102+
public interface ServerTlsCredentialSupplier {
103+
104+
/**
105+
* <p>Asynchronously retrieves TLS credentials for the proxy to use when connecting
106+
* to the target Kafka cluster.</p>
107+
*
108+
* <p>This method may be called multiple times and should return credentials
109+
* appropriate for the current request context. Implementations may cache
110+
* credentials, retrieve them from external sources, or generate them on-demand.</p>
111+
*
112+
* @param context The runtime context for this credential request
113+
* @return A {@link CompletionStage} that completes with the TLS credentials
114+
*/
115+
CompletionStage<TlsCredentials> tlsCredentials(ServerTlsCredentialSupplierContext context);
116+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
package io.kroxylicious.proxy.tls;
8+
9+
import java.security.PrivateKey;
10+
import java.security.cert.X509Certificate;
11+
import java.util.Optional;
12+
13+
import edu.umd.cs.findbugs.annotations.NonNull;
14+
15+
/**
16+
* <p>Runtime context provided to {@link ServerTlsCredentialSupplier} instances when
17+
* TLS credentials are requested.</p>
18+
*
19+
* <p>This context provides access to runtime information and a factory method for
20+
* creating validated {@link TlsCredentials} instances from JDK cryptographic objects.
21+
* The context is implemented by the Kroxylicious runtime and passed to the supplier's
22+
* {@link ServerTlsCredentialSupplier#tlsCredentials(ServerTlsCredentialSupplierContext)} method.</p>
23+
*
24+
* <h2>Usage Example</h2>
25+
* <pre>{@code
26+
* public class MyCredentialSupplier implements ServerTlsCredentialSupplier {
27+
* private final PrivateKey defaultKey;
28+
* private final X509Certificate[] defaultChain;
29+
*
30+
* @Override
31+
* public CompletionStage<TlsCredentials> tlsCredentials(ServerTlsCredentialSupplierContext context) {
32+
* TlsCredentials creds = context.tlsCredentials(defaultKey, defaultChain);
33+
* return CompletableFuture.completedFuture(creds);
34+
* }
35+
* }
36+
* }</pre>
37+
*/
38+
public interface ServerTlsCredentialSupplierContext {
39+
40+
/**
41+
* <p>Returns TLS information about the client-to-proxy connection, if available.</p>
42+
*
43+
* <p>This provides access to the client's TLS certificate (if client authentication
44+
* was performed) and the proxy's server certificate that was presented to the client.
45+
* This information can be used to make credential selection decisions based on
46+
* client identity or other TLS handshake data.</p>
47+
*
48+
* @return Optional containing the client TLS context, or empty if TLS is not in use
49+
* or if the handshake has not yet completed
50+
*/
51+
@NonNull
52+
Optional<ClientTlsContext> clientTlsContext();
53+
54+
/**
55+
* <p>Creates a {@link TlsCredentials} instance from the given private key and certificate chain.</p>
56+
*
57+
* <p>This factory method validates the provided credentials before creating the
58+
* {@link TlsCredentials} instance. The validation ensures that:</p>
59+
* <ul>
60+
* <li>The certificate chain is structurally valid</li>
61+
* <li>The private key matches the leaf certificate's public key</li>
62+
* </ul>
63+
*
64+
* <p>The plugin is responsible for loading and parsing the credentials from whatever
65+
* source and format it uses (PEM files, PKCS12 keystores, HSMs, etc.).</p>
66+
*
67+
* @param key The private key corresponding to the leaf certificate.
68+
* @param certificateChain The certificate chain, starting with the leaf certificate
69+
* and including any intermediate certificates up to (but not including) the root CA.
70+
* @return Validated TlsCredentials instance
71+
* @throws IllegalArgumentException if the key does not match the certificate or the chain is invalid
72+
*/
73+
@NonNull
74+
TlsCredentials tlsCredentials(@NonNull PrivateKey key, @NonNull X509Certificate[] certificateChain);
75+
}

0 commit comments

Comments
 (0)