Skip to content

Support routing based on TLS client certificate presence/signer for mixed-auth endpoints on shared port#1769

Merged
jfallows merged 7 commits into
aklivity:developfrom
akrambek:feature/1697
May 21, 2026
Merged

Support routing based on TLS client certificate presence/signer for mixed-auth endpoints on shared port#1769
jfallows merged 7 commits into
aklivity:developfrom
akrambek:feature/1697

Conversation

@akrambek
Copy link
Copy Markdown
Contributor

Fixes #1697

@akrambek akrambek requested a review from jfallows May 19, 2026 16:40
Comment on lines +83 to +102

/**
* Resolves the named trusted certificate entries from the vault by alias.
* <p>
* Used by callers that need to identify which configured trust alias signed a peer
* certificate (e.g. for routing decisions), without exposing the underlying certificate
* contents through configuration. Implementations should return only the aliases that
* resolve to a trusted certificate entry in the vault; aliases that do not resolve are
* omitted from the returned map.
* </p>
*
* @param certRefs list of vault entry names identifying the trusted certificates to resolve
* @return a map from resolved alias to the corresponding X.509 certificate, or {@code null}
* if no aliases could be resolved
*/
default Map<String, X509Certificate> resolveTrust(
List<String> certRefs)
{
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd like us to try to solve this without changing the VaultHandler abstraction if possible.
See comments below.

Comment on lines +1686 to +1706

boolean clientCertPresent = false;
List<String> clientCertTrustAliases = null;
try
{
Certificate[] peerCerts = tlsSession.getPeerCertificates();
if (peerCerts.length > 0 && binding != null)
{
X509Certificate leaf = (X509Certificate) peerCerts[0];
clientCertPresent = true;
clientCertTrustAliases = binding.resolveTrustAliases(leaf.getIssuerX500Principal());
}
}
catch (SSLPeerUnverifiedException ex)
{
// no client cert presented under mutual: requested
}

final TlsRouteConfig route = binding != null
? binding.resolve(authorization, tlsHostname, tlsProtocol, port, clientCertPresent, clientCertTrustAliases)
: null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The current VaultHandler abstract maps from aliases to TrustManagerFactory which we use to configure the SSLEngine.

Once that handshake completes successfully, we then want to know if we trust the client certificate to follow a specific route.

I think we can do that by creating a different TrustManagerFactory instance (via the VaultHandler) per route, for just the trusted aliases on that route. Then we can assess if the client certificate is trusted by using the TrustManager[] for that route, iterating over it to find the X509TrustManager instances, and checking if the client is trusted, if so follow the route.
For the mutual:none case, we just need to check that the client did not present a peer certificate chain.

So in terms of APIs, we can probably just pass the client certificate chain to binding.resolve(...) and let it figure out which routes are valid using the TrustManagerFactory for the trusted routes (mutual:required), and verifying no client certificate chain for the mutual:none routes.

Comment on lines +1685 to +1693
Certificate[] clientCerts = null;
try
{
clientCerts = tlsSession.getPeerCertificates();
}
catch (SSLPeerUnverifiedException ex)
{
// no client cert presented under mutual: requested
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lets move this to a static method to reduce the noise here regarding handling of the exception.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, let's only attempt to get the peer certificates if the options mutual is not none, i.e. if it is required or requested. This will avoid triggering a predictable exception in the common case when mutual TLS is not configured.

Comment on lines +303 to +322
@Test
@Configuration("server.mutual.signer.routed.yaml")
@Specification({
"${net}/server.mutual.auth/client",
"${app}/server.mutual.auth/server"})
public void shouldRouteByClientCertSigner() throws Exception
{
k3po.finish();
}

@Test
@Configuration("server.mutual.signer.routed.yaml")
@Specification({
"${net}/server.mutual.cert.absent/client",
"${app}/server.mutual.cert.absent/server"})
public void shouldRouteWhenClientCertAbsent() throws Exception
{
k3po.finish();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's also add a negative test, where the client certificate is not one of the trusted aliases, so even though the TLS handshake succeeds, we reject because there is no valid route.

return TlsState.closed(state) ? NULL_STREAM : stream;
}

private static Certificate[] peerCertificates(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rename to clientCertificates and move private static method after private non-static methods.

@@ -0,0 +1,30 @@
#
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should have a peer script, and a corresponding spec IT method.

@jfallows jfallows merged commit 115457a into aklivity:develop May 21, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support routing based on TLS client certificate presence/signer for mixed-auth endpoints on shared port

2 participants