Skip to content
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

Update Monitoring plugin's Elasticsearch configuration #55119

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions x-pack/legacy/plugins/monitoring/__tests__/deprecations.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,54 @@ describe('monitoring plugin deprecations', function() {
expect(log.called).to.be(true);
});
});

describe('elasticsearch.username', function() {
it('logs a warning if elasticsearch.username is set to "elastic"', () => {
const settings = { elasticsearch: { username: 'elastic' } };

const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(true);
});

it('does not log a warning if elasticsearch.username is set to something besides "elastic"', () => {
const settings = { elasticsearch: { username: 'otheruser' } };

const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(false);
});

it('does not log a warning if elasticsearch.username is unset', () => {
const settings = { elasticsearch: { username: undefined } };

const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(false);
});

it('logs a warning if ssl.key is set and ssl.certificate is not', () => {
const settings = { elasticsearch: { ssl: { key: '' } } };

const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(true);
});

it('logs a warning if ssl.certificate is set and ssl.key is not', () => {
const settings = { elasticsearch: { ssl: { certificate: '' } } };

const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(true);
});

it('does not log a warning if both ssl.key and ssl.certificate are set', () => {
const settings = { elasticsearch: { ssl: { key: '', certificate: '' } } };

const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(false);
});
});
});
8 changes: 8 additions & 0 deletions x-pack/legacy/plugins/monitoring/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ export const config = Joi => {
certificate: Joi.string(),
key: Joi.string(),
keyPassphrase: Joi.string(),
keystore: Joi.object({
path: Joi.string(),
password: Joi.string(),
}).default(),
truststore: Joi.object({
path: Joi.string(),
password: Joi.string(),
}).default(),
alwaysPresentCertificate: Joi.boolean().default(false),
}).default(),
apiVersion: Joi.string().default('master'),
Expand Down
26 changes: 26 additions & 0 deletions x-pack/legacy/plugins/monitoring/deprecations.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,31 @@ export const deprecations = () => {
);
}
},
(settings, log) => {
const fromPath = 'xpack.monitoring.elasticsearch';
const es = get(settings, 'elasticsearch');
if (es) {
if (es.username === 'elastic') {
log(
`Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana" user instead.`
);
}
}
},
(settings, log) => {
const fromPath = 'xpack.monitoring.elasticsearch.ssl';
const ssl = get(settings, 'elasticsearch.ssl');
if (ssl) {
if (ssl.key !== undefined && ssl.certificate === undefined) {
log(
`Setting [${fromPath}.key] without [${fromPath}.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`
);
} else if (ssl.certificate !== undefined && ssl.key === undefined) {
log(
`Setting [${fromPath}.certificate] without [${fromPath}.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`
);
}
}
},
];
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import expect from '@kbn/expect';
import sinon from 'sinon';
import { get, noop } from 'lodash';
import { noop } from 'lodash';
import { exposeClient, hasMonitoringCluster } from '../instantiate_client';

function getMockServerFromConnectionUrl(monitoringClusterUrl) {
Expand All @@ -26,15 +26,8 @@ function getMockServerFromConnectionUrl(monitoringClusterUrl) {
},
};

const config = {
get: path => {
return get(server, path);
},
set: noop,
};

