Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Plugin API for selecting TLS client credentials for proxy-to-server connection

This proposes a new plugin API for allowing the selection of the TLS client credentials to be used for the connection between the proxy and a Kafka server.
It makes use of [proposal-004](proposal-004) for the terminology used.

## Current situation

Currently the end user configures the client TLS information to be used for connections between the proxy and a target cluster.
This means that the client TLS certificate used cannot depend on information known at runtime.
This includes, in particular information about the client, such as the TLS client certificate that it provided to the proxy.

## Motivation

The lack of API support means all proxied clients have the same TLS identity from the broker's point of view.
This means a broker using the `TLS` value for `security.protocol` cannot distinguish between those clients.

Goals:

* Enable the selection of TLS client certificates at runtime.

## Proposal


### API for selecting target cluster TLS credentials

We will add a new plugin interface, `ServerTlsCredentialSupplierFactory`.
It will use the usual Kroxylicious plugin mechanism, leveraging `java.util.Service`-based discovery.
However, this plugin is not the same thing as a `FilterFactory`.
Rather, an implementation class will be defined on the `TargetCluster` configuration object, and instantiated once for each target cluster.
The TargetCluster's `tls` object will gain a `tlsCredentialSupplier` property, supporting `type` and `config` properties (similarly to how filters are configured).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sidebar: not for this proposal. Wonder where we would put this in the operator API. In operator land KafkaService has the upstream tls details, but we can reuse a KafkaService across virtual kafka clusters. Would we put it on KafkaService, so users need multiple KafkaServices if they want different selection logic per virtual cluster. Or put it on VirtualCluster, with the downside that we spread out the upstream TLS settings across two CRs.

The interface itself is declared like this:

```java
package io.kroxylicious.proxy.tls;

/**
* <p>A pluggable source of {@link ServerTlsCredentialSupplier} instances.</p>
* <p>ServerTlsCredentialSupplierFactories are:</p>
* <ul>
* <li>{@linkplain java.util.ServiceLoader service} implementations provided by plugin authors</li>
* <li>called by the proxy runtime to {@linkplain #create(Context, Object) create} instances</li>
* </ul>
* @param <C> The type of configuration.
* @param <I> The type of initialization data.
*/
public interface ServerTlsCredentialSupplierFactory<C, I> {
I initialize(Context context, C config) throws PluginConfigurationException;
ServerTlsCredentialSupplier create(Context context, I initializationData);
default void close(I initializationData) {
}
}
```

`ServerTlsCredentialSupplierFactory` is following the convention established by `FilterFactory`, and the `Context` referenced above is similar to the `FilterFactoryContext`:

```java
interface Context {

/**
* An executor backed by the single Thread responsible for dispatching
* work to a ServerTlsCredentialSupplier instance for a channel.
* It is safe to mutate ServerTlsCredentialSupplier members from this executor.
* @return executor
* @throws IllegalStateException if the factory is not bound to a channel yet.
*/
ScheduledExecutorService filterDispatchExecutor();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call this pluginDispatchExecutor?


/**
* Gets a plugin instance for the given plugin type and name
* @param pluginClass The plugin type
* @param instanceName The plugin instance name
* @return The plugin instance
* @param <P> The plugin manager type
* @throws UnknownPluginInstanceException If the plugin could not be instantiated.
*/
<P> P pluginInstance(Class<P> pluginClass,
String instanceName)
throws UnknownPluginInstanceException;

/**
* Creates some TLS credentials for the given parameters.
* @param key The key corresponding to the given client certificate.
* @param certificateChain The client certificate corresponding to the given {@code key}, plus any intermediate certificates forming the certificate chain up to (but not including) the TLS certificate trusted by the peer.
* @return The TLS credentials instance.
* @see ServerTlsCredentialSupplier.Context#tlsCredentials(PrivateKey, Certificate[])
*/
TlsCredentials tlsCredentials(PrivateKey key,
Certificate[] certificateChain);
}

```

So what is a `ServerTlsCredentialSupplier` that this factory creates?

```java
package io.kroxylicious.proxy.tls;

import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Optional;
import java.util.concurrent.CompletionStage;

import io.kroxylicious.proxy.authentication.ClientSaslContext;

/**
* Implemented by a {@link io.kroxylicious.proxy.filter.Filter} that provides
* the credentials for the TLS connection between the proxy and the Kafka server.
Comment on lines +106 to +107
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Implemented by a {@link io.kroxylicious.proxy.filter.Filter} that provides
* the credentials for the TLS connection between the proxy and the Kafka server.
* Implemented by a class annotated with {@link io.kroxylicious.proxy.plugin.Plugin} that provides
* the credentials for the TLS connection between the proxy and the Kafka server.

I don't think the intention is for filters to implement this, is it?

*/
public interface ServerTlsCredentialSupplier {
/**
* Return the TlsCredentials for the connection.
* @param context The context.
* @return the TlsCredentials for the connection.
*/
CompletionStage<TlsCredentials> tlsCredentials(Context context);
}
```

Where the `Context` will be a inner interface:

```java
/**
* The context API for {@link ServerTlsCredentialSupplier}.
* This is implemented by the runtime for use by plugins.
*/
interface Context {
Optional<ClientTlsContext> clientTlsContext();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that currently there is no method for getting the ClientSaslContext proposed in #74). That's because that SASL info would be provided by a SASL terminator, which can currently only be invoked once a filter chain is instantiated, which happens after the proxy-server connection is established. So a cyclic dependency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be visible somewhere in a proposal, it's a bit tricky since it's cumulative over the proposals. Maybe just a note somewhere like:

Plugins will not be able to use the downstream SASL client identity proposed in 006 for TLS credential selection initially, because the filter are currently installed after the upstream connection is established.

edit: maybe in a Future Work section like I've commented elsewhere.

/**
* Returns the default credentials for this target cluster (e.g. from the proxy configuration file).
* Implementations of {@link ServerTlsCredentialSupplier} may use this as a fall-back
* or default, for example if they apply a certificiate-per-client-principal pattern
* but are being used with an anonymous principal.
* @return the default credentials, or empty if none are define.
*/
Optional<TlsCredentials> defaultTlsCredentials();

/**
* <p>Factory methods for creating TLS credentials for the given parameters.</p>
*
* <p>The equivalent method on {@code FilterFactoryContext} can be used when the credentials
* are known at plugin configuration time.</p>
*
* @param key The key corresponding to the given client certificate.
* @param certificateChain The client certificate corresponding to the given {@code key}, plus any intermediate certificates forming the certificate chain up to (but not including) the TLS certificate trusted by the peer.
* @return The TLS credentials instance.
* see io.kroxylicious.proxy.filter.ServerTlsCredentialSupplierFactory.Context#tlsCredentials(PrivateKey, Certificate[])
*/
TlsCredentials tlsCredentials(
PrivateKey key,
Certificate[] certificateChain);
}
```

The `Context` is intentionally quite narrow in terms of the information it exposes to plugin implementations, but it is extensible in the future as use cases emerge for exposing more runtime information to implementations.

And `TlsCredentials` looks like this:

```java
package io.kroxylicious.proxy.authentication;

interface TlsCredentials {
/* Intentionally empty: implemented and accessed only in the runtime */
}
```

// TODO Why not use the JDK's `X500PrivateCredential`?


## Affected/not affected projects

The `kroxylicous` repo.

## Compatibility

This change would be backwards compatible for `Filter` developers and proxy users (i.e. all existing proxy configurations files would still be valid).

# Future Work

* Enable using identity from a SASL Terminator in upstream TLS certificate selection.