diff --git a/src/plugins/interactive_setup/server/elasticsearch_service.test.ts b/src/plugins/interactive_setup/server/elasticsearch_service.test.ts index ce4893112eba..f15e8e41956e 100644 --- a/src/plugins/interactive_setup/server/elasticsearch_service.test.ts +++ b/src/plugins/interactive_setup/server/elasticsearch_service.test.ts @@ -57,6 +57,13 @@ describe('ElasticsearchService', () => { return mockConnectionStatusClient; } }); + mockPingClient.asInternalUser.transport.request.mockResolvedValue( + interactiveSetupMock.createApiResponse({ + statusCode: 200, + body: {}, + headers: { 'x-elastic-product': 'Elasticsearch' }, + }) + ); setupContract = service.setup({ elasticsearch: mockElasticsearchPreboot, @@ -537,6 +544,19 @@ some weird+ca/with ); }); + it('fails if host is not Elasticsearch', async () => { + mockPingClient.asInternalUser.ping.mockResolvedValue( + interactiveSetupMock.createApiResponse({ statusCode: 200, body: true }) + ); + mockPingClient.asInternalUser.transport.request.mockResolvedValue( + interactiveSetupMock.createApiResponse({ statusCode: 200, body: {}, headers: {} }) + ); + + await expect(setupContract.ping('http://localhost:9200')).rejects.toMatchInlineSnapshot( + `[Error: Host did not respond with valid Elastic product header.]` + ); + }); + it('succeeds if host does not require authentication', async () => { mockPingClient.asInternalUser.ping.mockResolvedValue( interactiveSetupMock.createApiResponse({ statusCode: 200, body: true }) diff --git a/src/plugins/interactive_setup/server/elasticsearch_service.ts b/src/plugins/interactive_setup/server/elasticsearch_service.ts index edfe203df8e4..1b1b2781f1c7 100644 --- a/src/plugins/interactive_setup/server/elasticsearch_service.ts +++ b/src/plugins/interactive_setup/server/elasticsearch_service.ts @@ -290,6 +290,7 @@ export class ElasticsearchService { ssl: { verificationMode: 'none' }, }); + this.logger.debug(`Connecting to host "${host}"`); let authRequired = false; try { await client.asInternalUser.ping(); @@ -304,10 +305,9 @@ export class ElasticsearchService { } authRequired = getErrorStatusCode(error) === 401; - } finally { - await client.close(); } + this.logger.debug(`Fetching certificate chain from host "${host}"`); let certificateChain: Certificate[] | undefined; const { protocol, hostname, port } = new URL(host); if (protocol === 'https:') { @@ -320,10 +320,32 @@ export class ElasticsearchService { this.logger.error( `Failed to fetch peer certificate from host "${host}": ${getDetailedErrorMessage(error)}` ); + await client.close(); throw error; } } + // This check is a security requirement - Do not remove it! + this.logger.debug(`Verifying that host "${host}" responds with Elastic product header`); + + try { + const response = await client.asInternalUser.transport.request({ + method: 'OPTIONS', + path: '/', + }); + if (response.headers?.['x-elastic-product'] !== 'Elasticsearch') { + throw new Error('Host did not respond with valid Elastic product header.'); + } + } catch (error) { + this.logger.error( + `Host "${host}" is not a valid Elasticsearch cluster: ${getDetailedErrorMessage(error)}` + ); + await client.close(); + throw error; + } + + await client.close(); + return { authRequired, certificateChain,