Audit authn success in the SecurityRestFilter#94120
Audit authn success in the SecurityRestFilter#94120albertzaharovits merged 21 commits intoelastic:mainfrom
SecurityRestFilter#94120Conversation
|
|
||
| @Override | ||
| public void authenticationSuccess(String requestId, Authentication authentication, RestRequest request) { | ||
| public void authenticationSuccess(RestRequest request) { |
There was a problem hiding this comment.
This method must now be called in an authenticated context, when the HTTP request body is available.
...ity/src/main/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticator.java
Outdated
Show resolved
Hide resolved
|
|
||
| authenticationService.authenticate(maybeWrapRestRequest(request), ActionListener.wrap(authentication -> { | ||
| final RestRequest wrappedRequest = maybeWrapRestRequest(request); | ||
| authenticationService.authenticate(wrappedRequest, ActionListener.wrap(authentication -> { |
There was a problem hiding this comment.
This authenticationService.authenticate method is to be moved to a sooner site in the request handling flow (where it will not have access to the request body).
.../plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java
Outdated
Show resolved
Hide resolved
SecurityRestFilter
|
Pinging @elastic/es-security (Team:Security) |
| throw new ElasticsearchSecurityException("rest request attempted to inject a user", e); | ||
| } | ||
| if (authentication == null) { | ||
| throw new ElasticsearchSecurityException("Context is not authenticated"); |
There was a problem hiding this comment.
nit: can you add a "should never happen..." comment here, or just remove this in favor of an assert authentication != null ? ( I prefer the later)
There was a problem hiding this comment.
I added the comment. I strongly prefer a hard 500 when authN expectations are violated. It's going to become less of an obvious expectation (so less of an "assert" type of case) when we move the actual authN further away from this auditing point.
| public void authenticationSuccess(RestRequest request) { | ||
| final String requestId = AuditUtil.extractRequestId(securityContext.getThreadContext()); | ||
| if (requestId == null) { | ||
| throw new ElasticsearchSecurityException("Authenticated context must include request id"); |
There was a problem hiding this comment.
nit: can you add a "should never happen..." comment here, or just remove this in favor of an assert requestId != null ? ( I prefer the later)
There was a problem hiding this comment.
I added the comment, per https://github.com/elastic/elasticsearch/pull/94120/files#r1132377449
...security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java
Show resolved
Hide resolved
| } | ||
|
|
||
| /** | ||
| * This must be called after the HTTP request has been authenticated. |
There was a problem hiding this comment.
nit: not sure if there is easy access ... but if there is... can you add an assert in the code that authentication bits are present ?
There was a problem hiding this comment.
It's easier to assert authentication in the called AuditTrail#authenticationSuccess, if I get you right.
| @Override | ||
| void authenticationSuccess(Authentication authentication) { | ||
| auditTrail.authenticationSuccess(requestId, authentication, request); | ||
| // REST requests are audited in the {@code SecurityRestFilter} because they need access to the request body |
There was a problem hiding this comment.
This feels off ... it seems that we should have a reference to the AuditableRestRequest when we want to audit... even if we need to check instance type and downcast I think would be preferable to ignoring our own contract.
There was a problem hiding this comment.
I agree this is not great, but I struggle to find better options...
Fundamentally, the AuditableRequest#authenticationSuccess is called after authentication was successful, and we don't want to audit HTTP requests that way (because we do the auditing in the SecurityRestFilter#handleRequest, for the RestRequest).
It is possible to alternatively have auditTrail.authenticationSuccess(requestId, authentication, request) be a noop. Or, to not have the context.getRequest().authenticationSuccess(authentication) called in the AuthenticatorChain if the request is a HTTP request. I think these options are more hackish.
What do you think?
We could also file this objection of yours, and come back to it later when the puzzle pieces are in place?
There was a problem hiding this comment.
I think it is possible to change this method to be AuditableRequest.authenticationSuccess() and wrap the RestRequest earlier on in SecurityRestFilter. The authentication object should always be available in the threadContext (available for both AuditableRestRequest and AuditableTransportRequest) before this auditing method is called so that we don't it need to passed as an argument. In fact, having it available as both argument and threadContext is a potential duplication for bugs.
Concretely, what I am suggesting is:
- Change the
AuditableRequest#authenticationSuccessmethod to take no argument - No change to
AuditTrail#authenticationSuccessmethods - Let
AuditableRestRequestandAuditableTransportRequestto extractauthenticationobject from the threadContext and pass it down to theAuditTrail. - Wrap
RestRequestasAuditableRestRequestearlier inSecurityRestFilterso that auditing can be consistently done withrequest.authenticationSuccess.
It is a bit larger changeset. But it helps maintain a more consistent audit logging contract.
There was a problem hiding this comment.
Wrap RestRequest as AuditableRestRequest earlier in SecurityRestFilter so that auditing can be consistently done with request.authenticationSuccess.
I think this is glancing over important details.
Where are you proposing we invoke AuditableRequest#authenticationSuccess?
Currently, the callsite is in the AuthenticationService just after authn completed successfully AND has just been written to the thread context. The proposed implementation doesn't change the call site, but for REST requests only, turns it into a noop. It does that because the REST request body contents might be needed for auditing authn success and that won't be available, at the current callsite (i.e. after authN just finished and its result been written to the threadcontext). Instead, the proposed implementation, creates a new method in the AuditTrail class specifically for dealing with auditing of REST requests (to cover for the fact that now AuditableRequest#authenticationSuccess is a noop for REST requests) and calls that in the SecurityRestFilter.
I don't see an easy way to move the callsite of AuditableRequest#authenticationSuccess, which is what I think you're suggesting, because the AuditableRequest is tied to the AuthenticationService and it's lifetime doesn't go beyond the authN flow.
So maybe an option is to remove the AuditableRequest#authenticationSuccess completely and find a place to audit authN success directly via the AuditTrail class for both REST and transport requests?
There was a problem hiding this comment.
We've met to discuss this.
Yang clarified that it's OK to audit authN success in the SecurityRestFilter.
The standing question was more about the interface to achieve it:
The AuditableRequest is not available in the SecurityRestFilter as it's constructed in the AuthenticationService and not exposed. The alternative options are to either reconstruct another instance of an AuditableRequest in the SecurityRestFilter or to expose the existing one from the AuthenticationService. Both are not simple changes, and it's not even clear why they're better compared to calling the AuditTrail#authenticationSuccess method directly, as the AuditTrail is already exposed for authZ event auditing.
We also discussed whether we should instead remove the AuditableRequest#authenticationSuccess method completely (for both REST and transport request types). This is again not a simple change, because we'd have to be careful to not audit authentication success for transport requests with existing authentication.
It's a close call, but, given the constraints, we're OK to go with the proposed approach to:
- keep the
AuditableRequest#authenticationSuccessmethod around and keep invoking it in the same way, in theAuthenticationServicejust after the authN has been set in the thread context, but - make it a noop for REST requests, and instead
- directly call the
AuditTrail#authenticationSuccessin theSecurityRestFilter, where the request body is available and the authN just completed (and is set in the thread context)
There was a problem hiding this comment.
@albertzaharovits Thanks for summarizing our discussion. I think it might be useful to somehow make the summary available in the code, e.g. maybe add a comment with link to your above comment.
|
@ywangd @jakelandis ready for another round. |
ywangd
left a comment
There was a problem hiding this comment.
This is looking promosing. I have a comment along the line of maintaining the audit logging contract that @jakelandis raised. I think it is a point that worth exploring a bit more. Thanks!
...gin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java
Outdated
Show resolved
Hide resolved
| authenticate( | ||
| authListener -> authenticationService.authenticate(request, false, authListener), | ||
| authListener -> authenticationService.authenticate(request, false, authListener.delegateFailure((l, authentication) -> { | ||
| auditTrailService.get().authenticationSuccess(request); |
There was a problem hiding this comment.
Appologies, I may have been unclear. What I meant in #94120 (comment) is to audit both authentication and secondary authentication success in SecurityRestFilter, which I think is easier to follow and better visibility.
There was a problem hiding this comment.
I agree it's easier to follow, but looking closer I don't think this is going to work correctly.
auditTrailService.get().authenticationSuccess(request); looks in the thread context for the authentication, but the SecondaryAuthenticator makes it such that the thread context doesn't contain the secondary authN when it invokes the listener of authenticateAndAttachToContext.
There was a problem hiding this comment.
It is possible to have AuditTrail.authenticationSuccess taking an extra argument which specifies what key the authentication object is stored against, e.g.
void authenticationSuccess(RestRequest request, String authcContextKey);
or have it take a Function that knows how to extract authentication, e.g.
void authenticationSuccess(RestRequest request, Function<ThreadContext, Authentication> authcReader);
This would allow us to always call authenticationSuccess in the filter. It is not critical. I'll leave it to you to make the call. Thanks!
There was a problem hiding this comment.
Hmmm... yeah, I haven't thought of that.
Though if authenticationSuccess audits the "current" authentication passing a selector doesn't look elegant to me.
Alternatively, we could change the "authenticationSuccess` method to audit all the authentications, but now it's getting "smarter" and it means we'll to wait for both to complete before we invoke the method. A less smarter option is to pass the authentication or the secondary authentication as arguments, instead of using a no-arg that looks at the "current" authn.
Overall I think there isn't much difference between all of these options. I think I'll leave it to what's there right now: ie. a simple method no-arg method, that audits the "current" authentication, but that needs to be called in the SecondaryAuthenticator too.
| @Override | ||
| void authenticationSuccess(Authentication authentication) { | ||
| auditTrail.authenticationSuccess(requestId, authentication, request); | ||
| // REST requests are audited in the {@code SecurityRestFilter} because they need access to the request body |
There was a problem hiding this comment.
I think it is possible to change this method to be AuditableRequest.authenticationSuccess() and wrap the RestRequest earlier on in SecurityRestFilter. The authentication object should always be available in the threadContext (available for both AuditableRestRequest and AuditableTransportRequest) before this auditing method is called so that we don't it need to passed as an argument. In fact, having it available as both argument and threadContext is a potential duplication for bugs.
Concretely, what I am suggesting is:
- Change the
AuditableRequest#authenticationSuccessmethod to take no argument - No change to
AuditTrail#authenticationSuccessmethods - Let
AuditableRestRequestandAuditableTransportRequestto extractauthenticationobject from the threadContext and pass it down to theAuditTrail. - Wrap
RestRequestasAuditableRestRequestearlier inSecurityRestFilterso that auditing can be consistently done withrequest.authenticationSuccess.
It is a bit larger changeset. But it helps maintain a more consistent audit logging contract.
|
@ywangd I think we need to sync on #94120 (comment) . I'll reach out on Slack. |
|
@ywangd Thank you for the thorough review and discussion! Can you please take another look and verify that my 2 new comments #94120 (comment) and #94120 (comment) make sense to you? |
ywangd
left a comment
There was a problem hiding this comment.
LGTM
Thanks for the discussion! There are tradeoffs in design decisions that we have to make due to various constraints. But importantly we have been explicit and agreed on the approach.
| @Override | ||
| void authenticationSuccess(Authentication authentication) { | ||
| auditTrail.authenticationSuccess(requestId, authentication, request); | ||
| // REST requests are audited in the {@code SecurityRestFilter} because they need access to the request body |
There was a problem hiding this comment.
@albertzaharovits Thanks for summarizing our discussion. I think it might be useful to somehow make the summary available in the code, e.g. maybe add a comment with link to your above comment.
| authenticate( | ||
| authListener -> authenticationService.authenticate(request, false, authListener), | ||
| authListener -> authenticationService.authenticate(request, false, authListener.delegateFailure((l, authentication) -> { | ||
| auditTrailService.get().authenticationSuccess(request); |
There was a problem hiding this comment.
It is possible to have AuditTrail.authenticationSuccess taking an extra argument which specifies what key the authentication object is stored against, e.g.
void authenticationSuccess(RestRequest request, String authcContextKey);
or have it take a Function that knows how to extract authentication, e.g.
void authenticationSuccess(RestRequest request, Function<ThreadContext, Authentication> authcReader);
This would allow us to always call authenticationSuccess in the filter. It is not critical. I'll leave it to you to make the call. Thanks!
| } | ||
| secondaryAuthenticator.authenticateAndAttachToContext(request, ActionListener.wrap(secondaryAuthentication -> { | ||
| auditTrailService.get().authenticationSuccess(wrappedRequest); | ||
| secondaryAuthenticator.authenticateAndAttachToContext(wrappedRequest, ActionListener.wrap(secondaryAuthentication -> { |
There was a problem hiding this comment.
👍 Using wrappedRequest is in fact a bug fix! Good catch!
Not for this PR, but this prompts me to think: Do we still need audit request body on secondary authc success? It's the current behaviour. I don't think it's necessory. That said, secondary authc probably is not widely used to be a concern.
There was a problem hiding this comment.
Do we still need audit request body on secondary authc success?
Since we couple request body auditing to authn success auditing, I can see how one might want to query for all the request bodies of requests issued by some principal, in which case I think the result should include the requests bodies where the queried principal was the secondary authentication. So, I think it makes sense to keep auditing the request body for secondary authn success, even though the same contents are audited twice. I think this is an argument in favor of having a standalone audit entry solely for the request body and linking it via the audit id to the other audit events.... But that's another discussion.
...security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java
Outdated
Show resolved
Hide resolved
...security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java
Outdated
Show resolved
Hide resolved
|
@elasticsearchmachine run elasticsearch-ci/part-1-fips |
1 similar comment
|
@elasticsearchmachine run elasticsearch-ci/part-1-fips |
Authentication for the HTTP channel is changing to not have access to the contents of the HTTP request body. But auditing of authentication success still requires access to the request body. Consequently, auditing of authentication success is now separated from the authentication logic: auditing is invoked in the SecurityRestFilter#handleRequest, after the AuthenticationService has done its job earlier in the handling flow for the HTTP request.
Authentication for the HTTP channel is changing to not have access to the contents of the HTTP request body.
But auditing of authentication success still requires access to the request body.
Consequently, auditing of authentication success is now separated from the authentication logic:
auditing is invoked in the
SecurityRestFilter#handleRequest, after theAuthenticationServicehas done its job earlier in the handling flow for the HTTP request.