Skip to content

Audit authn success in the SecurityRestFilter#94120

Merged
albertzaharovits merged 21 commits intoelastic:mainfrom
albertzaharovits:authn-success-for-rest-request
Apr 3, 2023
Merged

Audit authn success in the SecurityRestFilter#94120
albertzaharovits merged 21 commits intoelastic:mainfrom
albertzaharovits:authn-success-for-rest-request

Conversation

@albertzaharovits
Copy link
Copy Markdown
Contributor

@albertzaharovits albertzaharovits commented Feb 24, 2023

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.


@Override
public void authenticationSuccess(String requestId, Authentication authentication, RestRequest request) {
public void authenticationSuccess(RestRequest request) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This method must now be called in an authenticated context, when the HTTP request body is available.


authenticationService.authenticate(maybeWrapRestRequest(request), ActionListener.wrap(authentication -> {
final RestRequest wrappedRequest = maybeWrapRestRequest(request);
authenticationService.authenticate(wrappedRequest, ActionListener.wrap(authentication -> {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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).

@albertzaharovits albertzaharovits changed the title Authn success for rest request Audit authn success in the SecurityRestFilter Feb 27, 2023
@albertzaharovits albertzaharovits marked this pull request as ready for review February 27, 2023 15:50
@elasticsearchmachine elasticsearchmachine added the Team:Security Meta label for security team label Feb 27, 2023
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

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");
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.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");
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.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

}

/**
* This must be called after the HTTP request has been authenticated.
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.

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 ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
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 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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:

  1. Change the AuditableRequest#authenticationSuccess method to take no argument
  2. No change to AuditTrail#authenticationSuccess methods
  3. Let AuditableRestRequest and AuditableTransportRequest to extract authentication object from the threadContext and pass it down to the AuditTrail.
  4. Wrap RestRequest as AuditableRestRequest earlier in SecurityRestFilter so that auditing can be consistently done with request.authenticationSuccess.

It is a bit larger changeset. But it helps maintain a more consistent audit logging contract.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

@albertzaharovits albertzaharovits Mar 30, 2023

Choose a reason for hiding this comment

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

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#authenticationSuccess method around and keep invoking it in the same way, in the AuthenticationService just after the authN has been set in the thread context, but
  • make it a noop for REST requests, and instead
  • directly call the AuditTrail#authenticationSuccess in the SecurityRestFilter, where the request body is available and the authN just completed (and is set in the thread context)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pushed 8f5e1b3 .

@albertzaharovits
Copy link
Copy Markdown
Contributor Author

@ywangd @jakelandis ready for another round.

Copy link
Copy Markdown
Member

@ywangd ywangd left a comment

Choose a reason for hiding this comment

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

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!

authenticate(
authListener -> authenticationService.authenticate(request, false, authListener),
authListener -> authenticationService.authenticate(request, false, authListener.delegateFailure((l, authentication) -> {
auditTrailService.get().authenticationSuccess(request);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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:

  1. Change the AuditableRequest#authenticationSuccess method to take no argument
  2. No change to AuditTrail#authenticationSuccess methods
  3. Let AuditableRestRequest and AuditableTransportRequest to extract authentication object from the threadContext and pass it down to the AuditTrail.
  4. Wrap RestRequest as AuditableRestRequest earlier in SecurityRestFilter so that auditing can be consistently done with request.authenticationSuccess.

It is a bit larger changeset. But it helps maintain a more consistent audit logging contract.

@albertzaharovits
Copy link
Copy Markdown
Contributor Author

@ywangd I think we need to sync on #94120 (comment) . I'll reach out on Slack.

@albertzaharovits
Copy link
Copy Markdown
Contributor Author

@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?

@albertzaharovits albertzaharovits requested a review from ywangd March 30, 2023 07:09
Copy link
Copy Markdown
Member

@ywangd ywangd left a comment

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 -> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

@jakelandis jakelandis left a comment

Choose a reason for hiding this comment

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

LGTM

@albertzaharovits
Copy link
Copy Markdown
Contributor Author

@elasticsearchmachine run elasticsearch-ci/part-1-fips

1 similar comment
@albertzaharovits
Copy link
Copy Markdown
Contributor Author

@elasticsearchmachine run elasticsearch-ci/part-1-fips

@albertzaharovits albertzaharovits merged commit f353be2 into elastic:main Apr 3, 2023
@albertzaharovits albertzaharovits deleted the authn-success-for-rest-request branch April 3, 2023 11:30
albertzaharovits added a commit to albertzaharovits/elasticsearch that referenced this pull request Jun 8, 2023
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

>non-issue :Security/Audit X-Pack Audit logging Team:Security Meta label for security team v8.8.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants