Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve ApiKey credentials for async verification #51244

Merged
merged 8 commits into from
Jan 24, 2020

Conversation

tvernum
Copy link
Contributor

@tvernum tvernum commented Jan 21, 2020

The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.

The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.
@tvernum tvernum added >bug :Security/Authentication Logging in, Usernames/passwords, Realms (Native/LDAP/AD/SAML/PKI/etc) v8.0.0 v7.5.2 v6.8.7 v7.7.0 v7.6.1 labels Jan 21, 2020
@tvernum tvernum requested review from ywangd and jkakavas January 21, 2020 05:32
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-security (:Security/Authentication)

@tvernum tvernum added v7.5.3 and removed v7.5.2 labels Jan 21, 2020
// The second authentication should pass (but not immediately, but will not block)
PlainActionFuture<AuthenticationResult> future2 = new PlainActionFuture<>();

service.authenticateWithApiKeyIfPresent(threadPool.getThreadContext(), future2);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without the fix to not close the credentials, this line would fail with:

SecureString has already been closed

@ywangd
Copy link
Member

ywangd commented Jan 21, 2020

This is good catch for race conditions. The fix itself is easy to follow. The test code is more involved and took me sometime to understand it. But I don't have any suggestions on how it can be made simpler. Race condition testing is mostly tricky anyway. Other than the one comment about wrapping ActionListener earlier, everything else LGTM.

Copy link
Member

@jkakavas jkakavas left a comment

Choose a reason for hiding this comment

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

LGTM

final ApiKeyCredentials creds = new ApiKeyCredentials(randomAlphaOfLength(12), new SecureString(apiKey.toCharArray()));
final PlainActionFuture<AuthenticationResult> future1 = new PlainActionFuture<>();

// Call the top level method because it has been know to be buggy in async situations
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Call the top level method because it has been know to be buggy in async situations
// Call the top level method because it has been known to be buggy in async situations

@tvernum
Copy link
Contributor Author

tvernum commented Jan 24, 2020

Other than the one comment about wrapping ActionListener earlier, everything else LGTM.

I can't see any comment from you @ywangd, can you point me to it?

Copy link
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 overall LGTM.

validateApiKeyCredentials(docId, source, credentials, clock, listener);
}
final Map<String, Object> source = response.getSource();
validateApiKeyCredentials(docId, source, credentials, clock, ActionListener.wrap(
Copy link
Member

Choose a reason for hiding this comment

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

A minor comment: Is it worthwhile to wrap the ActionListeners earlier on so that we don't have to call credentials.close() in multiple places. That is once we know credentials is not null, we can have:

var credentialsClosingListener = ActionListener.wrap(...);

Then just use the new listener in subsequent code. At this point, it may be worthwhile to extract this part of code into a new method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, I'll see how it looks.

hashWait.notify();
}

assertThat(future1.actionGet(TimeValue.timeValueSeconds(2)).isAuthenticated(), is(true));
Copy link
Member

Choose a reason for hiding this comment

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

Mainly curious: it really takes 2 seconds for the notify to work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, but in these tests there's a delicate balance between not having the tests run too long and not getting noise.
This should be done in milliseconds. But if the VM that running CI get slowed down for some reason, or we get really long GC pause, and this takes 1.5 seconds, that shouldn't cause the test to fail.
We want a time that is long enough that the timeout means "this is never going to happen, the test failed" but not so long that you're sitting there wondering if the test is ever going to be done.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks that makes sense.

ApiKeyService realService = createApiKeyService(Settings.EMPTY);
ApiKeyService service = Mockito.spy(realService);

final Object hashWait = new Object();
Copy link
Member

Choose a reason for hiding this comment

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

Trivial: Could probably replace this with a CompletableFuture object? If so, could save a couple of lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used a semaphore instead, but thanks for the prompt. When I wrote the test I planned to come back and replace the wait/notify with something else after it was all working, but I forgot.

- Wrap the listener in 1 place
- Use a semaphore instead of Object.wait
@tvernum tvernum merged commit f68f513 into elastic:master Jan 24, 2020
tvernum added a commit to tvernum/elasticsearch that referenced this pull request Jan 24, 2020
The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.

Backport of: elastic#51244
tvernum added a commit that referenced this pull request Jan 24, 2020
The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.

Backport of: #51244
tvernum added a commit to tvernum/elasticsearch that referenced this pull request Jan 24, 2020
The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.

Backport of: elastic#51244
tvernum added a commit that referenced this pull request Jan 28, 2020
The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.

Backport of: #51244
tvernum added a commit to tvernum/elasticsearch that referenced this pull request Feb 3, 2020
The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.

Backport of: elastic#51244
tvernum added a commit that referenced this pull request Feb 4, 2020
The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.

Backport of: #51244
tvernum added a commit to tvernum/elasticsearch that referenced this pull request Feb 4, 2020
The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.

Backport of: elastic#51244
tvernum added a commit that referenced this pull request Feb 4, 2020
The ApiKeyService would aggressively "close" ApiKeyCredentials objects
during processing. However, under rare circumstances, the verfication
of the secret key would be performed asychronously and may need access
to the SecureString after it had been closed by the caller.

The trigger for this would be if the cache already held a Future for
that ApiKey, but the future was not yet complete. In this case the
verification of the secret key would take place asynchronously on the
generic thread pool.

This commit moves the "close" of the credentials to the body of the
listener so that it only occurs after key verification is complete.

Backport of: #51244
@polyfractal polyfractal added v7.6.0 and removed v7.6.1 labels Feb 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
>bug :Security/Authentication Logging in, Usernames/passwords, Realms (Native/LDAP/AD/SAML/PKI/etc) v6.8.7 v7.5.3 v7.6.0 v7.7.0 v8.0.0-alpha1
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants