-
Notifications
You must be signed in to change notification settings - Fork 719
Support CA fingerprint validation #1499
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
Changes from 13 commits
793018d
8f60fa0
407a140
ad3b90b
d4844f6
b0f786f
ebbbee8
b7b0545
dd08d7e
d0c1040
a5c59db
ff279cc
9e1babd
fcf947d
1cc3281
33f90b0
1b69efe
5f85ded
a6a6dee
d9192e9
208d316
fecd15b
5886718
b4ea6aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -176,6 +176,28 @@ const client = new Client({ | |
| }) | ||
| ---- | ||
|
|
||
| [discrete] | ||
| [[auth-ca-fingerprint]] | ||
| ==== CA fingerprint | ||
|
|
||
| To improve the security of your connection to Elasticsearch, you can provide | ||
|
delvedor marked this conversation as resolved.
Outdated
|
||
| a `caFingerprint` option, which will verify the supplied certificate authority fingerprint. | ||
|
|
||
| [source,js] | ||
| ---- | ||
| const { Client } = require('@elastic/elasticsearch') | ||
| const client = new Client({ | ||
| node: 'https://example.com' | ||
| auth: { ... }, | ||
| // 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most CA certificates are self signed. Well root CAs are, intermediate aren't ( are signed by the root ) but this is not very uncommon |
||
| rejectUnauthorized: false | ||
| } | ||
| }) | ||
| ---- | ||
|
|
||
|
|
||
| [discrete] | ||
| [[client-usage]] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,7 @@ class Connection { | |
| this.headers = prepareHeaders(opts.headers, opts.auth) | ||
| this.deadCount = 0 | ||
| this.resurrectTimeout = 0 | ||
| this.caFingerprint = opts.caFingerprint | ||
|
|
||
| this._openRequests = 0 | ||
| this._status = opts.status || Connection.statuses.ALIVE | ||
|
|
@@ -123,10 +124,28 @@ class Connection { | |
| callback(new RequestAbortedError(), null) | ||
| } | ||
|
|
||
| const onSocket = socket => { | ||
| /* istanbul ignore else */ | ||
| if (!socket.isSessionReused()) { | ||
|
delvedor marked this conversation as resolved.
|
||
| socket.once('secureConnect', () => { | ||
| // Check if fingerprint matches | ||
| /* istanbul ignore else */ | ||
| if (this.caFingerprint !== getIssuerCertificate(socket).fingerprint256) { | ||
| onError(new Error('Fingerprint does not match')) | ||
|
delvedor marked this conversation as resolved.
Outdated
|
||
| request.once('error', () => {}) // we need to catch the request aborted error | ||
| return request.abort() | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| request.on('response', onResponse) | ||
| request.on('timeout', onTimeout) | ||
| request.on('error', onError) | ||
| request.on('abort', onAbort) | ||
| if (this.caFingerprint != null) { | ||
| request.on('socket', onSocket) | ||
| } | ||
|
|
||
| // Disables the Nagle algorithm | ||
| request.setNoDelay(true) | ||
|
|
@@ -152,6 +171,7 @@ class Connection { | |
| request.removeListener('timeout', onTimeout) | ||
| request.removeListener('error', onError) | ||
| request.removeListener('abort', onAbort) | ||
| request.removeListener('socket', onSocket) | ||
| cleanedListeners = true | ||
| } | ||
| } | ||
|
|
@@ -340,5 +360,24 @@ function prepareHeaders (headers = {}, auth) { | |
| return headers | ||
| } | ||
|
|
||
| function getIssuerCertificate (socket) { | ||
| let certificate = socket.getPeerCertificate(true) | ||
| while (certificate && Object.keys(certificate).length > 0) { | ||
| /* istanbul ignore else */ | ||
| if (certificate.issuerCertificate !== undefined) { | ||
| // For self-signed certificates, `issuerCertificate` may be a circular reference. | ||
| /* istanbul ignore else */ | ||
| if (certificate.fingerprint256 === certificate.issuerCertificate.fingerprint256) { | ||
| break | ||
| } | ||
| /* istanbul ignore next */ | ||
| certificate = certificate.issuerCertificate | ||
| } else { | ||
| break | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm, it seems that missing 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Correct. I focused on the former I guess :/ Still though, we're in a method called
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep
Would it make sense to rename it to 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Possibly, it makes it clearer I think but we can handle this with documentation also.
Well, we don't support the enrollment process for arbitrary configurations but if you'd want to use let's encrypt and use 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 )
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, but I had an impression that
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 Anyway, PR is merged, so let's if it ever becomes a problem 🤷♂️ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 ?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, I'd 👍 too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm exactly having this issue. Returned certificate only has |
||
| } | ||
| } | ||
| return certificate | ||
| } | ||
|
|
||
| module.exports = Connection | ||
| module.exports.internals = { prepareHeaders } | ||
Uh oh!
There was an error while loading. Please reload this page.