Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
5691897
ssl client principal propagation
ujjwalkalia Jun 2, 2025
ba4bd94
additional changes
ujjwalkalia Jun 4, 2025
1810078
additional changes
ujjwalkalia Jun 4, 2025
d2f3c92
Update quay.io/apicurio/apicurio-registry-mem Docker tag to v2.6.9.Fi…
kroxylicious-robot Jun 2, 2025
7f0694b
Revert "Update quay.io/apicurio/apicurio-registry-mem Docker tag to v…
k-wall Jun 2, 2025
17746fb
Bump org.junit:junit-bom from 5.12.2 to 5.13.0
dependabot[bot] Jun 2, 2025
952c4be
Bump org.apache.maven.plugins:maven-clean-plugin from 3.4.1 to 3.5.0
dependabot[bot] Jun 2, 2025
285e181
Bump dawidd6/action-download-artifact from 8 to 10
dependabot[bot] Jun 2, 2025
f129802
Enable remote debugging of the Operator
SamBarker May 30, 2025
deec229
Mention debug access in operator DEV_GUIDE.md
SamBarker Jun 2, 2025
18cb11c
PR Review: typo fix
SamBarker Jun 3, 2025
67c59f1
Typo and grammer fixes
SamBarker Jun 3, 2025
85bbde8
Fix #2239: Downstream / upstream kafka message counting metrics
k-wall May 29, 2025
8720ace
Use api key/version from Frame rather than re-reading the written res…
k-wall Jun 2, 2025
3fa4031
sonar
k-wall Jun 2, 2025
c0465cc
bug fix - deprecated metrics wrong way about
k-wall Jun 2, 2025
30da5d3
sonar
k-wall Jun 3, 2025
f09da5f
more sonar
k-wall Jun 3, 2025
2e14c80
add unit test for new metrics handler
k-wall Jun 3, 2025
cc0616a
Addressing code review comments.
k-wall Jun 3, 2025
de686a9
Make io.kroxylicious.proxy.internal.net.EndpointBinding#nodeId part o…
k-wall Jun 3, 2025
8df2041
adding short circuit IT
k-wall Jun 3, 2025
63daeb9
tidy it
k-wall Jun 3, 2025
47544a6
adding IT checking metrics are discriminated by node_id
k-wall Jun 3, 2025
8cc665d
Cert type thing in the system test
SamBarker May 27, 2025
3ceeea3
Updating the cert causes the podTemplate updated.
SamBarker May 28, 2025
f1c3b7d
Formatting!
SamBarker May 28, 2025
9048899
some fixes to the 2211 tactical PR
franvila May 28, 2025
f6f49a9
revert some changes
franvila May 28, 2025
376ce54
remove unused imports
franvila May 28, 2025
3ebf256
sort importsç
franvila May 28, 2025
bff131d
adding cert-manager namespace to the logCollector
franvila May 28, 2025
57ba25e
adding cert-manager namespace to the logCollector
franvila May 28, 2025
a351812
move cleaup afterEach to afterAll
franvila May 28, 2025
85bcd12
Fix formatting
SamBarker May 29, 2025
a70a8a1
Cope with optional lists
SamBarker May 29, 2025
25dae62
Cert type thing in the system test
SamBarker May 27, 2025
ccb2396
Updating the cert causes the podTemplate updated.
SamBarker May 28, 2025
55ee776
Formatting!
SamBarker May 28, 2025
0cff548
downstream only TLS
SamBarker May 28, 2025
f272064
Include downstream TLS certs directlu
SamBarker May 29, 2025
11df083
Warning cleanup
SamBarker May 29, 2025
64e3935
use field instead of local var
SamBarker May 30, 2025
1131682
Failing system test for downstream trust anchor
SamBarker May 30, 2025
41b0376
Add the ability to generate a slug from just metadata
SamBarker May 30, 2025
396c894
Only log creation if it's happening.
SamBarker May 30, 2025
89875e2
Debug logging.
SamBarker May 30, 2025
353bcc4
Tactical fix to include TLS config in VKC reconcilation.
SamBarker May 30, 2025
4656ddd
skip null trust anchors
SamBarker Jun 1, 2025
1acfc35
Sonar: build slug only if required
SamBarker Jun 2, 2025
5f947ce
Sonar: use more specific return type
SamBarker Jun 2, 2025
506119c
Sonar: Reduce complexity
SamBarker Jun 3, 2025
4c7f486
PR Review: drop log level
SamBarker Jun 3, 2025
fc45394
PR Review: add test to generate checksum generator
SamBarker Jun 3, 2025
d748079
Edit the right freaking resource
SamBarker Jun 3, 2025
abedcf7
Sonar nits
SamBarker Jun 3, 2025
34d668a
use the return value to make code analysis STFU
SamBarker Jun 3, 2025
1446612
stop things moaning about possible NPEs
SamBarker Jun 3, 2025
5d62ff8
PR Review: initialise workflowContext once
SamBarker Jun 3, 2025
c69c058
Remove dead code
SamBarker Jun 3, 2025
09dc790
Implement LoadBalancer ingress
robobario May 21, 2025
e84334b
Simplify mocking
SamBarker Jun 4, 2025
ce9274c
Add whitespace around assignment.
SamBarker Jun 4, 2025
8442679
Stop making calls to the API server when we don't need to.
SamBarker Jun 4, 2025
1769b09
Restore operator dockerbuild
SamBarker Jun 4, 2025
aef28a6
Fix #2266: Ensure connection is close if SNI hostname fails to resolv…
k-wall Jun 4, 2025
4b07f93
Added connection metrics
ShubhamRwt Jun 4, 2025
0bd59dd
ability to keep the label field empty
ShubhamRwt Jun 4, 2025
546a8bd
fix sonar issues
ShubhamRwt Jun 4, 2025
a31e3e8
fix test
ShubhamRwt Jun 4, 2025
f5425b3
Revert changes
ShubhamRwt Jun 4, 2025
2b97035
Add suggestions by Sam
ShubhamRwt Jun 5, 2025
63a3ac7
use greaterThanOrEqual() method
ShubhamRwt Jun 5, 2025
d934740
update w.r.t to Keith's changes for SNI handling
ShubhamRwt Jun 5, 2025
d7ae6e8
Added suggestions by Keith and Sam
ShubhamRwt Jun 5, 2025
b5275ef
Update localstack/localstack Docker tag to v4.5.0 (#2274)
kroxylicious-robot Jun 5, 2025
ce6be81
Strimzi Quickstart For Examples (#2275)
k-wall Jun 5, 2025
9d91722
Use SNI node identification for TLS ClusterIP ingress
robobario Jun 4, 2025
d0fa006
fix: run-operator failed with empty minikube
robobario Jun 4, 2025
0c9c042
Reduce max length of KafkaProxy metadata.name to 50
robobario Jun 5, 2025
bed49ed
Remove duplicate dependency definition
SamBarker Jun 6, 2025
937a5aa
Push operator image for release version
SamBarker Jun 6, 2025
b0e4f40
[System Tests] OLM installation (#2273)
franvila Jun 6, 2025
834dfcb
Merge branch 'main' into ssl_principal_propagation
ujjwalkalia Jun 8, 2025
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
Expand Up @@ -42,6 +42,20 @@ public interface FilterContext {
@Nullable
String sniHostname();

/**
* Returns the principal name from the client's TLS certificate.
* <p>
* For X.509 certificates, this is typically the Distinguished Name (DN) in format
* such as "CN=clientName, O=organizationName, C=US". This information can be used
* by filters for client authentication, authorization decisions, or audit logging.
*
* @return the client certificate principal as a string
*/
@Nullable
default String downstreamCertificatePrincipal() {
return null;
}

/**
* Creates a builder for a request filter result objects. This object encapsulates
* the request to forward and optionally orders for actions such as closing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class FilterHandler extends ChannelDuplexHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(FilterHandler.class);
private final long timeoutMs;
private final String sniHostname;
private final String downstreamCertificatePrincipal;
private final VirtualClusterModel virtualClusterModel;
private final Channel inboundChannel;
private final FilterAndInvoker filterAndInvoker;
Expand All @@ -67,10 +68,12 @@ public class FilterHandler extends ChannelDuplexHandler {
private ChannelHandlerContext ctx;
private PromiseFactory promiseFactory;

public FilterHandler(FilterAndInvoker filterAndInvoker, long timeoutMs, String sniHostname, VirtualClusterModel virtualClusterModel, Channel inboundChannel) {
public FilterHandler(FilterAndInvoker filterAndInvoker, long timeoutMs, String sniHostname, String downstreamCertificatePrincipal,
VirtualClusterModel virtualClusterModel, Channel inboundChannel) {
this.filterAndInvoker = Objects.requireNonNull(filterAndInvoker);
this.timeoutMs = Assertions.requireStrictlyPositive(timeoutMs, "timeout");
this.sniHostname = sniHostname;
this.downstreamCertificatePrincipal = downstreamCertificatePrincipal;
this.virtualClusterModel = virtualClusterModel;
this.inboundChannel = inboundChannel;
}
Expand Down Expand Up @@ -475,6 +478,12 @@ public String sniHostname() {
return sniHostname;
}

@Nullable
@Override
public String downstreamCertificatePrincipal() {
return downstreamCertificatePrincipal;
}

public String getVirtualClusterName() {
return virtualClusterModel.getClusterName();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import java.util.List;
import java.util.Objects;

import javax.net.ssl.SSLPeerUnverifiedException;

import org.apache.kafka.common.message.ApiVersionsRequestData;
import org.apache.kafka.common.message.ApiVersionsResponseData;
import org.apache.kafka.common.message.ApiVersionsResponseDataJsonConverter;
Expand All @@ -34,6 +36,7 @@
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SniCompletionEvent;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;

import io.kroxylicious.proxy.filter.FilterAndInvoker;
import io.kroxylicious.proxy.filter.NetFilter;
Expand Down Expand Up @@ -68,6 +71,7 @@ public class KafkaProxyFrontendHandler

private static final String NET_FILTER_INVOKED_IN_WRONG_STATE = "NetFilterContext invoked in wrong session state";
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaProxyFrontendHandler.class);
private static final String ANONYMOUS = "ANONYMOUS";

/** Cache ApiVersions response which we use when returning ApiVersions ourselves */
private static final ApiVersionsResponseData API_VERSIONS_RESPONSE;
Expand All @@ -87,6 +91,21 @@ public class KafkaProxyFrontendHandler
private boolean pendingClientFlushes;
private @Nullable AuthenticationEvent authentication;
private @Nullable String sniHostname;
private @Nullable String downstreamCertificatePrincipal;

@Nullable
public String getDownstreamCertificatePrincipal() {
return downstreamCertificatePrincipal;
}

@VisibleForTesting
SslHandler getSslHandler(ChannelHandlerContext ctx) {
if (ctx == null) {
throw new IllegalStateException("No context available");
}
SslHandler sslHandler = ctx.pipeline().get(SslHandler.class);
return sslHandler;
}

// Flag if we receive a channelReadComplete() prior to outbound connection activation
// so we can perform the channelReadComplete()/outbound flush & auto_read
Expand Down Expand Up @@ -171,6 +190,18 @@ public void userEventTriggered(
else if (event instanceof AuthenticationEvent authenticationEvent) {
this.authentication = authenticationEvent;
}
else if (event instanceof SslHandshakeCompletionEvent sslHandshakeCompletionEvent) {
if (sslHandshakeCompletionEvent.isSuccess()) {
SslHandler sslHandler = getSslHandler(ctx);
try {
downstreamCertificatePrincipal = sslHandler.engine().getSession().getPeerPrincipal().toString();
}
catch (SSLPeerUnverifiedException e) {
LOGGER.debug("No client principal received, setting principal as ANONYMOUS");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe add channel information in the logs ?
Lets check how kroxylicious adds debug logs

downstreamCertificatePrincipal = ANONYMOUS;
}
}
}
super.userEventTriggered(ctx, event);
}

Expand Down Expand Up @@ -640,6 +671,7 @@ private void addFiltersToPipeline(
protocolFilter,
20000,
sniHostname,
downstreamCertificatePrincipal,
virtualClusterModel,
inboundChannel));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ protected void buildChannel(Filter... filters) {
return d2;
})) // reverses order
.stream()
.map(f -> new FilterHandler(getOnlyElement(FilterAndInvoker.build(f.getClass().getSimpleName(), f)), timeoutMs, null, testVirtualCluster, inboundChannel))
.map(f -> new FilterHandler(getOnlyElement(FilterAndInvoker.build(f.getClass().getSimpleName(), f)), timeoutMs, null, null, testVirtualCluster, inboundChannel))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should add parametrised test in FilterHandlerTest also to cover non-null scenario.
Add parameterised test for inheritors if needed.

.map(ChannelHandler.class::cast);
var handlers = Stream.concat(channelProcessors, filterHandlers);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
*/
package io.kroxylicious.proxy.internal;

import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;

import org.apache.kafka.common.message.ApiVersionsRequestData;
import org.apache.kafka.common.message.MetadataRequestData;
import org.apache.kafka.common.message.RequestHeaderData;
Expand Down Expand Up @@ -43,6 +48,8 @@
import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol;
import io.netty.handler.ssl.SniCompletionEvent;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;

import io.kroxylicious.proxy.filter.NetFilter;
import io.kroxylicious.proxy.frame.DecodedFrame;
Expand Down Expand Up @@ -76,9 +83,12 @@ class KafkaProxyFrontendHandlerTest {

public static final String SNI_HOSTNAME = "external.example.com";
public static final String CLUSTER_HOST = "internal.example.org";
public static final String CLIENT_PRINCIPAL = "CN=client,O=example";
public static final int CLUSTER_PORT = 9092;
EmbeddedChannel inboundChannel;
EmbeddedChannel outboundChannel;
SSLSession sslSession = mock(SSLSession.class);
Principal mockPrincipal = mock(Principal.class);

int corrId = 0;
private final AtomicReference<NetFilter.NetFilterContext> connectContext = new AtomicReference<>();
Expand Down Expand Up @@ -123,12 +133,14 @@ public void closeChannel() {
public static List<Arguments> provideArgsForExpectedFlow() {
var result = new ArrayList<Arguments>();
boolean[] tf = { true, false };
for (boolean sslConfigured : tf) {
for (boolean haProxyConfigured : tf) {
for (boolean saslOffloadConfigured : tf) {
for (boolean sendApiVersions : tf) {
for (boolean sendSasl : tf) {
result.add(Arguments.of(sslConfigured, haProxyConfigured, saslOffloadConfigured, sendApiVersions, sendSasl));
for (boolean clientAuthConfigured : tf) {
for (boolean sslConfigured : tf) {
for (boolean haProxyConfigured : tf) {
for (boolean saslOffloadConfigured : tf) {
for (boolean sendApiVersions : tf) {
for (boolean sendSasl : tf) {
result.add(Arguments.of(clientAuthConfigured, sslConfigured, haProxyConfigured, saslOffloadConfigured, sendApiVersions, sendSasl));
}
}
}
}
Expand Down Expand Up @@ -310,12 +322,29 @@ ChannelFuture initConnection(String remoteHost, int remotePort, Bootstrap bootst
outboundChannel.pipeline().fireChannelRegistered();
return outboundChannel.newPromise();
}

@Override
SslHandler getSslHandler(ChannelHandlerContext ctx) {
// We don't need to test SSL here, so just return a mock
SSLEngine sslEngine = mock(SSLEngine.class);
when(sslEngine.getSession()).thenReturn(sslSession);
when(mockPrincipal.toString()).thenReturn(CLIENT_PRINCIPAL);
return new SslHandler(sslEngine) {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof SniCompletionEvent) {
ctx.fireUserEventTriggered(evt);
}
}
};
}
};
}

/**
* Test the normal flow, in a number of configurations.
*
* @param clientAuthConfigured whether mTLS is configured
* @param sslConfigured Whether SSL is configured
* @param haProxyConfigured
* @param saslOffloadConfigured
Expand All @@ -324,7 +353,8 @@ ChannelFuture initConnection(String remoteHost, int remotePort, Bootstrap bootst
*/
@ParameterizedTest
@MethodSource("provideArgsForExpectedFlow")
void expectedFlow(boolean sslConfigured,
void expectedFlow(boolean clientAuthConfigured,
boolean sslConfigured,
boolean haProxyConfigured,
boolean saslOffloadConfigured,
boolean sendApiVersions,
Expand Down Expand Up @@ -379,11 +409,37 @@ void expectedFlow(boolean sslConfigured,
var handler = handler(filter, dp, endpointBinding);
initialiseInboundChannel(handler);

if (clientAuthConfigured) {
try {
when(sslSession.getPeerPrincipal()).thenReturn(mockPrincipal);
}
catch (SSLPeerUnverifiedException e) {
throw new RuntimeException(e);
}
}
else {
try {
when(sslSession.getPeerPrincipal()).thenThrow(new SSLPeerUnverifiedException("No peer certificate"));
}
catch (SSLPeerUnverifiedException e) {
throw new RuntimeException(e);
}
}

if (sslConfigured) {
// Simulate the SSL handler
inboundChannel.pipeline().fireUserEventTriggered(new SniCompletionEvent(SNI_HOSTNAME));
inboundChannel.pipeline().fireUserEventTriggered(SslHandshakeCompletionEvent.SUCCESS);
}

if (sslConfigured) {
if (clientAuthConfigured) {
assertEquals(CLIENT_PRINCIPAL, handler.getDownstreamCertificatePrincipal());
}
else {
assertEquals("ANONYMOUS", handler.getDownstreamCertificatePrincipal());
}
}
assertThat(proxyChannelStateMachine.state()).isExactlyInstanceOf(ProxyChannelState.ClientActive.class);

if (haProxyConfigured) {
Expand Down