Validate Certificate Identity provided in Cross Cluster API Key Certificate#136299
Validate Certificate Identity provided in Cross Cluster API Key Certificate#136299jfreden merged 28 commits intoelastic:mainfrom
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.
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.
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.
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.
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.
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.
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.
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.
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.
This was unused so I removed it.
gmjehovich
left a comment
There was a problem hiding this comment.
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] (
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.
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.
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.
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.
| @@ -192,9 +192,6 @@ public boolean verify(X509CertificateSignature signature, String... headers) thr | |||
| } | |||
| authTrustManager.checkClientTrusted(signature.certificates(), signature.certificates()[0].getPublicKey().getAlgorithm()); | |||
|
|
|||
There was a problem hiding this comment.
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.
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.
Added this to make sure we don't accept expired certs.
There was a problem hiding this comment.
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 adds documentation for the RCS Strong Verification feature added in elastic/elasticsearch#136299, elastic/elasticsearch#134137, elastic/elasticsearch#134893, elastic/elasticsearch#135674 and elastic/elasticsearch#134604. Related settings docs PR: elastic/elasticsearch#137822 --------- Co-authored-by: shainaraskas <58563081+shainaraskas@users.noreply.github.com> Co-authored-by: Edu González de la Herrán <25320357+eedugon@users.noreply.github.com>
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.