Skip to content

Commit 31bb26b

Browse files
authored
Allow SaslInspection to function as a barrier (kroxylicious#2951)
The `SaslInspection` filter was purely about inspecting the SASL exchange. This meant that any client that was not configured do SASL authentication would be able to make any other requests it likes which the proxy would forward to the broker. A broker configured with a `required` JAAS `LoginModule` would then reject the request. So in a sense no harm done. However: * It's very confusing when a client not configured to use SASL behaves like this: nothing in the proxy (in particular neither the `SaslInspection` nor `Authentication` filters) will log either the requests or the responses (which will be error responses with `SASL_AUTHENTICATION_FAILED` error code). But this seems like an important security event which the proxy should explicitly know about. * It also means that those requests will transit the filter chain before being ultimately rejected by the broker. Any side-effects caused by those filters would be applied. It seems likely that this could lead to subtle errors because of the expectation by a person configuring the proxy that the inspection filter prevents this kind of thing. For these reasons it seems sensible to allow the SASL inspection filter to function as a security barrier (based on configuration), rejecting on its own account any requests except ApiVersions, SaslHandshake and SaslAuthenticate, which are made prior to a successful authentication. Because SaslInspection has been public API since 0.17.0 we have to disable this behaviour by default. Signed-off-by: Tom Bentley <[email protected]>
1 parent f29c41e commit 31bb26b

File tree

6 files changed

+271
-70
lines changed

6 files changed

+271
-70
lines changed

kroxylicious-filters/kroxylicious-sasl-inspection/src/main/java/io/kroxylicious/filters/sasl/inspection/Config.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919
* {@link SaslObserverFactory#transmitsCredentialInCleartext()} values of false.
2020
* @param subjectBuilder The name of a plugin class implementing {@link io.kroxylicious.proxy.authentication.SaslSubjectBuilderService}
2121
* @param subjectBuilderConfig The configuration for the SaslSubjectBuilderService.
22+
* @param requireAuthentication If true then successful authentication will be required before the filter
23+
* will forward any requests other than those strictly required to perform SASL authentication.
24+
* If false then the filter will forward all requests regardless of whether
25+
* SASL authentication has been attempted or was successful. Defaults to false.
2226
*/
2327
public record Config(@Nullable Set<String> enabledMechanisms,
2428
@Nullable String subjectBuilder,
25-
@Nullable Object subjectBuilderConfig) {}
29+
@Nullable Object subjectBuilderConfig,
30+
@Nullable Boolean requireAuthentication) {}

kroxylicious-filters/kroxylicious-sasl-inspection/src/main/java/io/kroxylicious/filters/sasl/inspection/SaslInspection.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@ public CompletionStage<Subject> buildSaslSubject(Context context) {
5050
};
5151
private @Nullable Map<String, SaslObserverFactory> observerFactoryMap;
5252
private @Nullable SaslSubjectBuilder subjectBuilder;
53+
private boolean authenticationRequired = false;
5354

5455
@Override
5556
public Void initialize(FilterFactoryContext context,
5657
@Nullable Config config)
5758
throws PluginConfigurationException {
5859
observerFactoryMap = buildEnabledObserverFactoryMap(context, config);
5960
subjectBuilder = buildSubjectBuilder(context, config);
61+
authenticationRequired = config != null && config.requireAuthentication() != null && config.requireAuthentication();
6062
return null;
6163
}
6264

@@ -78,7 +80,7 @@ private static SaslSubjectBuilder buildSubjectBuilder(FilterFactoryContext conte
7880
public Filter createFilter(FilterFactoryContext context, @Nullable Void unused) {
7981
Objects.requireNonNull(observerFactoryMap);
8082
Objects.requireNonNull(subjectBuilder);
81-
return new SaslInspectionFilter(observerFactoryMap, subjectBuilder);
83+
return new SaslInspectionFilter(observerFactoryMap, subjectBuilder, authenticationRequired);
8284
}
8385

8486
private Map<String, SaslObserverFactory> buildEnabledObserverFactoryMap(FilterFactoryContext context,

kroxylicious-filters/kroxylicious-sasl-inspection/src/main/java/io/kroxylicious/filters/sasl/inspection/SaslInspectionFilter.java

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.apache.kafka.common.message.SaslAuthenticateResponseData;
2121
import org.apache.kafka.common.message.SaslHandshakeRequestData;
2222
import org.apache.kafka.common.message.SaslHandshakeResponseData;
23+
import org.apache.kafka.common.protocol.ApiKeys;
2324
import org.apache.kafka.common.protocol.ApiMessage;
2425
import org.apache.kafka.common.protocol.Errors;
2526
import org.slf4j.Logger;
@@ -30,11 +31,10 @@
3031
import io.kroxylicious.proxy.authentication.Subject;
3132
import io.kroxylicious.proxy.authentication.SubjectBuildingException;
3233
import io.kroxylicious.proxy.filter.FilterContext;
34+
import io.kroxylicious.proxy.filter.RequestFilter;
3335
import io.kroxylicious.proxy.filter.RequestFilterResult;
3436
import io.kroxylicious.proxy.filter.ResponseFilterResult;
35-
import io.kroxylicious.proxy.filter.SaslAuthenticateRequestFilter;
3637
import io.kroxylicious.proxy.filter.SaslAuthenticateResponseFilter;
37-
import io.kroxylicious.proxy.filter.SaslHandshakeRequestFilter;
3838
import io.kroxylicious.proxy.filter.SaslHandshakeResponseFilter;
3939
import io.kroxylicious.proxy.tag.VisibleForTesting;
4040
import io.kroxylicious.proxy.tls.ClientTlsContext;
@@ -53,9 +53,8 @@
5353
*/
5454
class SaslInspectionFilter
5555
implements
56-
SaslHandshakeRequestFilter,
56+
RequestFilter,
5757
SaslHandshakeResponseFilter,
58-
SaslAuthenticateRequestFilter,
5958
SaslAuthenticateResponseFilter {
6059

6160
private static final Logger LOGGER = LoggerFactory.getLogger(SaslInspectionFilter.class);
@@ -65,20 +64,50 @@ class SaslInspectionFilter
6564

6665
private final Map<String, SaslObserverFactory> observerFactoryMap;
6766
private final SaslSubjectBuilder subjectBuilder;
67+
private final boolean authenticationRequired;
6868

6969
private State currentState = State.start();
7070

7171
SaslInspectionFilter(Map<String, SaslObserverFactory> mechanismFactories,
72-
SaslSubjectBuilder subjectBuilder) {
72+
SaslSubjectBuilder subjectBuilder,
73+
boolean authenticationRequired) {
7374
this.observerFactoryMap = Objects.requireNonNull(mechanismFactories, "mechanismFactories");
7475
this.subjectBuilder = Objects.requireNonNull(subjectBuilder, "subjectBuilder");
76+
this.authenticationRequired = authenticationRequired;
7577
}
7678

7779
@Override
78-
public CompletionStage<RequestFilterResult> onSaslHandshakeRequest(short apiVersion,
79-
RequestHeaderData header,
80-
SaslHandshakeRequestData request,
81-
FilterContext context) {
80+
public CompletionStage<RequestFilterResult> onRequest(ApiKeys apiKey, RequestHeaderData header, ApiMessage request, FilterContext context) {
81+
return switch (apiKey) {
82+
case API_VERSIONS -> context.forwardRequest(header, request);
83+
case SASL_AUTHENTICATE -> onSaslAuthenticateRequest(header.requestApiVersion(), header, (SaslAuthenticateRequestData) request, context);
84+
case SASL_HANDSHAKE -> onSaslHandshakeRequest(header, (SaslHandshakeRequestData) request, context);
85+
default -> {
86+
if (!authenticationRequired || currentState.clientIsAuthenticated()) {
87+
yield context.forwardRequest(header, request);
88+
}
89+
else {
90+
String disposition;
91+
if (currentState instanceof State.RequiringAuthenticateRequest) {
92+
disposition = "attempted";
93+
}
94+
else {
95+
disposition = "completed";
96+
}
97+
LOGGER.info("{}: Client attempted {} request without having {} SASL authentication", context.sessionId(), apiKey, disposition);
98+
99+
yield context.requestFilterResultBuilder()
100+
.errorResponse(header, request, Errors.SASL_AUTHENTICATION_FAILED.exception())
101+
.withCloseConnection()
102+
.completed();
103+
}
104+
}
105+
};
106+
}
107+
108+
CompletionStage<RequestFilterResult> onSaslHandshakeRequest(RequestHeaderData header,
109+
SaslHandshakeRequestData request,
110+
FilterContext context) {
82111
if (currentState instanceof State.ExpectingHandshakeRequestState handshakeRequestState) {
83112
var saslObserverFactory = observerFactoryMap.get(request.mechanism());
84113
SaslObserver saslObserver;
@@ -177,11 +206,10 @@ private CompletionStage<ResponseFilterResult> processFailedHandshakeResponse(Res
177206
.completed();
178207
}
179208

180-
@Override
181-
public CompletionStage<RequestFilterResult> onSaslAuthenticateRequest(short apiVersion,
182-
RequestHeaderData header,
183-
SaslAuthenticateRequestData request,
184-
FilterContext context) {
209+
CompletionStage<RequestFilterResult> onSaslAuthenticateRequest(short apiVersion,
210+
RequestHeaderData header,
211+
SaslAuthenticateRequestData request,
212+
FilterContext context) {
185213
if (this.currentState instanceof State.RequiringAuthenticateRequest state) {
186214
try {
187215
var acquiredAuthorizationId = state.saslObserver().clientResponse(request.authBytes());
@@ -229,8 +257,23 @@ public CompletionStage<RequestFilterResult> onSaslAuthenticateRequest(short apiV
229257
@Override
230258
public CompletionStage<ResponseFilterResult> onSaslAuthenticateResponse(short apiVersion,
231259
ResponseHeaderData header,
232-
SaslAuthenticateResponseData response,
260+
SaslAuthenticateResponseData brokerResponse,
233261
FilterContext context) {
262+
return this.handleSaslAuthenticateResponse(header, brokerResponse, context)
263+
.thenApply(clientFacingResponse -> {
264+
if (currentState.clientIsAuthenticated()) {
265+
LOGGER.info("Authentication successful");
266+
}
267+
else {
268+
LOGGER.info("Authentication NOT successful (yet)");
269+
}
270+
return clientFacingResponse;
271+
});
272+
}
273+
274+
CompletionStage<ResponseFilterResult> handleSaslAuthenticateResponse(ResponseHeaderData header,
275+
SaslAuthenticateResponseData response,
276+
FilterContext context) {
234277
if (this.currentState instanceof State.AwaitingAuthenticateResponse state) {
235278
try {
236279
state.saslObserver().serverChallenge(response.authBytes());

kroxylicious-filters/kroxylicious-sasl-inspection/src/main/java/io/kroxylicious/filters/sasl/inspection/State.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ static RequiringHandshakeRequest start() {
3838
return new RequiringHandshakeRequest();
3939
}
4040

41+
default boolean clientIsAuthenticated() {
42+
return false;
43+
}
44+
4145
sealed interface ExpectingHandshakeRequestState extends State permits RequiringHandshakeRequest, AllowingHandshakeRequest {
4246
/**
4347
* Transition to the next state.
@@ -151,6 +155,11 @@ final class AllowingHandshakeRequest implements ExpectingHandshakeRequestState {
151155
private AllowingHandshakeRequest() {
152156
}
153157

158+
@Override
159+
public boolean clientIsAuthenticated() {
160+
return true;
161+
}
162+
154163
/**
155164
* Transition to the next state.
156165
*
@@ -171,6 +180,11 @@ public AwaitingHandshakeResponse nextState(SaslObserver saslObserver) {
171180
final class DisallowingAuthenticateRequest implements State {
172181
private DisallowingAuthenticateRequest() {
173182
}
183+
184+
@Override
185+
public boolean clientIsAuthenticated() {
186+
return true;
187+
}
174188
}
175189

176190
enum NegotiationType {

0 commit comments

Comments
 (0)