Conversation
lib/Connection.js
Outdated
| if (socket.authorized === false) { | ||
| onError(new Error(socket.authorizationError)) | ||
| request.once('error', () => {}) // we need to catch the request aborted error | ||
| return request.abort() | ||
| } |
There was a problem hiding this comment.
I’m not entirely sure this is needed, as this check would catch the self-signed certificate.
One of the test is failing because of this check, if we remove it, everything works as expected.
There was a problem hiding this comment.
I suspect it's not needed since we're going to validate a fingerprint of the top-most CA certificate, that will most-likely be a self-signed one (from the tech spec The fingerprint (SHA256) of the CA certificate that is used to sign the certificate that the Elasticsearch node presents for TLS. ). @jkakavas is my understanding correct?
There was a problem hiding this comment.
@jkakavas is my understanding correct?
Yes. We elected to use the CA certificate for the pinning as it originally felt like it provided for more options. It will be a self signed CA certificate that Elasticsearch generates on startup for security on by default but in general, it is common/fine/expected that CA certificates ( the root ones ) are self-signed
thomheymann
left a comment
There was a problem hiding this comment.
I haven't tested this locally yet but added a few comments based on the code.
One general question I have is how we can avoid the "self signed certificate in certificate chain" error nodejs will throw during interactive setup when connecting to the (at that point) untrusted cert. We can't ask users to set NODE_TLS_REJECT_UNAUTHORIZED=0 and setting ssl.verificationMode=none in the elasticsearch client doesn't seem to prevent it.
lib/Connection.js
Outdated
| this.headers = prepareHeaders(opts.headers, opts.auth) | ||
| this.deadCount = 0 | ||
| this.resurrectTimeout = 0 | ||
| this.certFingerprint = opts.certFingerprint |
There was a problem hiding this comment.
question: should we name it caCertFingerprint? I assume we're planning to validate fingerprint of the CA certificate, not the leaf one.
There was a problem hiding this comment.
Good call, I would say that caFingerprint is the best, so we avoid the double "certificate" repetition :)
There was a problem hiding this comment.
Technically we don't repeat (or rather repeat, but legitimately) since it's Certificate Authority certificate fingerprint, but either works for me 🙂
lib/Connection.js
Outdated
| if (socket.authorized === false) { | ||
| onError(new Error(socket.authorizationError)) | ||
| request.once('error', () => {}) // we need to catch the request aborted error | ||
| return request.abort() | ||
| } |
There was a problem hiding this comment.
I suspect it's not needed since we're going to validate a fingerprint of the top-most CA certificate, that will most-likely be a self-signed one (from the tech spec The fingerprint (SHA256) of the CA certificate that is used to sign the certificate that the Elasticsearch node presents for TLS. ). @jkakavas is my understanding correct?
thomheymann
left a comment
There was a problem hiding this comment.
I've tested this locally and the fingerprint is correctly verified. 🥳
However, this only works when running node with NODE_TLS_REJECT_UNAUTHORIZED=0 flag. Otherwise we don't get to the verification step since node throws an error for self signed certificates.
Asking users to run Kibana with that flag is not a viable option. Appreciate that this is node throwing an error but could we set rejectUnauthorized to false when caFingerprint option is set on the client to avoid this? Is there another option you can think of?
$ node test.js
ERROR ConnectionError: self signed certificate in certificate chain
at ClientRequest.onError (/Users/thom/Repositories/elasticsearch-js/lib/Connection.js:116:16)
at ClientRequest.emit (events.js:375:28)
at TLSSocket.socketErrorListener (_http_client.js:475:9)
at TLSSocket.emit (events.js:375:28)
at emitErrorNT (internal/streams/destroy.js:106:8)
at emitErrorCloseNT (internal/streams/destroy.js:74:3)
at processTicksAndRejections (internal/process/task_queues.js:82:21)
I'm fine to setting |
thomheymann
left a comment
There was a problem hiding this comment.
As discussed offline, let's leave this as opt in and let the consumer decide whether to disable rejectUnauthorized or not for now.
Otherwise LGTM! Thanks for implementing this so swiftly. 🙏
azasypkin
left a comment
There was a problem hiding this comment.
LGTM from Kibana standpoint, thanks! Just one issue we discussed over Slack: handle/test the case when caFingerprint is provided, but ES client tries to connect to HTTP address.
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
Just validating my understanding that Kibana will set this so that the users won't need to configure something extra. Can this be set per connection and only set it for the initial call to the enroll API ? We wouldn't want to set this globally off for Kibana |
jkakavas
left a comment
There was a problem hiding this comment.
Added just a few comments, thanks for tackling this so quickly!
| // the fingerprint (SHA256) of the CA certificate that is used to sign the certificate that the Elasticsearch node presents for TLS. | ||
| caFingerprint: '20:0D:CA:FA:76:...', | ||
| ssl: { | ||
| // might be required if it's a self-signed certificate |
There was a problem hiding this comment.
Most CA certificates are self signed. Well root CAs are, intermediate aren't ( are signed by the root ) but this is not very uncommon
| /* istanbul ignore next */ | ||
| certificate = certificate.issuerCertificate | ||
| } else { | ||
| break |
There was a problem hiding this comment.
when issuerCertificate is undefined, the certificate is invalid or malformed, let's not try to use it. Even if we don't care about the standards, then this here is the peer certificate, and not it's issuer's.
There was a problem hiding this comment.
when issuerCertificate is undefined, the certificate is invalid or malformed, let's not try to use it.
Hmm, it seems that missing issuerCertificate in certificate is one thing and missing issuerCertificate property in a certificate parsed by NodeJS is a different thing. If host just doesn't return a full chain, I guess, we'll have an empty issuerCertificate property (or if we pass false to socket.getPeerCertificate).
If ES is configured to not return full chain, do we treat it as a misconfiguration @jkakavas or you meant something else?
There was a problem hiding this comment.
missing issuerCertificate in certificate is one thing and missing issuerCertificate property in a certificate parsed by NodeJS is a different thing
Correct. I focused on the former I guess :/ Still though, we're in a method called getIssuerCertificate and we shouldn't return a certificate just because we couldn't get it's issuer. If ES doesn't return the full chain, then caFingerprint does not make much sense to be set.
There was a problem hiding this comment.
Still though, we're in a method called getIssuerCertificate and we shouldn't return a certificate just because we couldn't get it's issuer.
Yep
If ES doesn't return the full chain, then
caFingerprintdoes not make much sense to be set.
Would it make sense to rename it to rootCAFingerprint then? Because we may still have a number intermediate CA certificates in the chain even though root CA certificate isn't there.
And just for my own education, if I issue a Let's Encrypt certificate to use with my ES instance, I still have to configure ES with the full chain, even though root CA certificate is issued by the well-known public CA, right?
There was a problem hiding this comment.
Would it make sense to rename it to rootCAFingerprint then? Because we may still have a number intermediate CA certificates in the chain even though root CA certificate isn't there.
Possibly, it makes it clearer I think but we can handle this with documentation also.
And just for my own education, if I issue a Let's Encrypt certificate to use with my ES instance, I still have to configure ES with the full chain, even though root CA certificate is issued by the well-known public CA, right?
Well, we don't support the enrollment process for arbitrary configurations but if you'd want to use let's encrypt and use caFingerprint in a way in your es client, then yes ,you'd need to do that.
Generically, it would make more sense for a client to support leaf certificate pinning ( as in I want to trust this server certificate only ). The idea behind us using CA certificate pinning for the enrollment process is that it allows you to get a single enrollment token that might contain many addresses for all ES nodes in a cluster and you can try/selct any of them and validate the connection with a single fingerprint ( as opposed to one per node )
There was a problem hiding this comment.
Yeah, but I had an impression that caFingerprint we've introduced here is not specific to enrollment process otherwise we should have named it differently I guess.
Generically, it would make more sense for a client to support leaf certificate pinning ( as in I want to trust this server certificate only ).
That's actually what I was alluding to initially, pinning to the leaf (this specific server) or intermediate CA (this specific group of servers) seems to be a perfectly valid use case. My concern was that ES seems to not explicitly require full chains and moreover works fine with non-complete chains. That'd be enough for the aforementioned use case, but it won't work if we treat certificate with null/undefined issuerCertificate as invalid (the one that NodeJS parses, aka if (certificate.issuerCertificate == null) return null).
Anyway, PR is merged, so let's if it ever becomes a problem 🤷♂️
There was a problem hiding this comment.
That's actually what I was alluding to initially, pinning to the leaf (this specific server) or intermediate CA (this specific group of servers) seems to be a perfectly valid use case.
Good point, I'd be 👍 This would cover security on by default enrollment process, and more generic cases at the same time. Maybe we can do in a followup ?
There was a problem hiding this comment.
Maybe we can do in a followup ?
Yeah, I'd 👍 too.
There was a problem hiding this comment.
I'm exactly having this issue. Returned certificate only has issuer not issuerCertificate. It results in error: Unhandled Rejection at: Promise [object Promise] reason ConnectionError: Invalid or malformed certificate. This is in contrast with Python client where ssl_assert_fingerprint parameter works as expected with self-signed certificate that elastic generates by default.
Co-authored-by: Ioannis Kakavas <ioannis@elastic.co>
Co-authored-by: Ioannis Kakavas <ioannis@elastic.co>
Co-authored-by: Ioannis Kakavas <ioannis@elastic.co>
Correct, Kibana will disable All other requests to Elasticsearch will keep that security check enabled. |
jkakavas
left a comment
There was a problem hiding this comment.
LGTM with my latest comments addressed, I don't think this needs another round. Thanks Tomas!
Co-authored-by: Ioannis Kakavas <ioannis@elastic.co>
Co-authored-by: Ioannis Kakavas <ioannis@elastic.co>
|
The backport to To backport manually, run these commands in your terminal: # Fetch latest updates from GitHub
git fetch
# Create a new working tree
git worktree add .worktrees/backport-7.x 7.x
# Navigate to the new working tree
cd .worktrees/backport-7.x
# Create a new branch
git switch --create backport-1499-to-7.x
# Cherry-pick the merged commit of this pull request and resolve the conflicts
git cherry-pick --mainline 1 2d1505eb2b7ad05387ca8ba8a6874f580a126ecd
# Push it to GitHub
git push --set-upstream origin backport-1499-to-7.x
# Go back to the original working tree
cd ../..
# Delete the working tree
git worktree remove .worktrees/backport-7.xThen, create a pull request where the |
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Ioannis Kakavas <ioannis@elastic.co>
As titled.
Usage:
TODO:
Integration test with Elasticsearch