- Notifications
You must be signed in to change notification settings - Fork 25.6k
Validate Certificate Identity provided in Cross Cluster API Key Certificate #136299
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
Conversation
| assertCrossClusterSearchSuccessfulWithResult(); | ||
| | ||
| // Change the CA to the default trust store to make sure untrusted signature fails auth even if it's not required | ||
| updateClusterSettingsFulfillingCluster(Settings.builder().putNull("cluster.remote.signing.certificate_authorities").build()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am curious, why fail if a certificate is invalid but not required? Is it to protect against misconfiguration?
| // Always validate a signature if provided | ||
| if (signature != null && verifySignature(signature, crossClusterAccessHeaders, listener) == false) { | ||
| // TODO audit logging | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With signature verification happening before API key validation, if the certificate is invalid but the API key is also expired, the user sees 'invalid certificate' and never learns about the expired key. Is this the intended priority?
I do think this way makes sense; if trust can't be established, then there isn't much point in validating the API key itself
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order right now is:
- Validate that the API key is valid, this happens first because of authenticateHeaders
- Validate that the provided certificate identity is ok also happens in authenticateHeaders
- Validate signature
With the current implementation (1) and (2) would actually not generate an audit log from what I can see. Since (1) is the current implementation I think it's a bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the intended priority?
My thinking is that validating the signature on every request, even failing ones is potentially expensive, so doing it in auth headers is probably not right. The point of auth headers is to short circuit the auth flow.
Doing the signature verification first in the auth flow decouples it from the rest of the api key service, if we don't do it there we require the api key credentials to contain all the signable payload + signature info and to pass it along to the api key service where we do validation. Not sure if that's a good enough reason, but I don't think it matters if we do it first or last, so keeping decoupling and doing it early is probably acceptable.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Realizing now I responded to you on Slack but not here. This argument makes sense to me, I don't ultimately see a huge difference between checking it first or last, and architecturally it's easier to do here than trying to couple it into the rest of the auth flow.
| assertCrossClusterSearchSuccessfulWithResult(); | ||
| | ||
| // Change the CA to the default trust store to make sure untrusted signature fails auth even if it's not required | ||
| updateClusterSettingsFulfillingCluster(Settings.builder().putNull("cluster.remote.signing.certificate_authorities").build()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am curious, why fail if a certificate is invalid but not required? Is it to catch misconfigurations early?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, if a signature is provided I think we should try to validate it and fail if it's not correct since it's unexpected. Better to be less lenient in this case I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also gives customers a way to progressively switch over to signing. If they configure signing on the origin cluster, and it doesn't break, then they can rely on the fact that it's probably working correctly. Then they can turn on enforcement on the linked cluster.
And if that first step does break, then they can just turn off signing on the origin, without needing to reconfigure both sides.
| Thanks for working on this! The approach looks good. Moving cert validation into the authentication flow should fix the double audit logging I was seeing. I just left some questions to help with my understanding |
79d328b to d1293d5 Compare 93eef5b to cc2e580 Compare | } | ||
| } | ||
| | ||
| private ApiKeyService createApiKeyService(Settings baseSettings, FeatureService customFeatureService) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was unused so I removed it.
gmjehovich left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked some into the test failures from the CI. The serverless ones seem unrelated, but the failing test on the ci test is interesting.
It looks like that case in testInvalidHeaders never triggers the callback passed to the call to authenticationService.authenticate as [this line] (
Line 127 in 87edbc9
| final CrossClusterAccessSubjectInfo subjectInfo = crossClusterAccessHeaders.getCleanAndValidatedSubjectInfo(); |
crossClusterAccessHeaders.getCleanAndValidatedSubjectInfo() should be triggering that exception I think x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java Outdated Show resolved Hide resolved
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java Outdated Show resolved Hide resolved
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java Outdated Show resolved Hide resolved
.../org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java Show resolved Hide resolved
| // Always validate a signature if provided | ||
| if (signature != null && verifySignature(signature, crossClusterAccessHeaders, listener) == false) { | ||
| // TODO audit logging | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Realizing now I responded to you on Slack but not here. This argument makes sense to me, I don't ultimately see a huge difference between checking it first or last, and architecturally it's easier to do here than trying to couple it into the rest of the auth flow.
87edbc9 to 2618ac9 Compare 8cce705 to 99fac10 Compare x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java Outdated Show resolved Hide resolved
| Hi @jfreden, I've created a changelog YAML for you. |
| Pinging @elastic/es-security (Team:Security) |
| Ended up adding a pattern cache. Still need to add some testing for it but the basics are there. Will continue tomorrow. |
f39c655 to ed4093a Compare | private final boolean enabled; | ||
| private final Settings settings; | ||
| private final InactiveApiKeysRemover inactiveApiKeysRemover; | ||
| private final Cache<String, Pattern> certificateIdentityPatternCache; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What was the motivation for certificateIdentityPatternCache? Is regex compilation measurably slow relative to the surrounding changes (e.g. signature verification)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was on the fence about this initially and didn't add a cache.
We accept arbitrary patterns, so measuring if it's slow relative the surrounding changes is difficult. Another consideration is that the Pattern object can be large in memory, so a cache could make things worse. My guess is that most of the time the patterns won't be that expensive and like you point out, the pattern compilation is probably not that slow.
What convinced me to eventually add it was that we check the pattern twice per request, so if it's a complex pattern it could be expensive. This is also something that's very easy to cache and if we add a signature cache in the future it could be the next bottleneck.
| ); | ||
| } | ||
| authTrustManager.checkClientTrusted(signature.certificates(), signature.certificates()[0].getPublicKey().getAlgorithm()); | ||
| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small suggestion: consider using leaf rather than signature.certificates()[0] since the intermediate var is defined above in method scope. Relatedly, it might be worthwhile to introduce a leafCerificate method (or similar name) on X509CertificateSignature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great idea! I've added a leafCerificate() method and updated to use the leaf variable instead.
| ); | ||
| } | ||
| authTrustManager.checkClientTrusted(signature.certificates(), signature.certificates()[0].getPublicKey().getAlgorithm()); | ||
| for (var certificate : signature.certificates()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this to make sure we don't accept expired certs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my testing it looks like if I add an expired cert to the trust store, we trust it even if it's expired unless we do this explicit check.
f39ddba to 566770e Compare | CI failing is unrelated, see: https://github.com/elastic/elasticsearch-serverless/issues/4737 |
…ficate (elastic#136299) * Validate certificate identity from cross cluster creds Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co> Co-authored-by: Tim Vernum <tim@adjective.org>
This is a follow up to #134137, #134893, #135674 and #134604.
This PR pulls the DN out of the provided signature leaf certificate and uses it to match against the configured
certificate_identitypattern on the cross cluster api key.This PR does not deal with audit logging, that will be done in a followup PR.