interactive setup compatibility check (#119059)

* use root CA for interactive setup

* Use intermediate CA for end-to-end test

* Align setup CLI enrollment token param with ES

* Add CA private key to certificate

* Check cluster version during interactive setup

* Fix setup CLI

* add updated docs

* Added suggestions rom code review

* Update docs

* Fix paths

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Thom Heymann 2021-11-19 18:08:06 +00:00 committed by GitHub
parent 09a1a74069
commit fa5ccfedbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 306 additions and 16 deletions

View file

@ -128,6 +128,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. |
| [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. |
| [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. |
| [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) | |
| [PrebootPlugin](./kibana-plugin-core-server.prebootplugin.md) | The interface that should be returned by a <code>PluginInitializer</code> for a <code>preboot</code> plugin. |
| [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) | Kibana Preboot Service allows to control the boot flow of Kibana. Preboot plugins can use it to hold the boot until certain condition is met. |
| [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) | |
@ -236,6 +237,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| --- | --- |
| [APP\_WRAPPER\_CLASS](./kibana-plugin-core-server.app_wrapper_class.md) | The class name for top level \*and\* nested application wrappers to ensure proper layout |
| [kibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Set of helpers used to create <code>KibanaResponse</code> to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution. |
| [pollEsNodesVersion](./kibana-plugin-core-server.pollesnodesversion.md) | |
| [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) | The current "level" of availability of a service. |
| [validBodyOutput](./kibana-plugin-core-server.validbodyoutput.md) | The set of valid body.output |

View file

@ -0,0 +1,12 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [pollEsNodesVersion](./kibana-plugin-core-server.pollesnodesversion.md)
## pollEsNodesVersion variable
<b>Signature:</b>
```typescript
pollEsNodesVersion: ({ internalClient, log, kibanaVersion, ignoreVersionMismatch, esVersionCheckInterval: healthCheckInterval, }: PollEsNodesVersionOptions) => Observable<NodesVersionCompatibility>
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) &gt; [esVersionCheckInterval](./kibana-plugin-core-server.pollesnodesversionoptions.esversioncheckinterval.md)
## PollEsNodesVersionOptions.esVersionCheckInterval property
<b>Signature:</b>
```typescript
esVersionCheckInterval: number;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) &gt; [ignoreVersionMismatch](./kibana-plugin-core-server.pollesnodesversionoptions.ignoreversionmismatch.md)
## PollEsNodesVersionOptions.ignoreVersionMismatch property
<b>Signature:</b>
```typescript
ignoreVersionMismatch: boolean;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) &gt; [internalClient](./kibana-plugin-core-server.pollesnodesversionoptions.internalclient.md)
## PollEsNodesVersionOptions.internalClient property
<b>Signature:</b>
```typescript
internalClient: ElasticsearchClient;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) &gt; [kibanaVersion](./kibana-plugin-core-server.pollesnodesversionoptions.kibanaversion.md)
## PollEsNodesVersionOptions.kibanaVersion property
<b>Signature:</b>
```typescript
kibanaVersion: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) &gt; [log](./kibana-plugin-core-server.pollesnodesversionoptions.log.md)
## PollEsNodesVersionOptions.log property
<b>Signature:</b>
```typescript
log: Logger;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md)
## PollEsNodesVersionOptions interface
<b>Signature:</b>
```typescript
export interface PollEsNodesVersionOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [esVersionCheckInterval](./kibana-plugin-core-server.pollesnodesversionoptions.esversioncheckinterval.md) | number | |
| [ignoreVersionMismatch](./kibana-plugin-core-server.pollesnodesversionoptions.ignoreversionmismatch.md) | boolean | |
| [internalClient](./kibana-plugin-core-server.pollesnodesversionoptions.internalclient.md) | ElasticsearchClient | |
| [kibanaVersion](./kibana-plugin-core-server.pollesnodesversionoptions.kibanaversion.md) | string | |
| [log](./kibana-plugin-core-server.pollesnodesversionoptions.log.md) | Logger | |

View file

@ -10,6 +10,7 @@ import { getConfigPath, getDataPath } from '@kbn/utils';
import inquirer from 'inquirer';
import { duration } from 'moment';
import { merge } from 'lodash';
import { kibanaPackageJson } from '@kbn/utils';
import { Logger } from '../core/server';
import { ClusterClient } from '../core/server/elasticsearch/client';
@ -31,7 +32,7 @@ const logger: Logger = {
};
export const kibanaConfigWriter = new KibanaConfigWriter(getConfigPath(), getDataPath(), logger);
export const elasticsearch = new ElasticsearchService(logger).setup({
export const elasticsearch = new ElasticsearchService(logger, kibanaPackageJson.version).setup({
connectionCheckInterval: duration(Infinity),
elasticsearch: {
createClient: (type, config) => {

View file

@ -9,7 +9,10 @@
export { ElasticsearchService } from './elasticsearch_service';
export { config, configSchema } from './elasticsearch_config';
export { ElasticsearchConfig } from './elasticsearch_config';
export type { NodesVersionCompatibility } from './version_check/ensure_es_version';
export type {
NodesVersionCompatibility,
PollEsNodesVersionOptions,
} from './version_check/ensure_es_version';
export type {
ElasticsearchServicePreboot,
ElasticsearchServiceSetup,
@ -38,3 +41,4 @@ export type {
ElasticsearchErrorDetails,
} from './client';
export { getRequestDebugMeta, getErrorMessage } from './client';
export { pollEsNodesVersion } from './version_check/ensure_es_version';

View file

@ -20,6 +20,7 @@ import {
import { Logger } from '../../logging';
import type { ElasticsearchClient } from '../client';
/** @public */
export interface PollEsNodesVersionOptions {
internalClient: ElasticsearchClient;
log: Logger;
@ -139,6 +140,7 @@ function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompati
);
}
/** @public */
export const pollEsNodesVersion = ({
internalClient,
log,

View file

@ -115,7 +115,7 @@ export type { CoreId } from './core_context';
export { CspConfig } from './csp';
export type { ICspConfig } from './csp';
export { ElasticsearchConfig } from './elasticsearch';
export { ElasticsearchConfig, pollEsNodesVersion } from './elasticsearch';
export type {
ElasticsearchServicePreboot,
ElasticsearchServiceSetup,
@ -137,6 +137,7 @@ export type {
DeleteDocumentResponse,
ElasticsearchConfigPreboot,
ElasticsearchErrorDetails,
PollEsNodesVersionOptions,
} from './elasticsearch';
export type { IExternalUrlConfig, IExternalUrlPolicy } from './external_url';

View file

@ -1697,6 +1697,23 @@ export enum PluginType {
standard = "standard"
}
// @public (undocumented)
export const pollEsNodesVersion: ({ internalClient, log, kibanaVersion, ignoreVersionMismatch, esVersionCheckInterval: healthCheckInterval, }: PollEsNodesVersionOptions) => Observable<NodesVersionCompatibility>;
// @public (undocumented)
export interface PollEsNodesVersionOptions {
// (undocumented)
esVersionCheckInterval: number;
// (undocumented)
ignoreVersionMismatch: boolean;
// (undocumented)
internalClient: ElasticsearchClient;
// (undocumented)
kibanaVersion: string;
// (undocumented)
log: Logger;
}
// @public
export interface PrebootPlugin<TSetup = void, TPluginsSetup extends object = object> {
// (undocumented)

View file

@ -15,3 +15,4 @@ export const ERROR_KIBANA_CONFIG_FAILURE = 'kibana_config_failure';
export const ERROR_ENROLL_FAILURE = 'enroll_failure';
export const ERROR_CONFIGURE_FAILURE = 'configure_failure';
export const ERROR_PING_FAILURE = 'ping_failure';
export const ERROR_COMPATIBILITY_FAILURE = 'compatibility_failure';

View file

@ -16,5 +16,6 @@ export {
ERROR_KIBANA_CONFIG_NOT_WRITABLE,
ERROR_OUTSIDE_PREBOOT_STAGE,
ERROR_PING_FAILURE,
ERROR_COMPATIBILITY_FAILURE,
VERIFICATION_CODE_LENGTH,
} from './constants';

View file

@ -14,6 +14,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import type { IHttpFetchError, ResponseErrorBody } from 'kibana/public';
import {
ERROR_COMPATIBILITY_FAILURE,
ERROR_CONFIGURE_FAILURE,
ERROR_ELASTICSEARCH_CONNECTION_CONFIGURED,
ERROR_ENROLL_FAILURE,
@ -119,6 +120,15 @@ export const SubmitErrorCallout: FunctionComponent<SubmitErrorCalloutProps> = (p
id="interactiveSetup.submitErrorCallout.pingFailureErrorDescription"
defaultMessage="Check the address and retry."
/>
) : error.body?.attributes?.type === ERROR_COMPATIBILITY_FAILURE ? (
<FormattedMessage
id="interactiveSetup.submitErrorCallout.compatibilityFailureErrorDescription"
defaultMessage="The Elasticsearch cluster (v{elasticsearchVersion}) is incompatible with this version of Kibana (v{kibanaVersion})."
values={{
elasticsearchVersion: error.body?.attributes?.elasticsearchVersion as string,
kibanaVersion: error.body?.attributes?.kibanaVersion as string,
}}
/>
) : (
error.body?.message || error.message
)}

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { NodesVersionCompatibility } from 'src/core/server';
export class CompatibilityError extends Error {
constructor(private meta: NodesVersionCompatibility) {
super('Compatibility Error');
Error.captureStackTrace(this, CompatibilityError);
this.name = 'CompatibilityError';
this.message = meta.message!;
}
public get elasticsearchVersion() {
return this.meta.incompatibleNodes[0].version;
}
public get kibanaVersion() {
return this.meta.kibanaVersion;
}
}

View file

@ -7,11 +7,14 @@
*/
import { errors } from '@elastic/elasticsearch';
import { BehaviorSubject } from 'rxjs';
import tls from 'tls';
import { nextTick } from '@kbn/test/jest';
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
import { pollEsNodesVersion } from '../../../../src/core/server';
import type { NodesVersionCompatibility } from '../../../../src/core/server';
import { ElasticsearchConnectionStatus } from '../common';
import { ConfigSchema } from './config';
import type { ElasticsearchServiceSetup } from './elasticsearch_service';
@ -19,14 +22,24 @@ import { ElasticsearchService } from './elasticsearch_service';
import { interactiveSetupMock } from './mocks';
jest.mock('tls');
jest.mock('../../../../src/core/server', () => ({
pollEsNodesVersion: jest.fn(),
}));
const tlsConnectMock = tls.connect as jest.MockedFunction<typeof tls.connect>;
const mockPollEsNodesVersion = pollEsNodesVersion as jest.MockedFunction<typeof pollEsNodesVersion>;
function mockCompatibility(isCompatible: boolean, message?: string) {
mockPollEsNodesVersion.mockReturnValue(
new BehaviorSubject({ isCompatible, message } as NodesVersionCompatibility).asObservable()
);
}
describe('ElasticsearchService', () => {
let service: ElasticsearchService;
let mockElasticsearchPreboot: ReturnType<typeof elasticsearchServiceMock.createPreboot>;
beforeEach(() => {
service = new ElasticsearchService(loggingSystemMock.createLogger());
service = new ElasticsearchService(loggingSystemMock.createLogger(), '8.0.0');
mockElasticsearchPreboot = elasticsearchServiceMock.createPreboot();
});
@ -64,6 +77,7 @@ describe('ElasticsearchService', () => {
headers: { 'x-elastic-product': 'Elasticsearch' },
})
);
mockCompatibility(true);
setupContract = service.setup({
elasticsearch: mockElasticsearchPreboot,
@ -383,6 +397,38 @@ describe('ElasticsearchService', () => {
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
});
it('fails if version is incompatible', async () => {
const mockEnrollScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockEnrollScopedClusterClient.asCurrentUser.transport.request.mockResolvedValue(
interactiveSetupMock.createApiResponse({
statusCode: 200,
body: {
token: { name: 'some-name', value: 'some-value' },
http_ca: '\n\nsome weird-ca_with\n content\n\n',
},
})
);
mockEnrollClient.asScoped.mockReturnValue(mockEnrollScopedClusterClient);
mockAuthenticateClient.asInternalUser.security.authenticate.mockResolvedValue(
interactiveSetupMock.createApiResponse({ statusCode: 200, body: {} as any })
);
mockCompatibility(false, 'Oh no!');
await expect(
setupContract.enroll({
apiKey: 'apiKey',
hosts: ['host1', 'host2'],
caFingerprint: 'DE:AD:BE:EF',
})
).rejects.toMatchInlineSnapshot(`[CompatibilityError: Oh no!]`);
// Check that we properly closed all clients.
expect(mockEnrollClient.close).toHaveBeenCalledTimes(1);
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
});
it('iterates through all provided hosts until find an accessible one', async () => {
mockElasticsearchPreboot.createClient.mockClear();
@ -510,6 +556,20 @@ some weird+ca/with
await expect(
setupContract.authenticate({ host: 'http://localhost:9200' })
).rejects.toMatchInlineSnapshot(`[ConnectionError: some-message]`);
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
});
it('fails if version is incompatible', async () => {
mockAuthenticateClient.asInternalUser.ping.mockResolvedValue(
interactiveSetupMock.createApiResponse({ statusCode: 200, body: true })
);
mockCompatibility(false, 'Oh no!');
await expect(
setupContract.authenticate({ host: 'http://localhost:9200' })
).rejects.toMatchInlineSnapshot(`[CompatibilityError: Oh no!]`);
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
});
it('succeeds if ping call succeeds', async () => {
@ -520,6 +580,7 @@ some weird+ca/with
await expect(
setupContract.authenticate({ host: 'http://localhost:9200' })
).resolves.toEqual(undefined);
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
});
});
@ -535,6 +596,7 @@ some weird+ca/with
await expect(setupContract.ping('http://localhost:9200')).rejects.toMatchInlineSnapshot(
`[ConnectionError: some-message]`
);
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
});
it('fails if host is not supported', async () => {
@ -546,6 +608,7 @@ some weird+ca/with
await expect(setupContract.ping('http://localhost:9200')).rejects.toMatchInlineSnapshot(
`[ProductNotSupportedError: The client noticed that the server is not Elasticsearch and we do not support this unknown product.]`
);
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
});
it('fails if host is not Elasticsearch', async () => {
@ -559,6 +622,7 @@ some weird+ca/with
await expect(setupContract.ping('http://localhost:9200')).rejects.toMatchInlineSnapshot(
`[Error: Host did not respond with valid Elastic product header.]`
);
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
});
it('succeeds if host does not require authentication', async () => {
@ -570,6 +634,7 @@ some weird+ca/with
authRequired: false,
certificateChain: undefined,
});
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
});
it('succeeds if host requires authentication', async () => {
@ -583,6 +648,7 @@ some weird+ca/with
authRequired: true,
certificateChain: undefined,
});
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
});
it('succeeds if host requires SSL', async () => {
@ -616,6 +682,7 @@ some weird+ca/with
port: 9200,
rejectUnauthorized: false,
});
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
});
it('fails if peer certificate cannot be fetched', async () => {
@ -636,6 +703,7 @@ some weird+ca/with
await expect(setupContract.ping('https://localhost:9200')).rejects.toMatchInlineSnapshot(
`[Error: some-message]`
);
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -15,6 +15,7 @@ import {
catchError,
distinctUntilChanged,
exhaustMap,
first,
map,
shareReplay,
takeWhile,
@ -22,14 +23,17 @@ import {
import tls from 'tls';
import type {
ElasticsearchClient,
ElasticsearchServicePreboot,
ICustomClusterClient,
Logger,
ScopeableRequest,
} from 'src/core/server';
import { pollEsNodesVersion } from '../../../../src/core/server';
import { ElasticsearchConnectionStatus } from '../common';
import type { Certificate, PingResult } from '../common';
import { CompatibilityError } from './compatibility_error';
import { getDetailedErrorMessage, getErrorStatusCode } from './errors';
export interface EnrollParameters {
@ -101,6 +105,11 @@ export interface EnrollResult {
serviceAccountToken: { name: string; value: string };
}
export interface EnrollKibanaResponse {
token: { name: string; value: string };
http_ca: string;
}
export interface AuthenticateResult {
host: string;
username?: string;
@ -113,7 +122,7 @@ export class ElasticsearchService {
* Elasticsearch client used to check Elasticsearch connection status.
*/
private connectionStatusClient?: ICustomClusterClient;
constructor(private readonly logger: Logger) {}
constructor(private readonly logger: Logger, private kibanaVersion: string) {}
public setup({
elasticsearch,
@ -186,14 +195,14 @@ export class ElasticsearchService {
ssl: { verificationMode: 'none' },
});
let enrollmentResponse;
let enrollmentResponse: TransportResult<EnrollKibanaResponse>;
try {
enrollmentResponse = (await enrollClient
enrollmentResponse = await enrollClient
.asScoped(scopeableRequest)
.asCurrentUser.transport.request({
.asCurrentUser.transport.request<EnrollKibanaResponse>({
method: 'GET',
path: '/_security/enroll/kibana',
})) as TransportResult<{ token: { name: string; value: string }; http_ca: string }>;
});
} catch (err) {
// We expect that all hosts belong to exactly same node and any non-connection error for one host would mean
// that enrollment will fail for any other host and we should bail out.
@ -245,9 +254,17 @@ export class ElasticsearchService {
enrollmentResponse.body.token.name
}" token to host "${host}": ${getDetailedErrorMessage(err)}.`
);
throw err;
} finally {
await authenticateClient.close();
throw err;
}
const versionCompatibility = await this.checkCompatibility(authenticateClient.asInternalUser);
await authenticateClient.close();
if (!versionCompatibility.isCompatible) {
this.logger.error(
`Failed compatibility check of host "${host}": ${versionCompatibility.message}`
);
throw new CompatibilityError(versionCompatibility);
}
return enrollResult;
@ -275,9 +292,17 @@ export class ElasticsearchService {
this.logger.error(
`Failed to authenticate with host "${host}": ${getDetailedErrorMessage(error)}`
);
throw error;
} finally {
await client.close();
throw error;
}
const versionCompatibility = await this.checkCompatibility(client.asInternalUser);
await client.close();
if (!versionCompatibility.isCompatible) {
this.logger.error(
`Failed compatibility check of host "${host}": ${versionCompatibility.message}`
);
throw new CompatibilityError(versionCompatibility);
}
}
@ -303,6 +328,7 @@ export class ElasticsearchService {
error instanceof errors.ProductNotSupportedError
) {
this.logger.error(`Unable to connect to host "${host}": ${getDetailedErrorMessage(error)}`);
await client.close();
throw error;
}
@ -354,6 +380,18 @@ export class ElasticsearchService {
};
}
private async checkCompatibility(internalClient: ElasticsearchClient) {
return pollEsNodesVersion({
internalClient,
log: this.logger,
kibanaVersion: this.kibanaVersion,
ignoreVersionMismatch: false,
esVersionCheckInterval: -1, // Passing a negative number here will result in immediate completion after the first value is emitted
})
.pipe(first())
.toPromise();
}
private static fetchPeerCertificate(host: string, port: string | number) {
return new Promise<tls.DetailedPeerCertificate>((resolve, reject) => {
const socket = tls.connect({ host, port: Number(port), rejectUnauthorized: false });

View file

@ -46,7 +46,8 @@ export class InteractiveSetupPlugin implements PrebootPlugin {
constructor(private readonly initializerContext: PluginInitializerContext) {
this.#logger = this.initializerContext.logger.get();
this.#elasticsearch = new ElasticsearchService(
this.initializerContext.logger.get('elasticsearch')
this.initializerContext.logger.get('elasticsearch'),
initializerContext.env.packageInfo.version
);
this.#verification = new VerificationService(
this.initializerContext.logger.get('verification')

View file

@ -13,12 +13,14 @@ import { schema } from '@kbn/config-schema';
import type { RouteDefinitionParams } from '.';
import {
ElasticsearchConnectionStatus,
ERROR_COMPATIBILITY_FAILURE,
ERROR_CONFIGURE_FAILURE,
ERROR_ELASTICSEARCH_CONNECTION_CONFIGURED,
ERROR_KIBANA_CONFIG_FAILURE,
ERROR_KIBANA_CONFIG_NOT_WRITABLE,
ERROR_OUTSIDE_PREBOOT_STAGE,
} from '../../common';
import { CompatibilityError } from '../compatibility_error';
import type { AuthenticateParameters } from '../elasticsearch_service';
import { ElasticsearchService } from '../elasticsearch_service';
import type { WriteConfigParameters } from '../kibana_config_writer';
@ -121,7 +123,19 @@ export function defineConfigureRoute({
try {
await elasticsearch.authenticate(configToWrite);
} catch {
} catch (error) {
if (error instanceof CompatibilityError) {
return response.badRequest({
body: {
message: 'Failed to configure due to version incompatibility.',
attributes: {
type: ERROR_COMPATIBILITY_FAILURE,
elasticsearchVersion: error.elasticsearchVersion,
kibanaVersion: error.kibanaVersion,
},
},
});
}
// For security reasons, we shouldn't leak to the user whether Elasticsearch node couldn't process enrollment
// request or we just couldn't connect to any of the provided hosts.
return response.customError({

View file

@ -12,12 +12,14 @@ import { schema } from '@kbn/config-schema';
import {
ElasticsearchConnectionStatus,
ERROR_COMPATIBILITY_FAILURE,
ERROR_ELASTICSEARCH_CONNECTION_CONFIGURED,
ERROR_ENROLL_FAILURE,
ERROR_KIBANA_CONFIG_FAILURE,
ERROR_KIBANA_CONFIG_NOT_WRITABLE,
ERROR_OUTSIDE_PREBOOT_STAGE,
} from '../../common';
import { CompatibilityError } from '../compatibility_error';
import { ElasticsearchService } from '../elasticsearch_service';
import type { EnrollResult } from '../elasticsearch_service';
import type { WriteConfigParameters } from '../kibana_config_writer';
@ -100,7 +102,19 @@ export function defineEnrollRoutes({
hosts: request.body.hosts,
caFingerprint: ElasticsearchService.formatFingerprint(request.body.caFingerprint),
});
} catch {
} catch (error) {
if (error instanceof CompatibilityError) {
return response.badRequest({
body: {
message: 'Failed to enroll due to version incompatibility.',
attributes: {
type: ERROR_COMPATIBILITY_FAILURE,
elasticsearchVersion: error.elasticsearchVersion,
kibanaVersion: error.kibanaVersion,
},
},
});
}
// For security reasons, we shouldn't leak to the user whether Elasticsearch node couldn't process enrollment
// request or we just couldn't connect to any of the provided hosts.
return response.customError({