return {
config,
elasticsearchConfig: server.xpack.monitoring.elasticsearch,
elasticsearchPlugin: {
getCluster: sinon
.stub()
Expand Down Expand Up @@ -141,12 +134,12 @@ describe('Instantiate Client', () => {
describe('hasMonitoringCluster', () => {
it('returns true if monitoring is configured', () => {
const server = getMockServerFromConnectionUrl('http://monitoring-cluster.test:9200'); // pass null for URL to create the client using prod config
expect(hasMonitoringCluster(server.config)).to.be(true);
expect(hasMonitoringCluster(server.elasticsearchConfig)).to.be(true);
});

it('returns false if monitoring is not configured', () => {
const server = getMockServerFromConnectionUrl(null);
expect(hasMonitoringCluster(server.config)).to.be(false);
expect(hasMonitoringCluster(server.elasticsearchConfig)).to.be(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,21 @@ import { LOGGING_TAG } from '../../common/constants';
* Kibana itself is connected to a production cluster.
*/

export function exposeClient({ config, events, log, elasticsearchPlugin }) {
const elasticsearchConfig = hasMonitoringCluster(config)
? config.get('xpack.monitoring.elasticsearch')
: {};
export function exposeClient({ elasticsearchConfig, events, log, elasticsearchPlugin }) {
const isMonitoringCluster = hasMonitoringCluster(elasticsearchConfig);
const cluster = elasticsearchPlugin.createCluster('monitoring', {
...elasticsearchConfig,
...(isMonitoringCluster ? elasticsearchConfig : {}),
plugins: [monitoringBulk],
logQueries: Boolean(elasticsearchConfig.logQueries),
});

events.on('stop', bindKey(cluster, 'close'));
const configSource = hasMonitoringCluster(config) ? 'monitoring' : 'production';
const configSource = isMonitoringCluster ? 'monitoring' : 'production';
log([LOGGING_TAG, 'es-client'], `config sourced from: ${configSource} cluster`);
}

export function hasMonitoringCluster(config) {
const hosts = config.get('xpack.monitoring.elasticsearch.hosts');
return Boolean(hosts && hosts.length);
return Boolean(config.hosts && config.hosts.length);
}

export const instantiateClient = once(exposeClient);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export const mockReadFileSync = jest.fn();
jest.mock('fs', () => ({ readFileSync: mockReadFileSync }));

export const mockReadPkcs12Keystore = jest.fn();
export const mockReadPkcs12Truststore = jest.fn();
jest.mock('../../../../../../src/core/utils', () => ({
readPkcs12Keystore: mockReadPkcs12Keystore,
readPkcs12Truststore: mockReadPkcs12Truststore,
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import {
mockReadFileSync,
mockReadPkcs12Keystore,
mockReadPkcs12Truststore,
} from './parse_elasticsearch_config.test.mocks';

import { parseElasticsearchConfig } from './parse_elasticsearch_config';

const parse = (config: any) => {
return parseElasticsearchConfig({
get: () => config,
});
};

describe('reads files', () => {
beforeEach(() => {
mockReadFileSync.mockReset();
mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
mockReadPkcs12Keystore.mockReset();
mockReadPkcs12Keystore.mockImplementation((path: string) => ({
key: `content-of-${path}.key`,
cert: `content-of-${path}.cert`,
ca: [`content-of-${path}.ca`],
}));
mockReadPkcs12Truststore.mockReset();
mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]);
});

it('reads certificate authorities when ssl.keystore.path is specified', () => {
const configValue = parse({ ssl: { keystore: { path: 'some-path' } } });
expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path.ca']);
});

it('reads certificate authorities when ssl.truststore.path is specified', () => {
const configValue = parse({ ssl: { truststore: { path: 'some-path' } } });
expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
});

it('reads certificate authorities when ssl.certificateAuthorities is specified', () => {
let configValue = parse({ ssl: { certificateAuthorities: 'some-path' } });
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);

mockReadFileSync.mockClear();
configValue = parse({ ssl: { certificateAuthorities: ['some-path'] } });
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);

mockReadFileSync.mockClear();
configValue = parse({ ssl: { certificateAuthorities: ['some-path', 'another-path'] } });
expect(mockReadFileSync).toHaveBeenCalledTimes(2);
expect(configValue.ssl.certificateAuthorities).toEqual([
'content-of-some-path',
'content-of-another-path',
]);
});

it('reads certificate authorities when ssl.keystore.path, ssl.truststore.path, and ssl.certificateAuthorities are specified', () => {
const configValue = parse({
ssl: {
keystore: { path: 'some-path' },
truststore: { path: 'another-path' },
certificateAuthorities: 'yet-another-path',
},
});
expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual([
'content-of-some-path.ca',
'content-of-another-path',
'content-of-yet-another-path',
]);
});

it('reads a private key and certificate when ssl.keystore.path is specified', () => {
const configValue = parse({ ssl: { keystore: { path: 'some-path' } } });
expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
expect(configValue.ssl.key).toEqual('content-of-some-path.key');
expect(configValue.ssl.certificate).toEqual('content-of-some-path.cert');
});

it('reads a private key when ssl.key is specified', () => {
const configValue = parse({ ssl: { key: 'some-path' } });
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.key).toEqual('content-of-some-path');
});

it('reads a certificate when ssl.certificate is specified', () => {
const configValue = parse({ ssl: { certificate: 'some-path' } });
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificate).toEqual('content-of-some-path');
});
});

describe('throws when config is invalid', () => {
beforeAll(() => {
const realFs = jest.requireActual('fs');
mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path));
const utils = jest.requireActual('../../../../../../src/core/utils');
mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) =>
utils.readPkcs12Keystore(path, password)
);
mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) =>
utils.readPkcs12Truststore(path, password)
);
});

it('throws if key is invalid', () => {
const value = { ssl: { key: '/invalid/key' } };
expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/key'"`
);
});

it('throws if certificate is invalid', () => {
const value = { ssl: { certificate: '/invalid/cert' } };
expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/cert'"`
);
});

it('throws if certificateAuthorities is invalid', () => {
const value = { ssl: { certificateAuthorities: '/invalid/ca' } };
expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/ca'"`
);
});

it('throws if keystore path is invalid', () => {
const value = { ssl: { keystore: { path: '/invalid/keystore' } } };
expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/keystore'"`
);
});

it('throws if keystore does not contain a key', () => {
mockReadPkcs12Keystore.mockReturnValueOnce({});
const value = { ssl: { keystore: { path: 'some-path' } } };
expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot(
`"Did not find key in Elasticsearch keystore."`
);
});

it('throws if keystore does not contain a certificate', () => {
mockReadPkcs12Keystore.mockReturnValueOnce({ key: 'foo' });
const value = { ssl: { keystore: { path: 'some-path' } } };
expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot(
`"Did not find certificate in Elasticsearch keystore."`
);
});

it('throws if truststore path is invalid', () => {
const value = { ssl: { keystore: { path: '/invalid/truststore' } } };
expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/truststore'"`
);
});

it('throws if key and keystore.path are both specified', () => {
const value = { ssl: { key: 'foo', keystore: { path: 'bar' } } };
expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot(
`"[config validation of [xpack.monitoring.elasticsearch].ssl]: cannot use [key] when [keystore.path] is specified"`
);
});

it('throws if certificate and keystore.path are both specified', () => {
const value = { ssl: { certificate: 'foo', keystore: { path: 'bar' } } };
expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot(
`"[config validation of [xpack.monitoring.elasticsearch].ssl]: cannot use [certificate] when [keystore.path] is specified"`
);
});
});
Loading