Modifying SSL settings to be consistent with the stack (#9823)

This introduces the following new settings:
- server.ssl.enabled
- server.ssl.keyPassphrase
- server.ssl.certificateAuthorities
- server.ssl.clientAuthentication
- server.ssl.supportedProtocols
- elasticsearch.ssl.keyPassphrase


and deprecates the following:
- server.ssl.cert -> server.ssl.certificate
- elasticsearch.ssl.ca -> elasticsearch.ssl.certificateAuthorities
- elasticsearch.ssl.cert -> elasticsearch.ssl.certificate
- elasticsearch.ssl.verify -> elasticsearch.ssl.verificationMode
- console.proxyConfig
This commit is contained in:
Brandon Kobel 2017-01-25 10:58:56 -05:00 committed by GitHub
parent 1df8e80bc2
commit 5a4263835d
61 changed files with 1338 additions and 390 deletions

View file

@ -39,22 +39,23 @@
#elasticsearch.username: "user"
#elasticsearch.password: "pass"
# Paths to the PEM-format SSL certificate and SSL key files, respectively. These
# files enable SSL for outgoing requests from the Kibana server to the browser.
#server.ssl.cert: /path/to/your/server.crt
# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively.
# These settings enable SSL for outgoing requests from the Kibana server to the browser.
#server.ssl.enabled: false
#server.ssl.certificate: /path/to/your/server.crt
#server.ssl.key: /path/to/your/server.key
# Optional settings that provide the paths to the PEM-format SSL certificate and key files.
# These files validate that your Elasticsearch backend uses the same key files.
#elasticsearch.ssl.cert: /path/to/your/client.crt
#elasticsearch.ssl.certificate: /path/to/your/client.crt
#elasticsearch.ssl.key: /path/to/your/client.key
# Optional setting that enables you to specify a path to the PEM file for the certificate
# authority for your Elasticsearch instance.
#elasticsearch.ssl.ca: /path/to/your/CA.pem
#elasticsearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ]
# To disregard the validity of SSL certificates, change this setting's value to false.
#elasticsearch.ssl.verify: true
# To disregard the validity of SSL certificates, change this setting's value to 'none'.
#elasticsearch.ssl.verificationMode: full
# Time in milliseconds to wait for Elasticsearch to respond to pings. Defaults to the value of
# the elasticsearch.requestTimeout setting.

View file

@ -4,54 +4,3 @@
You can add the following options in the `config/kibana.yml` file:
`console.enabled`:: *Default: true* Set to false to disable Console. Toggling this will cause the server to regenerate assets on the next startup, which may cause a delay before pages start being served.
`console.proxyFilter`:: *Default: `.*`* A list of regular expressions that are used to validate any outgoing request from Console. If none
of these match, the request will be rejected. See <<securing-console>> for more details.
`console.proxyConfig`:: A list of configuration options that are based on the proxy target. Use this to set custom timeouts or SSL settings for specific hosts. This is done by defining a set of `match` criteria using wildcards/globs which will be checked against each request. The configuration from all matching rules will then be merged together to configure the proxy used for that request.
+
The valid match keys are `match.protocol`, `match.host`, `match.port`, and `match.path`. All of these keys default to `*`, which means they will match any value.
+
Example:
+
[source,yaml]
--------
console.proxyConfig:
- match:
host: "*.internal.org" # allow any host that ends in .internal.org
port: "{9200..9299}" # allow any port from 9200-9299
ssl:
ca: "/opt/certs/internal.ca"
# "key" and "cert" are also valid options here
- match:
protocol: "https"
ssl:
verify: false # allows any certificate to be used, even self-signed certs
# since this rule has no "match" section it matches everything
- timeout: 180000 # 3 minutes
--------
[[securing-console]]
=== Securing Console
Console is meant to be used as a local development tool. As such, it will send requests to any host & port combination,
just as a local curl command would. To overcome the CORS limitations enforced by browsers, Console's Node.js backend
serves as a proxy to send requests on behalf of the browser. However, if put on a server and exposed to the internet
this can become a security risk. In those cases, we highly recommend you lock down the proxy by setting the
`console.proxyFilter` setting. The setting accepts a list of regular expressions that are evaluated against each URL
the proxy is requested to retrieve. If none of the regular expressions match the proxy will reject the request.
Here is an example configuration the only allows Console to connect to localhost:
[source,yaml]
--------
console.proxyFilter:
- ^https?://(localhost|127\.0\.0\.1|\[::0\]).*
--------
You will need to restart Kibana for these changes to take effect.

View file

@ -42,14 +42,15 @@ to work with X-Pack, see {xpack-ref}kibana.html.
Kibana supports SSL encryption for both client requests and the requests the Kibana server
sends to Elasticsearch.
To encrypt communications between the browser and the Kibana server, you configure the `ssl_key_file` and
`ssl_cert_file` properties in `kibana.yml`:
To encrypt communications between the browser and the Kibana server, you configure the `server.ssl.enabled`,
`server.ssl.certificate` and `server.ssl.key` properties in `kibana.yml`:
[source,text]
----
# SSL for outgoing requests from the Kibana Server (PEM formatted)
server.ssl.enabled: true
server.ssl.key: /path/to/your/server.key
server.ssl.cert: /path/to/your/server.crt
server.ssl.certificate: /path/to/your/server.crt
----
If you are using X-Pack Security or a proxy that provides an HTTPS endpoint for Elasticsearch,
@ -61,17 +62,18 @@ protocol when you configure the Elasticsearch URL in `kibana.yml`:
[source,text]
----
elasticsearch: "https://<your_elasticsearch_host>.com:9200"
elasticsearch.url: "https://<your_elasticsearch_host>.com:9200"
----
If you are using a self-signed certificate for Elasticsearch, set the `ca` property in
`kibana.yml` to specify the location of the PEM file. Setting the `ca` property lets you leave the `verify_ssl` option enabled.
If you are using a self-signed certificate for Elasticsearch, set the `certificateAuthorities` property in
`kibana.yml` to specify the location of the PEM file. Setting the `certificateAuthorities` property lets you use the
default `verificationMode` option of `full`.
[source,text]
----
# If you need to provide a CA certificate for your Elasticsearch instance, put
# the path of the pem file here.
ca: /path/to/your/ca/cacert.pem
elasticsearch.ssl.certificateAuthorities: [ "/path/to/your/ca/cacert.pem" ]
----
[float]

View file

@ -33,14 +33,21 @@ Specify the position of the subdomain the URL with the token `{s}`.
`elasticsearch.username:` and `elasticsearch.password:`:: If your Elasticsearch is protected with basic authentication,
these settings provide the username and password that the Kibana server uses to perform maintenance on the Kibana index at
startup. Your Kibana users still need to authenticate with Elasticsearch, which is proxied through the Kibana server.
`server.ssl.cert:` and `server.ssl.key:`:: Paths to the PEM-format SSL certificate and SSL key files, respectively. These
files enable SSL for outgoing requests from the Kibana server to the browser.
`server.ssl.enabled`:: *Default: "false"* Enables SSL for outgoing requests from the Kibana server to the browser. When set to `true`, `server.ssl.certificate` and `server.ssl.key` are required
`server.ssl.certificate:` and `server.ssl.key:`:: Paths to the PEM-format SSL certificate and SSL key files, respectively.
`server.ssl.keyPassphrase`:: The passphrase that will be used to decrypt the private key. This value is optional as the key may not be encrypted.
`server.ssl.certificateAuthorities`:: List of paths to PEM encoded certificate files that should be trusted.
`server.ssl.clientAuthentication`:: *Default: none* Controls Kibana's server behavior in regard to requesting a certificate from client connections. Valid values are `required`,
and `none`. `required` forces a client to present a certificate, while `none` does not.
`server.ssl.supportedProtocols`:: *Default: TLSv1, TLSv1.1, TLSv1.2* Supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`
`server.ssl.cipherSuites`:: *Default: ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA*. Details on the format, and the valid options, are available via the [OpenSSL cipher list format documentation](https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT)
`elasticsearch.ssl.cert:` and `elasticsearch.ssl.key:`:: Optional settings that provide the paths to the PEM-format SSL
certificate and key files. These files validate that your Elasticsearch backend uses the same key files.
`elasticsearch.ssl.ca:`:: Optional setting that enables you to specify a path to the PEM file for the certificate
`elasticsearch.ssl.keyPassphrase`:: The passphrase that will be used to decrypt the private key. This value is optional as the key may not be encrypted.
`elasticsearch.ssl.certificateAuthorities:`:: Optional setting that enables you to specify a list of paths to the PEM file for the certificate
authority for your Elasticsearch instance.
`elasticsearch.ssl.verify:`:: *Default: true* To disregard the validity of SSL certificates, change this settings value
to `false`.
`elasticsearch.ssl.verificationMode:`:: *Default: full* Controls the verification of certificates. Valid values are `none`, `certificate`, and `full`.
`full` performs hostname verification, and `certificate` does not.
`elasticsearch.pingTimeout:`:: *Default: the value of the `elasticsearch.requestTimeout` setting* Time in milliseconds to
wait for Elasticsearch to respond to pings.
`elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait for responses from the back end or
@ -65,10 +72,6 @@ The minimum value is 100.
`status.allowAnonymous`:: *Default: false* If authentication is enabled, setting this to `true` allows
unauthenticated users to access the Kibana server status API and status page.
`console.enabled`:: *Default: true* Set to false to disable Console. Toggling this will cause the server to regenerate assets on the next startup, which may cause a delay before pages start being served.
`console.proxyFilter`:: *Default: `.*`* A list of regular expressions that are used to validate any outgoing request from Console. If none of these match, the request will be rejected.
`console.proxyConfig`:: A list of configuration options that are based on the proxy target. Use this to set custom timeouts or SSL settings for specific hosts. This is done by defining a set of `match` criteria using wildcards/globs which will be checked against each request. The configuration from all matching rules will then be merged together to configure the proxy used for that request.
+
The valid match keys are `match.protocol`, `match.host`, `match.port`, and `match.path`. All of these keys default to `*`, which means they will match any value. See <<configuring-console>> for an example.
`elasticsearch.tribe.url:`:: Optional URL of the Elasticsearch tribe instance to use for all your
queries.

View file

@ -1,8 +1,8 @@
import { Server } from 'hapi';
import { notFound } from 'boom';
import { merge, sample } from 'lodash';
import { map, merge, sample } from 'lodash';
import { format as formatUrl } from 'url';
import { map, fromNode } from 'bluebird';
import { map as promiseMap, fromNode } from 'bluebird';
import { Agent as HttpsAgent } from 'https';
import { readFileSync } from 'fs';
@ -10,6 +10,7 @@ import Config from '../../server/config/config';
import setupConnection from '../../server/http/setup_connection';
import registerHapiPlugins from '../../server/http/register_hapi_plugins';
import setupLogging from '../../server/logging';
import { transformDeprecations } from '../../server/config/transform_deprecations';
import { DEV_SSL_CERT_PATH } from '../dev_ssl';
const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split('');
@ -19,20 +20,21 @@ export default class BasePathProxy {
this.clusterManager = clusterManager;
this.server = new Server();
const config = Config.withDefaultSchema(userSettings);
const settings = transformDeprecations(userSettings);
const config = Config.withDefaultSchema(settings);
this.targetPort = config.get('dev.basePathProxyTarget');
this.basePath = config.get('server.basePath');
const { cert } = config.get('server.ssl');
if (cert) {
const httpsAgentConfig = {};
if (cert === DEV_SSL_CERT_PATH && config.get('server.host') !== 'localhost') {
httpsAgentConfig.rejectUnauthorized = false;
} else {
httpsAgentConfig.ca = readFileSync(cert);
}
this.proxyAgent = new HttpsAgent(httpsAgentConfig);
const sslEnabled = config.get('server.ssl.enabled');
if (sslEnabled) {
this.proxyAgent = new HttpsAgent({
key: readFileSync(config.get('server.ssl.key')),
passphrase: config.get('server.ssl.keyPassphrase'),
cert: readFileSync(config.get('server.ssl.certificate')),
ca: map(config.get('server.ssl.certificateAuthorities'), readFileSync),
rejectUnauthorized: false
});
}
if (!this.basePath) {
@ -67,7 +69,7 @@ export default class BasePathProxy {
config: {
pre: [
(req, reply) => {
map(clusterManager.workers, worker => {
promiseMap(clusterManager.workers, worker => {
if (worker.type === 'server' && !worker.listening && !worker.crashed) {
return fromNode(cb => {
const done = () => {

View file

@ -1,48 +0,0 @@
import expect from 'expect.js';
import { set } from 'lodash';
import { checkForDeprecatedConfig } from '../deprecated_config';
import sinon from 'auto-release-sinon';
describe('cli/serve/deprecated_config', function () {
it('passes original config through', function () {
const config = {};
set(config, 'server.xsrf.token', 'xxtokenxx');
const output = checkForDeprecatedConfig(config);
expect(output).to.be(config);
expect(output.server).to.be(config.server);
expect(output.server.xsrf).to.be(config.server.xsrf);
expect(output.server.xsrf.token).to.be(config.server.xsrf.token);
});
it('logs warnings about deprecated config values', function () {
const log = sinon.stub();
const config = {};
set(config, 'server.xsrf.token', 'xxtokenxx');
checkForDeprecatedConfig(config, log);
sinon.assert.calledOnce(log);
expect(log.firstCall.args[0]).to.match(/server\.xsrf\.token.+deprecated/);
});
describe('does not support compound.keys', function () {
it('ignores fully compound keys', function () {
const log = sinon.stub();
const config = { 'server.xsrf.token': 'xxtokenxx' };
checkForDeprecatedConfig(config, log);
sinon.assert.notCalled(log);
});
it('ignores partially compound keys', function () {
const log = sinon.stub();
const config = { server: { 'xsrf.token': 'xxtokenxx' } };
checkForDeprecatedConfig(config, log);
sinon.assert.notCalled(log);
});
it('ignores partially compound keys', function () {
const log = sinon.stub();
const config = { 'server.xsrf': { token: 'xxtokenxx' } };
checkForDeprecatedConfig(config, log);
sinon.assert.notCalled(log);
});
});
});

View file

@ -1 +0,0 @@
server.xsrf.token: token

View file

@ -1 +0,0 @@
kibana_index: indexname

View file

@ -1,28 +0,0 @@
import expect from 'expect.js';
import { rewriteLegacyConfig } from '../legacy_config';
import sinon from 'auto-release-sinon';
describe('cli/serve/legacy_config', function () {
it('returns a clone of the input', function () {
const file = {};
const output = rewriteLegacyConfig(file);
expect(output).to.not.be(file);
});
it('rewrites legacy config values with literal path replacement', function () {
const file = { port: 4000, host: 'kibana.com' };
const output = rewriteLegacyConfig(file);
expect(output).to.not.be(file);
expect(output).to.eql({
'server.port': 4000,
'server.host': 'kibana.com',
});
});
it('logs warnings when legacy config properties are encountered', function () {
const log = sinon.stub();
rewriteLegacyConfig({ port: 5555 }, log);
sinon.assert.calledOnce(log);
expect(log.firstCall.args[0]).to.match(/port.+deprecated.+server\.port/);
});
});

View file

@ -57,46 +57,4 @@ describe('cli/serve/read_yaml_config', function () {
process.chdir(oldCwd);
});
});
context('stubbed stdout', function () {
let stub;
beforeEach(function () {
stub = sinon.stub(process.stdout, 'write');
});
context('deprecated settings', function () {
it('warns about deprecated settings', function () {
readYamlConfig(fixture('deprecated.yml'));
sinon.assert.calledOnce(stub);
expect(stub.firstCall.args[0]).to.match(/deprecated/);
stub.restore();
});
it('only warns once about deprecated settings', function () {
readYamlConfig(fixture('deprecated.yml'));
readYamlConfig(fixture('deprecated.yml'));
readYamlConfig(fixture('deprecated.yml'));
sinon.assert.notCalled(stub); // already logged in previous test
stub.restore();
});
});
context('legacy settings', function () {
it('warns about deprecated settings', function () {
readYamlConfig(fixture('legacy.yml'));
sinon.assert.calledOnce(stub);
expect(stub.firstCall.args[0]).to.match(/has been replaced/);
stub.restore();
});
it('only warns once about legacy settings', function () {
readYamlConfig(fixture('legacy.yml'));
readYamlConfig(fixture('legacy.yml'));
readYamlConfig(fixture('legacy.yml'));
sinon.assert.notCalled(stub); // already logged in previous test
stub.restore();
});
});
});
});

View file

@ -1,16 +0,0 @@
import { forOwn, has, noop } from 'lodash';
// deprecated settings are still allowed, but will be removed at a later time. They
// are checked for after the config object is prepared and known, so legacySettings
// will have already been transformed.
export const deprecatedSettings = new Map([
[['server', 'xsrf', 'token'], 'server.xsrf.token is deprecated. It is no longer used when providing xsrf protection.']
]);
// check for and warn about deprecated settings
export function checkForDeprecatedConfig(object, log = noop) {
for (const [key, msg] of deprecatedSettings.entries()) {
if (has(object, key)) log(msg);
}
return object;
}

View file

@ -1,52 +0,0 @@
import { noop, transform } from 'lodash';
// legacySettings allow kibana 4.2+ to accept the same config file that people
// used for kibana 4.0 and 4.1. These settings are transformed to their modern
// equivalents at the very begining of the process
export const legacySettings = {
// server
port: 'server.port',
host: 'server.host',
pid_file: 'pid.file',
ssl_cert_file: 'server.ssl.cert',
ssl_key_file: 'server.ssl.key',
// logging
log_file: 'logging.dest',
// kibana
kibana_index: 'kibana.index',
default_app_id: 'kibana.defaultAppId',
// es
ca: 'elasticsearch.ssl.ca',
elasticsearch_preserve_host: 'elasticsearch.preserveHost',
elasticsearch_url: 'elasticsearch.url',
kibana_elasticsearch_client_crt: 'elasticsearch.ssl.cert',
kibana_elasticsearch_client_key: 'elasticsearch.ssl.key',
kibana_elasticsearch_password: 'elasticsearch.password',
kibana_elasticsearch_username: 'elasticsearch.username',
ping_timeout: 'elasticsearch.pingTimeout',
request_timeout: 'elasticsearch.requestTimeout',
shard_timeout: 'elasticsearch.shardTimeout',
startup_timeout: 'elasticsearch.startupTimeout',
tilemap_url: 'tilemap.url',
tilemap_min_zoom: 'tilemap.options.minZoom',
tilemap_max_zoom: 'tilemap.options.maxZoom',
tilemap_attribution: 'tilemap.options.attribution',
tilemap_subdomains: 'tilemap.options.subdomains',
verify_ssl: 'elasticsearch.ssl.verify',
};
// transform legacy options into new namespaced versions
export function rewriteLegacyConfig(object, log = noop) {
return transform(object, (clone, val, key) => {
if (legacySettings.hasOwnProperty(key)) {
const replacement = legacySettings[key];
log(`Config key "${key}" is deprecated. It has been replaced with "${replacement}"`);
clone[replacement] = val;
} else {
clone[key] = val;
}
}, {});
}

View file

@ -1,15 +1,9 @@
import { chain, isArray, isPlainObject, forOwn, memoize, set, transform } from 'lodash';
import { isArray, isPlainObject, forOwn, set, transform } from 'lodash';
import { readFileSync as read } from 'fs';
import { safeLoad } from 'js-yaml';
import { red } from 'ansicolors';
import { fromRoot } from '../../utils';
import { rewriteLegacyConfig } from './legacy_config';
import { checkForDeprecatedConfig } from './deprecated_config';
const log = memoize(function (message) {
console.log(red('WARNING:'), message);
});
export function merge(sources) {
return transform(sources, (merged, source) => {
@ -35,6 +29,5 @@ export function merge(sources) {
export default function (paths) {
const files = [].concat(paths || []);
const yamls = files.map(path => safeLoad(read(path, 'utf8')));
const config = merge(yamls.map(file => rewriteLegacyConfig(file, log)));
return checkForDeprecatedConfig(config, log);
return merge(yamls);
}

View file

@ -38,8 +38,13 @@ function readServerSettings(opts, extraCliOptions) {
if (opts.dev) {
set('env', 'development');
set('optimize.lazy', true);
if (opts.ssl && !has('server.ssl.cert') && !has('server.ssl.key')) {
set('server.ssl.cert', DEV_SSL_CERT_PATH);
if (opts.ssl) {
set('server.ssl.enabled', true);
}
if (opts.ssl && !has('server.ssl.certificate') && !has('server.ssl.key')) {
set('server.ssl.certificate', DEV_SSL_CERT_PATH);
set('server.ssl.key', DEV_SSL_KEY_PATH);
}
}

View file

@ -0,0 +1,56 @@
import { Deprecations } from '../../../deprecation';
import expect from 'expect.js';
import index from '../index';
import { noop } from 'lodash';
import sinon from 'sinon';
describe('plugins/console', function () {
describe('#deprecate()', function () {
let transformDeprecations;
before(function () {
const Plugin = function (options) {
this.deprecations = options.deprecations;
};
const plugin = index({ Plugin });
const deprecations = plugin.deprecations(Deprecations);
transformDeprecations = (settings, log = noop) => {
deprecations.forEach(deprecation => deprecation(settings, log));
};
});
context('proxyConfig', function () {
it('leaves the proxyConfig settings', function () {
const proxyConfigOne = {};
const proxyConfigTwo = {};
const settings = {
proxyConfig: [proxyConfigOne, proxyConfigTwo]
};
transformDeprecations(settings);
expect(settings.proxyConfig[0]).to.be(proxyConfigOne);
expect(settings.proxyConfig[1]).to.be(proxyConfigTwo);
});
it('logs a warning when proxyConfig is specified', function () {
const settings = {
proxyConfig: []
};
const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.calledOnce).to.be(true);
});
it(`doesn't log a warning when proxyConfig isn't specified`, function () {
const settings = {};
const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(false);
});
});
});
});

View file

@ -3,8 +3,9 @@ import Boom from 'boom';
import apiServer from './api_server/server';
import { existsSync } from 'fs';
import { resolve, join, sep } from 'path';
import { startsWith, endsWith } from 'lodash';
import { has, startsWith, endsWith } from 'lodash';
import { ProxyConfigCollection } from './server/proxy_config_collection';
import { getElasticsearchProxyConfig } from './server/elasticsearch_proxy_config';
export default function (kibana) {
const modules = resolve(__dirname, 'public/webpackShims/');
@ -50,24 +51,20 @@ export default function (kibana) {
key: Joi.string()
}).default()
})
).default([
{
match: {
protocol: '*',
host: '*',
port: '*',
path: '*'
},
timeout: 180000,
ssl: {
verify: true
}
}
])
).default()
}).default();
},
deprecations: function () {
return [
(settings, log) => {
if (has(settings, 'proxyConfig')) {
log('Config key "proxyConfig" is deprecated. Configuration can be inferred from the "elasticsearch" settings');
}
}
];
},
init: function (server, options) {
const filters = options.proxyFilter.map(str => new RegExp(str));
@ -108,6 +105,14 @@ export default function (kibana) {
const requestHeadersWhitelist = server.config().get('elasticsearch.requestHeadersWhitelist');
const filterHeaders = server.plugins.elasticsearch.filterHeaders;
let additionalConfig;
if (server.config().get('console.proxyConfig')) {
additionalConfig = proxyConfigCollection.configForUri(uri);
} else {
additionalConfig = getElasticsearchProxyConfig(server);
}
reply.proxy({
mapUri: function (request, done) {
done(null, uri, filterHeaders(request.headers, requestHeadersWhitelist));
@ -121,7 +126,7 @@ export default function (kibana) {
}
},
...proxyConfigCollection.configForUri(uri)
...additionalConfig
});
}
};

View file

@ -0,0 +1,121 @@
import expect from 'expect.js';
import { getElasticsearchProxyConfig } from '../elasticsearch_proxy_config';
import https from 'https';
import http from 'http';
import sinon from 'sinon';
describe('plugins/console', function () {
describe('#getElasticsearchProxyConfig', function () {
let server;
beforeEach(function () {
const stub = sinon.stub();
server = {
config() {
return {
get: stub
};
}
};
server.config().get.withArgs('elasticsearch.url').returns('http://localhost:9200');
server.config().get.withArgs('elasticsearch.ssl.verificationMode').returns('full');
});
const setElasticsearchConfig = (key, value) => {
server.config().get.withArgs(`elasticsearch.${key}`).returns(value);
};
it('sets timeout', function () {
const value = 1000;
setElasticsearchConfig('requestTimeout', value);
const proxyConfig = getElasticsearchProxyConfig(server);
expect(proxyConfig.timeout).to.be(value);
});
it(`uses https.Agent when url's protocol is https`, function () {
setElasticsearchConfig('url', 'https://localhost:9200');
const { agent } = getElasticsearchProxyConfig(server);
expect(agent).to.be.a(https.Agent);
});
it(`uses http.Agent when url's protocol is http`, function () {
setElasticsearchConfig('url', 'http://localhost:9200');
const { agent } = getElasticsearchProxyConfig(server);
expect(agent).to.be.a(http.Agent);
});
context('ssl', function () {
beforeEach(function () {
setElasticsearchConfig('url', 'https://localhost:9200');
});
it('sets rejectUnauthorized to false when verificationMode is none', function () {
setElasticsearchConfig('ssl.verificationMode', 'none');
const { agent } = getElasticsearchProxyConfig(server);
expect(agent.options.rejectUnauthorized).to.be(false);
});
it('sets rejectUnauthorized to true when verificationMode is certificate', function () {
setElasticsearchConfig('ssl.verificationMode', 'certificate');
const { agent } = getElasticsearchProxyConfig(server);
expect(agent.options.rejectUnauthorized).to.be(true);
});
it('sets checkServerIdentity to not check hostname when verificationMode is certificate', function () {
setElasticsearchConfig('ssl.verificationMode', 'certificate');
const { agent } = getElasticsearchProxyConfig(server);
const cert = {
subject: {
CN: 'wrong.com'
}
};
expect(agent.options.checkServerIdentity).withArgs('right.com', cert).to.not.throwException();
const result = agent.options.checkServerIdentity('right.com', cert);
expect(result).to.be(undefined);
});
it('sets rejectUnauthorized to true when verificationMode is full', function () {
setElasticsearchConfig('ssl.verificationMode', 'full');
const { agent } = getElasticsearchProxyConfig(server);
expect(agent.options.rejectUnauthorized).to.be(true);
});
it(`doesn't set checkServerIdentity when verificationMode is full`, function () {
setElasticsearchConfig('ssl.verificationMode', 'full');
const { agent } = getElasticsearchProxyConfig(server);
expect(agent.options.checkServerIdentity).to.be(undefined);
});
it(`sets ca when certificateAuthorities are specified`, function () {
setElasticsearchConfig('ssl.certificateAuthorities', [__dirname + '/fixtures/ca.crt']);
const { agent } = getElasticsearchProxyConfig(server);
expect(agent.options.ca).to.contain('test ca certificate\n');
});
it(`sets cert and key when certificate and key paths are specified`, function () {
setElasticsearchConfig('ssl.certificate', __dirname + '/fixtures/cert.crt');
setElasticsearchConfig('ssl.key', __dirname + '/fixtures/cert.key');
const { agent } = getElasticsearchProxyConfig(server);
expect(agent.options.cert).to.be('test certificate\n');
expect(agent.options.key).to.be('test key\n');
});
it(`sets passphrase when certificate, key and keyPassphrase are specified`, function () {
setElasticsearchConfig('ssl.certificate', __dirname + '/fixtures/cert.crt');
setElasticsearchConfig('ssl.key', __dirname + '/fixtures/cert.key');
setElasticsearchConfig('ssl.keyPassphrase', 'secret');
const { agent } = getElasticsearchProxyConfig(server);
expect(agent.options.passphrase).to.be('secret');
});
});
});
});

View file

@ -0,0 +1 @@
test ca certificate

View file

@ -0,0 +1 @@
test certificate

View file

@ -0,0 +1 @@
test key

View file

@ -0,0 +1,54 @@
import _ from 'lodash';
import { readFileSync } from 'fs';
import http from 'http';
import https from 'https';
import url from 'url';
const readFile = (file) => readFileSync(file, 'utf8');
const createAgent = (server) => {
const config = server.config();
const target = url.parse(config.get('elasticsearch.url'));
if (!/^https/.test(target.protocol)) return new http.Agent();
const agentOptions = {};
const verificationMode = config.get('elasticsearch.ssl.verificationMode');
switch (verificationMode) {
case 'none':
agentOptions.rejectUnauthorized = false;
break;
case 'certificate':
agentOptions.rejectUnauthorized = true;
// by default, NodeJS is checking the server identify
agentOptions.checkServerIdentity = _.noop;
break;
case 'full':
agentOptions.rejectUnauthorized = true;
break;
default:
throw new Error(`Unknown ssl verificationMode: ${verificationMode}`);
}
if (_.size(config.get('elasticsearch.ssl.certificateAuthorities'))) {
agentOptions.ca = config.get('elasticsearch.ssl.certificateAuthorities').map(readFile);
}
// Add client certificate and key if required by elasticsearch
if (config.get('elasticsearch.ssl.certificate') && config.get('elasticsearch.ssl.key')) {
agentOptions.cert = readFile(config.get('elasticsearch.ssl.certificate'));
agentOptions.key = readFile(config.get('elasticsearch.ssl.key'));
agentOptions.passphrase = config.get('elasticsearch.ssl.keyPassphrase');
}
return new https.Agent(agentOptions);
};
export const getElasticsearchProxyConfig = (server) => {
return {
timeout: server.config().get('elasticsearch.requestTimeout'),
agent: createAgent(server)
};
};

View file

@ -0,0 +1,84 @@
import { Deprecations } from '../../../deprecation';
import expect from 'expect.js';
import index from '../index';
import { compact, noop, set } from 'lodash';
import sinon from 'sinon';
describe('plugins/elasticsearch', function () {
describe('#deprecations()', function () {
let transformDeprecations;
before(function () {
const Plugin = function (options) {
this.deprecations = options.deprecations;
};
const plugin = index({ Plugin });
const deprecations = plugin.deprecations(Deprecations);
transformDeprecations = (settings, log = noop) => {
deprecations.forEach(deprecation => deprecation(settings, log));
};
});
[null, 'tribe'].forEach((basePath) => {
const getKey = (path) => {
return compact([basePath, path]).join('.');
};
context(getKey('ssl.verificationMode'), function () {
let settings;
let sslSettings;
beforeEach(function () {
settings = {};
sslSettings = {};
set(settings, getKey('ssl'), sslSettings);
});
it(`sets verificationMode to none when verify is false`, function () {
sslSettings.verify = false;
transformDeprecations(settings);
expect(sslSettings.verificationMode).to.be('none');
expect(sslSettings.verify).to.be(undefined);
});
it('should log when deprecating verify from false', function () {
sslSettings.verify = false;
const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.calledOnce).to.be(true);
});
it('sets verificationMode to full when verify is true', function () {
sslSettings.verify = true;
transformDeprecations(settings);
expect(sslSettings.verificationMode).to.be('full');
expect(sslSettings.verify).to.be(undefined);
});
it('should log when deprecating verify from true', function () {
sslSettings.verify = true;
const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.calledOnce).to.be(true);
});
it(`shouldn't set verificationMode when verify isn't present`, function () {
transformDeprecations(settings);
expect(sslSettings.verificationMode).to.be(undefined);
});
it(`shouldn't log when verify isn't present`, function () {
const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(false);
});
});
});
});
});

View file

@ -1,4 +1,5 @@
import { trim, trimRight, bindKey, get } from 'lodash';
import { compact, get, has, set, trim, trimRight } from 'lodash';
import { unset } from '../../utils';
import { methodNotAllowed } from 'boom';
import healthCheck from './lib/health_check';
@ -19,6 +20,14 @@ module.exports = function ({ Plugin }) {
config(Joi) {
const { array, boolean, number, object, string, ref } = Joi;
const sslSchema = object({
verificationMode: string().valid('none', 'certificate', 'full').default('full'),
certificateAuthorities: array().single().items(string()),
certificate: string(),
key: string(),
keyPassphrase: string()
}).default();
return object({
enabled: boolean().default(true),
url: string().uri({ scheme: ['http', 'https'] }).default('http://localhost:9200'),
@ -32,12 +41,7 @@ module.exports = function ({ Plugin }) {
pingTimeout: number().default(ref('requestTimeout')),
startupTimeout: number().default(5000),
logQueries: boolean().default(false),
ssl: object({
verify: boolean().default(true),
ca: array().single().items(string()),
cert: string(),
key: string()
}).default(),
ssl: sslSchema,
apiVersion: Joi.string().default('master'),
healthCheck: object({
delay: number().default(2500)
@ -54,17 +58,43 @@ module.exports = function ({ Plugin }) {
pingTimeout: number().default(ref('requestTimeout')),
startupTimeout: number().default(5000),
logQueries: boolean().default(false),
ssl: object({
verify: boolean().default(true),
ca: array().single().items(string()),
cert: string(),
key: string()
}).default(),
ssl: sslSchema,
apiVersion: Joi.string().default('master'),
}).default()
}).default();
},
deprecations({ rename }) {
const sslVerify = (basePath) => {
const getKey = (path) => {
return compact([basePath, path]).join('.');
};
return (settings, log) => {
const sslSettings = get(settings, getKey('ssl'));
if (!has(sslSettings, 'verify')) {
return;
}
const verificationMode = get(sslSettings, 'verify') ? 'full' : 'none';
set(sslSettings, 'verificationMode', verificationMode);
unset(sslSettings, 'verify');
log(`Config key "${getKey('ssl.verify')}" is deprecated. It has been replaced with "${getKey('ssl.verificationMode')}"`);
};
};
return [
rename('ssl.ca', 'ssl.certificateAuthorities'),
rename('ssl.cert', 'ssl.certificate'),
sslVerify(),
rename('tribe.ssl.ca', 'tribe.ssl.certificateAuthorities'),
rename('tribe.ssl.cert', 'tribe.ssl.certificate'),
sslVerify('tribe')
];
},
uiExports: {
injectDefaultVars(server, options) {
return {

View file

@ -10,7 +10,7 @@ describe('plugins/elasticsearch', function () {
let cluster;
const config = {
url: 'http://localhost:9200',
ssl: { verify: false },
ssl: { verificationMode: 'full' },
requestHeadersWhitelist: [ 'authorization' ]
};

View file

@ -0,0 +1,39 @@
import expect from 'expect.js';
import createAgent from '../create_agent';
import https from 'https';
import http from 'http';
import { set } from 'lodash';
describe('plugins/elasticsearch', function () {
describe('lib/create_agent', function () {
it(`uses http.Agent when url's protocol is http`, function () {
const config = {
url: 'http://localhost:9200'
};
const agent = createAgent(config);
expect(agent).to.be.a(http.Agent);
});
it(`throws an Error when url's protocol is https and ssl.verificationMode isn't set`, function () {
const config = {
url: 'https://localhost:9200'
};
expect(createAgent).withArgs(config).to.throwException();
});
it(`uses https.Agent when url's protocol is https and ssl.verificationMode is full`, function () {
const config = {
url: 'https://localhost:9200',
ssl: {
verificationMode: 'full'
}
};
const agent = createAgent(config);
expect(agent).to.be.a(https.Agent);
});
});
});

View file

@ -25,7 +25,7 @@ describe('plugins/elasticsearch', function () {
const config = {
url: 'http://localhost:9200',
ssl: {
verify: false
verificationMode: 'none'
}
};

View file

@ -0,0 +1 @@
test ca certificate

View file

@ -0,0 +1 @@
test certificate

View file

@ -0,0 +1 @@
test key

View file

@ -0,0 +1,91 @@
import expect from 'expect.js';
import { parseConfig } from '../parse_config';
describe('plugins/elasticsearch', function () {
describe('lib/parse_config', function () {
context('ssl', function () {
let serverConfig;
beforeEach(function () {
serverConfig = {
url: 'https://localhost:9200',
ssl: {
verificationMode: 'full'
}
};
});
it('throws an Exception when verificationMode is undefined', function () {
delete serverConfig.ssl.verificationMode;
expect(parseConfig).withArgs(serverConfig).to.throwException();
});
it('sets rejectUnauthorized to false when verificationMode is none', function () {
serverConfig.ssl.verificationMode = 'none';
const config = parseConfig(serverConfig);
expect(config.ssl.rejectUnauthorized).to.be(false);
});
it('sets rejectUnauthorized to true when verificationMode is certificate', function () {
serverConfig.ssl.verificationMode = 'certificate';
const config = parseConfig(serverConfig);
expect(config.ssl.rejectUnauthorized).to.be(true);
});
it('sets checkServerIdentity to not check hostname when verificationMode is certificate', function () {
serverConfig.ssl.verificationMode = 'certificate';
const config = parseConfig(serverConfig);
const cert = {
subject: {
CN: 'wrong.com'
}
};
expect(config.ssl.checkServerIdentity).withArgs('right.com', cert).to.not.throwException();
const result = config.ssl.checkServerIdentity('right.com', cert);
expect(result).to.be(undefined);
});
it('sets rejectUnauthorized to true when verificationMode is full', function () {
serverConfig.ssl.verificationMode = 'full';
const config = parseConfig(serverConfig);
expect(config.ssl.rejectUnauthorized).to.be(true);
});
it(`doesn't set checkServerIdentity when verificationMode is full`, function () {
serverConfig.ssl.verificationMode = 'full';
const config = parseConfig(serverConfig);
expect(config.ssl.checkServerIdentity).to.be(undefined);
});
it(`sets ca when certificateAuthorities are specified`, function () {
serverConfig.ssl.certificateAuthorities = [__dirname + '/fixtures/ca.crt'];
const config = parseConfig(serverConfig);
expect(config.ssl.ca).to.contain('test ca certificate\n');
});
it(`sets cert and key when certificate and key paths are specified`, function () {
serverConfig.ssl.certificate = __dirname + '/fixtures/cert.crt';
serverConfig.ssl.key = __dirname + '/fixtures/cert.key';
const config = parseConfig(serverConfig);
expect(config.ssl.cert).to.be('test certificate\n');
expect(config.ssl.key).to.be('test key\n');
});
it(`sets passphrase when certificate, key and keyPassphrase are specified`, function () {
serverConfig.ssl.certificate = __dirname + '/fixtures/cert.crt';
serverConfig.ssl.key = __dirname + '/fixtures/cert.key';
serverConfig.ssl.keyPassphrase = 'secret';
const config = parseConfig(serverConfig);
expect(config.ssl.passphrase).to.be('secret');
});
});
});
});

View file

@ -1,6 +1,6 @@
import util from 'util';
import url from 'url';
import { get, size, pick } from 'lodash';
import { get, noop, size, pick } from 'lodash';
import { readFileSync } from 'fs';
import Bluebird from 'bluebird';
@ -30,15 +30,35 @@ export function parseConfig(serverConfig = {}) {
}
// SSL
config.ssl = { rejectUnauthorized: get(serverConfig, 'ssl.verify') };
config.ssl = {};
if (get(serverConfig, 'ssl.cert') && get(serverConfig, 'ssl.key')) {
config.ssl.cert = readFile(serverConfig.ssl.cert);
config.ssl.key = readFile(serverConfig.ssl.key);
const verificationMode = get(serverConfig, 'ssl.verificationMode');
switch (verificationMode) {
case 'none':
config.ssl.rejectUnauthorized = false;
break;
case 'certificate':
config.ssl.rejectUnauthorized = true;
// by default, NodeJS is checking the server identify
config.ssl.checkServerIdentity = noop;
break;
case 'full':
config.ssl.rejectUnauthorized = true;
break;
default:
throw new Error(`Unknown ssl verificationMode: ${verificationMode}`);
}
if (size(get(serverConfig, 'ssl.ca'))) {
config.ssl.ca = serverConfig.ssl.ca.map(readFile);
if (size(get(serverConfig, 'ssl.certificateAuthorities'))) {
config.ssl.ca = serverConfig.ssl.certificateAuthorities.map(readFile);
}
// Add client certificate and key if required by elasticsearch
if (get(serverConfig, 'ssl.certificate') && get(serverConfig, 'ssl.key')) {
config.ssl.cert = readFile(serverConfig.ssl.certificate);
config.ssl.key = readFile(serverConfig.ssl.key);
config.ssl.passphrase = serverConfig.ssl.keyPassphrase;
}
config.defer = () => Bluebird.defer();

View file

@ -0,0 +1,39 @@
import createTransform from '../create_transform';
import expect from 'expect.js';
import { noop } from 'lodash';
import sinon from 'sinon';
describe('deprecation', function () {
describe('createTransform', function () {
it(`doesn't modify settings parameter`, function () {
const settings = {
original: true
};
const deprecations = [(settings) => {
settings.origial = false;
}];
createTransform(deprecations)(settings);
expect(settings.original).to.be(true);
});
it('calls single deprecation in array', function () {
const deprecations = [sinon.spy()];
createTransform(deprecations)({});
expect(deprecations[0].calledOnce).to.be(true);
});
it('calls multiple deprecations in array', function () {
const deprecations = [sinon.spy(), sinon.spy()];
createTransform(deprecations)({});
expect(deprecations[0].calledOnce).to.be(true);
expect(deprecations[1].calledOnce).to.be(true);
});
it('passes log function to deprecation', function () {
const deprecation = sinon.spy();
const log = function () {};
createTransform([deprecation])({}, log);
expect(deprecation.args[0][1]).to.be(log);
});
});
});

View file

@ -0,0 +1,14 @@
import { deepCloneWithBuffers as clone } from '../utils';
import { forEach, noop } from 'lodash';
export default function (deprecations) {
return (settings, log = noop) => {
const result = clone(settings);
forEach(deprecations, (deprecation) => {
deprecation(result, log);
});
return result;
};
}

View file

@ -0,0 +1,65 @@
import expect from 'expect.js';
import { noop } from 'lodash';
import rename from '../rename';
import sinon from 'sinon';
describe('deprecation/deprecations', function () {
describe('rename', function () {
it('should rename simple property', function () {
const value = 'value';
const settings = {
before: value
};
rename('before', 'after')(settings);
expect(settings.before).to.be(undefined);
expect(settings.after).to.be(value);
});
it ('should rename nested property', function () {
const value = 'value';
const settings = {
someObject: {
before: value
}
};
rename('someObject.before', 'someObject.after')(settings);
expect(settings.someObject.before).to.be(undefined);
expect(settings.someObject.after).to.be(value);
});
it ('should rename property, even when the value is null', function () {
const value = null;
const settings = {
before: value
};
rename('before', 'after')(settings);
expect(settings.before).to.be(undefined);
expect(settings.after).to.be(null);
});
it (`shouldn't log when a rename doesn't occur`, function () {
const settings = {
exists: true
};
const log = sinon.spy();
rename('doesntExist', 'alsoDoesntExist')(settings, log);
expect(log.called).to.be(false);
});
it ('should log when a rename does occur', function () {
const settings = {
exists: true
};
const log = sinon.spy();
rename('exists', 'alsoExists')(settings, log);
expect(log.calledOnce).to.be(true);
expect(log.args[0][0]).to.match(/exists.+deprecated/);
});
});
});

View file

@ -0,0 +1,57 @@
import expect from 'expect.js';
import sinon from 'sinon';
import unused from '../unused';
describe('deprecation/deprecations', function () {
describe('unused', function () {
it('should remove unused setting', function () {
const settings = {
old: true
};
unused('old')(settings);
expect(settings.old).to.be(undefined);
});
it(`shouldn't remove used setting`, function () {
const value = 'value';
const settings = {
new: value
};
unused('old')(settings);
expect(settings.new).to.be(value);
});
it('should remove unused setting, even when null', function () {
const settings = {
old: null
};
unused('old')(settings);
expect(settings.old).to.be(undefined);
});
it('should log when removing unused setting', function () {
const settings = {
old: true
};
const log = sinon.spy();
unused('old')(settings, log);
expect(log.calledOnce).to.be(true);
expect(log.args[0][0]).to.match(/old.+deprecated/);
});
it(`shouldn't log when no setting is unused`, function () {
const settings = {
new: true
};
const log = sinon.spy();
unused('old')(settings, log);
expect(log.called).to.be(false);
});
});
});

View file

@ -0,0 +1,2 @@
export rename from './rename';
export unused from './unused';

View file

@ -0,0 +1,16 @@
import { get, isUndefined, noop, set } from 'lodash';
import { unset } from '../../utils';
export default function (oldKey, newKey) {
return (settings, log = noop) => {
const value = get(settings, oldKey);
if (isUndefined(value)) {
return;
}
unset(settings, oldKey);
set(settings, newKey, value);
log(`Config key "${oldKey}" is deprecated. It has been replaced with "${newKey}"`);
};
}

View file

@ -0,0 +1,14 @@
import { get, isUndefined, noop } from 'lodash';
import { unset } from '../../utils';
export default function (oldKey) {
return (settings, log = noop) => {
const value = get(settings, oldKey);
if (isUndefined(value)) {
return;
}
unset(settings, oldKey);
log(`${oldKey} is deprecated and is no longer used`);
};
}

2
src/deprecation/index.js Normal file
View file

@ -0,0 +1,2 @@
export createTransform from './create_transform';
export * as Deprecations from './deprecations';

View file

@ -0,0 +1,99 @@
import complete from '../complete';
import expect from 'expect.js';
import { noop } from 'lodash';
import sinon from 'sinon';
describe('server config complete', function () {
it(`should call server.log when there's an unused setting`, function () {
const kbnServer = {
settings: {
unused: true
}
};
const server = {
decorate: noop,
log: sinon.spy()
};
const config = {
get: sinon.stub().returns({
used: true
})
};
complete(kbnServer, server, config);
expect(server.log.calledOnce).to.be(true);
});
it(`shouldn't call server.log when there isn't an unused setting`, function () {
const kbnServer = {
settings: {
used: true
}
};
const server = {
decorate: noop,
log: sinon.spy()
};
const config = {
get: sinon.stub().returns({
used: true
})
};
complete(kbnServer, server, config);
expect(server.log.called).to.be(false);
});
it(`shouldn't call server.log when there are more config values than settings`, function () {
const kbnServer = {
settings: {
used: true
}
};
const server = {
decorate: noop,
log: sinon.spy()
};
const config = {
get: sinon.stub().returns({
used: true,
foo: 'bar'
})
};
complete(kbnServer, server, config);
expect(server.log.called).to.be(false);
});
it('should transform deprecated settings ', function () {
const kbnServer = {
settings: {
port: 8080
}
};
const server = {
decorate: noop,
log: sinon.spy()
};
const config = {
get: sinon.stub().returns({
server: {
port: 8080
}
})
};
complete(kbnServer, server, config);
expect(server.log.called).to.be(false);
});
});

View file

@ -217,13 +217,13 @@ describe('lib/config/config', function () {
it('should allow you to extend the schema at the top level', function () {
const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
config.extendSchema('myTest', newSchema);
config.extendSchema(newSchema, {}, 'myTest');
expect(config.get('myTest.test')).to.be(true);
});
it('should allow you to extend the schema with a prefix', function () {
const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
config.extendSchema('prefix.myTest', newSchema);
config.extendSchema(newSchema, {}, 'prefix.myTest');
expect(config.get('prefix')).to.eql({ myTest: { test: true } });
expect(config.get('prefix.myTest')).to.eql({ test: true });
expect(config.get('prefix.myTest.test')).to.be(true);

View file

@ -1,6 +1,7 @@
import schemaProvider from '../schema';
import expect from 'expect.js';
import Joi from 'joi';
import { set } from 'lodash';
describe('Config schema', function () {
let schema;
@ -11,6 +12,11 @@ describe('Config schema', function () {
}
describe('server', function () {
it('everything is optional', function () {
const { error } = validate({});
expect(error).to.be(null);
});
describe('basePath', function () {
it('accepts empty strings', function () {
const { error } = validate({ server: { basePath: '' } });
@ -33,6 +39,163 @@ describe('Config schema', function () {
expect(error).to.have.property('details');
expect(error.details[0]).to.have.property('path', 'server.basePath');
});
});
describe('ssl', function () {
describe('enabled', function () {
it('can\'t be a string', function () {
const config = {};
set(config, 'server.ssl.enabled', 'bogus');
const { error } = validate(config);
expect(error).to.be.an(Object);
expect(error).to.have.property('details');
expect(error.details[0]).to.have.property('path', 'server.ssl.enabled');
});
it('can be true', function () {
const config = {};
set(config, 'server.ssl.enabled', true);
set(config, 'server.ssl.certificate', '/path.cert');
set(config, 'server.ssl.key', '/path.key');
const { error } = validate(config);
expect(error).to.be(null);
});
it('can be false', function () {
const config = {};
set(config, 'server.ssl.enabled', false);
const { error } = validate(config);
expect(error).to.be(null);
});
});
describe('certificate', function () {
it('isn\'t required when ssl isn\'t enabled', function () {
const config = {};
set(config, 'server.ssl.enabled', false);
const { error } = validate(config);
expect(error).to.be(null);
});
it('is required when ssl is enabled', function () {
const config = {};
set(config, 'server.ssl.enabled', true);
set(config, 'server.ssl.key', '/path.key');
const { error } = validate(config);
expect(error).to.be.an(Object);
expect(error).to.have.property('details');
expect(error.details[0]).to.have.property('path', 'server.ssl.certificate');
});
});
describe('key', function () {
it('isn\'t required when ssl isn\'t enabled', function () {
const config = {};
set(config, 'server.ssl.enabled', false);
const { error } = validate(config);
expect(error).to.be(null);
});
it('is required when ssl is enabled', function () {
const config = {};
set(config, 'server.ssl.enabled', true);
set(config, 'server.ssl.certificate', '/path.cert');
const { error } = validate(config);
expect(error).to.be.an(Object);
expect(error).to.have.property('details');
expect(error.details[0]).to.have.property('path', 'server.ssl.key');
});
});
describe('keyPassphrase', function () {
it ('is a possible config value', function () {
const config = {};
set(config, 'server.ssl.keyPassphrase', 'password');
const { error } = validate(config);
expect(error).to.be(null);
});
});
describe('clientAuthentication', function () {
it ('defaults to \'none\'', function () {
const config = {};
const { error, value } = validate({});
expect(error).to.be(null);
expect(value).to.be.an(Object);
expect(value.server).to.be.an(Object);
expect(value.server.ssl).to.be.an(Object);
expect(value.server.ssl.clientAuthentication).to.be('none');
});
['none', 'required'].forEach((option) => {
it(`allows ${option}`, function () {
const config = {};
set(config, 'server.ssl.clientAuthentication', option);
const { error } = validate(config);
expect(error).to.be(null);
});
});
['bogus', 'somethingelse'].forEach((option) => {
it(`rejects ${option}`, function () {
const config = {};
set(config, 'server.ssl.clientAuthentication', option);
const { error } = validate(config);
expect(error).to.be.an(Object);
expect(error).to.have.property('details');
expect(error.details[0]).to.have.property('path', 'server.ssl.clientAuthentication');
});
});
});
describe('certificateAuthorities', function () {
it('allows array of string', function () {
const config = {};
set(config, 'server.ssl.certificateAuthorities', ['/path1.crt', '/path2.crt']);
const { error } = validate(config);
expect(error).to.be(null);
});
it('allows a single string', function () {
const config = {};
set(config, 'server.ssl.certificateAuthorities', '/path1.crt');
const { error } = validate(config);
expect(error).to.be(null);
});
});
describe('supportedProtocols', function () {
it ('rejects SSLv2', function () {
const config = {};
set(config, 'server.ssl.supportedProtocols', ['SSLv2']);
const { error } = validate(config);
expect(error).to.be.an(Object);
expect(error).to.have.property('details');
expect(error.details[0]).to.have.property('path', 'server.ssl.supportedProtocols.0');
});
it('rejects SSLv3', function () {
const config = {};
set(config, 'server.ssl.supportedProtocols', ['SSLv3']);
const { error } = validate(config);
expect(error).to.be.an(Object);
expect(error).to.have.property('details');
expect(error.details[0]).to.have.property('path', 'server.ssl.supportedProtocols.0');
});
it('accepts TLSv1, TLSv1.1, TLSv1.2', function () {
const config = {};
set(config, 'server.ssl.supportedProtocols', ['TLSv1', 'TLSv1.1', 'TLSv1.2']);
const { error } = validate(config);
expect(error).to.be(null);
});
});
});
});
});

View file

@ -0,0 +1,61 @@
import expect from 'expect.js';
import sinon from 'sinon';
import { transformDeprecations } from '../transform_deprecations';
describe('server/config', function () {
describe('transformDeprecations', function () {
describe('server.ssl.enabled', function () {
it('sets enabled to true when certificate and key are set', function () {
const settings = {
server: {
ssl: {
certificate: '/cert.crt',
key: '/key.key'
}
}
};
const result = transformDeprecations(settings);
expect(result.server.ssl.enabled).to.be(true);
});
it('logs a message when automatically setting enabled to true', function () {
const settings = {
server: {
ssl: {
certificate: '/cert.crt',
key: '/key.key'
}
}
};
const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.calledOnce).to.be(true);
});
it(`doesn't set enabled when key and cert aren't set`, function () {
const settings = {
server: {
ssl: {}
}
};
const result = transformDeprecations(settings);
expect(result.server.ssl.enabled).to.be(undefined);
});
it(`doesn't log a message when not automatically setting enabled`, function () {
const settings = {
server: {
ssl: {}
}
};
const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(false);
});
});
});
});

View file

@ -1,11 +1,17 @@
import { difference, keys } from 'lodash';
import { transformDeprecations } from './transform_deprecations';
const getUnusedSettings = (settings, configValues) => {
return difference(keys(transformDeprecations(settings)), keys(configValues));
};
export default function (kbnServer, server, config) {
server.decorate('server', 'config', function () {
return kbnServer.config;
});
const tmpl = 'Settings for "<%= key %>" were not applied, check for spelling errors and ensure the plugin is loaded.';
for (const [key, val] of config.getPendingSets()) {
server.log(['warning', 'config'], { key, val, tmpl });
for (const key of getUnusedSettings(kbnServer.settings, config.get())) {
server.log(['warning', 'config'], `Settings for "${key}" were not applied, check for spelling errors and ensure the plugin is loaded.`);
}
}

View file

@ -1,15 +1,13 @@
import Joi from 'joi';
import _ from 'lodash';
import override from './override';
import unset from './unset';
import createDefaultSchema from './schema';
import pkg from '../../utils/package_json';
import clone from './deep_clone_with_buffers';
import { pkg, unset } from '../../utils';
import { deepCloneWithBuffers as clone } from '../../utils';
const schema = Symbol('Joi Schema');
const schemaExts = Symbol('Schema Extensions');
const vals = Symbol('config values');
const pendingSets = Symbol('Pending Settings');
module.exports = class Config {
static withDefaultSchema(settings = {}) {
@ -19,19 +17,18 @@ module.exports = class Config {
constructor(initialSchema, initialSettings) {
this[schemaExts] = Object.create(null);
this[vals] = Object.create(null);
this[pendingSets] = _.merge(Object.create(null), initialSettings || {});
if (initialSchema) this.extendSchema(initialSchema);
this.extendSchema(initialSchema, initialSettings);
}
getPendingSets() {
return new Map(_.pairs(this[pendingSets]));
}
extendSchema(extension, settings, key) {
if (!extension) {
return;
}
extendSchema(key, extension) {
if (key && key.isJoi) {
return _.each(key._inner.children, (child) => {
this.extendSchema(child.key, child.schema);
if (!key) {
return _.each(extension._inner.children, (child) => {
this.extendSchema(child.schema, _.get(settings, child.key), child.key);
});
}
@ -42,13 +39,7 @@ module.exports = class Config {
_.set(this[schemaExts], key, extension);
this[schema] = null;
const initialVals = _.get(this[pendingSets], key);
if (initialVals) {
this.set(key, initialVals);
unset(this[pendingSets], key);
} else {
this._commit(this[vals]);
}
this.set(key, settings);
}
removeSchema(key) {
@ -58,7 +49,6 @@ module.exports = class Config {
this[schema] = null;
unset(this[schemaExts], key);
unset(this[pendingSets], key);
unset(this[vals], key);
}

View file

@ -0,0 +1,7 @@
import { transformDeprecations } from './transform_deprecations';
export default function (kbnServer, server) {
transformDeprecations(kbnServer.settings, (message) => {
server.log(['warning', 'config', 'deprecation'], message);
});
}

View file

@ -1,6 +1,6 @@
import Joi from 'joi';
import { get } from 'lodash';
import { randomBytes } from 'crypto';
import { randomBytes, constants as cryptoConstants } from 'crypto';
import os from 'os';
import { fromRoot } from '../../utils';
@ -41,8 +41,20 @@ module.exports = () => Joi.object({
defaultRoute: Joi.string().default('/app/kibana').regex(/^\//, `start with a slash`),
basePath: Joi.string().default('').allow('').regex(/(^$|^\/.*[^\/]$)/, `start with a slash, don't end with one`),
ssl: Joi.object({
cert: Joi.string(),
key: Joi.string()
enabled: Joi.boolean().default(false),
certificate: Joi.string().when('enabled', {
is: true,
then: Joi.required(),
}),
key: Joi.string().when('enabled', {
is: true,
then: Joi.required()
}),
keyPassphrase: Joi.string(),
certificateAuthorities: Joi.array().single().items(Joi.string()),
clientAuthentication: Joi.string().valid('none', 'required').default('none'),
supportedProtocols: Joi.array().items(Joi.string().valid('TLSv1', 'TLSv1.1', 'TLSv1.2')),
cipherSuites: Joi.array().items(Joi.string()).default(cryptoConstants.defaultCoreCipherList.split(':'))
}).default(),
cors: Joi.when('$dev', {
is: true,

View file

@ -1,4 +1,7 @@
import Config from './config';
import { transformDeprecations } from './transform_deprecations';
module.exports = function (kbnServer) {
kbnServer.config = Config.withDefaultSchema(kbnServer.settings);
const settings = transformDeprecations(kbnServer.settings);
kbnServer.config = Config.withDefaultSchema(settings);
};

View file

@ -0,0 +1,56 @@
import _ , { partial } from 'lodash';
import { createTransform, Deprecations } from '../../deprecation';
const { rename, unused } = Deprecations;
const serverSslEnabled = (settings, log) => {
const has = partial(_.has, settings);
const set = partial(_.set, settings);
if (!has('server.ssl.enabled') && has('server.ssl.certificate') && has('server.ssl.key')) {
set('server.ssl.enabled', true);
log('Enabling ssl by only specifying server.ssl.certificate and server.ssl.key is deprecated. Please set server.ssl.enabled to true');
}
};
const deprecations = [
//server
rename('port' ,'server.port'),
rename('host', 'server.host'),
rename('pid_file', 'pid.file'),
rename('ssl_cert_file', 'server.ssl.certificate'),
rename('server.ssl.cert', 'server.ssl.certificate'),
rename('ssl_key_file', 'server.ssl.key'),
unused('server.xsrf.token'),
serverSslEnabled,
// logging
rename('log_file', 'logging.dest'),
// kibana
rename('kibana_index', 'kibana.index'),
rename('default_app_id', 'kibana.defaultAppId'),
// es
rename('ca', 'elasticsearch.ssl.ca'),
rename('elasticsearch_preserve_host', 'elasticsearch.preserveHost'),
rename('elasticsearch_url', 'elasticsearch.url'),
rename('kibana_elasticsearch_client_crt', 'elasticsearch.ssl.cert'),
rename('kibana_elasticsearch_client_key', 'elasticsearch.ssl.key'),
rename('kibana_elasticsearch_password', 'elasticsearch.password'),
rename('kibana_elasticsearch_username', 'elasticsearch.username'),
rename('ping_timeout', 'elasticsearch.pingTimeout'),
rename('request_timeout', 'elasticsearch.requestTimeout'),
rename('shard_timeout', 'elasticsearch.shardTimeout'),
rename('startup_timeout', 'elasticsearch.startupTimeout'),
rename('verify_ssl', 'elasticsearch.ssl.verify'),
// tilemap
rename('tilemap_url', 'tilemap.url'),
rename('tilemap_min_zoom', 'tilemap.options.minZoom'),
rename('tilemap_max_zoom', 'tilemap.options.maxZoom'),
rename('tilemap_attribution', 'tilemap.options.attribution'),
rename('tilemap_subdomains', 'tilemap.options.subdomains')
];
export const transformDeprecations = createTransform(deprecations);

View file

@ -0,0 +1,28 @@
import expect from 'expect.js';
import secureOptions from '../secure_options';
import crypto from 'crypto';
const constants = crypto.constants;
describe('secure_options', function () {
it('allows null', function () {
expect(secureOptions(null)).to.be(null);
});
it ('allows an empty array', function () {
expect(secureOptions([])).to.be(null);
});
it ('removes TLSv1 if we only support TLSv1.1 and TLSv1.2', function () {
expect(secureOptions(['TLSv1.1', 'TLSv1.2'])).to.be(constants.SSL_OP_NO_TLSv1);
});
it ('removes TLSv1.1 and TLSv1.2 if we only support TLSv1', function () {
expect(secureOptions(['TLSv1'])).to.be(constants.SSL_OP_NO_TLSv1_1 | constants.SSL_OP_NO_TLSv1_2);
});
it ('removes TLSv1 and TLSv1.1 if we only support TLSv1.2', function () {
expect(secureOptions(['TLSv1.2'])).to.be(constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1);
});
});

View file

@ -0,0 +1,24 @@
import crypto from 'crypto';
import { chain } from 'lodash';
const constants = crypto.constants;
const protocolMap = {
TLSv1: crypto.constants.SSL_OP_NO_TLSv1,
'TLSv1.1': crypto.constants.SSL_OP_NO_TLSv1_1,
'TLSv1.2': crypto.constants.SSL_OP_NO_TLSv1_2
};
export default function (supportedProtocols) {
if (!supportedProtocols || !supportedProtocols.length) {
return null;
}
return chain(protocolMap)
.omit(supportedProtocols)
.values()
.reduce(function (value, sum) {
return value | sum;
}, 0)
.value();
}

View file

@ -1,8 +1,25 @@
import { readFileSync } from 'fs';
import { format as formatUrl } from 'url';
import httpolyglot from 'httpolyglot';
import { map } from 'lodash';
import secureOptions from './secure_options';
import tlsCiphers from './tls_ciphers';
const getClientAuthenticationHttpOptions = (clientAuthentication) => {
switch (clientAuthentication) {
case 'none':
return {
requestCert: false,
rejectUnauthorized: false
};
case 'required':
return {
requestCert: true,
rejectUnauthorized: true
};
default:
throw new Error(`Unknown clientAuthentication option: ${clientAuthentication}`);
}
};
export default function (kbnServer, server, config) {
// this mixin is used outside of the kbn server, so it MUST work without a full kbnServer object.
@ -25,8 +42,7 @@ export default function (kbnServer, server, config) {
}
};
// enable tlsOpts if ssl key and cert are defined
const useSsl = config.get('server.ssl.key') && config.get('server.ssl.cert');
const useSsl = config.get('server.ssl.enabled');
// not using https? well that's easy!
if (!useSsl) {
@ -34,16 +50,23 @@ export default function (kbnServer, server, config) {
return;
}
const { requestCert, rejectUnauthorized } = getClientAuthenticationHttpOptions(config.get('server.ssl.clientAuthentication'));
server.connection({
...connectionOptions,
tls: true,
listener: httpolyglot.createServer({
key: readFileSync(config.get('server.ssl.key')),
cert: readFileSync(config.get('server.ssl.cert')),
cert: readFileSync(config.get('server.ssl.certificate')),
ca: map(config.get('server.ssl.certificateAuthorities'), readFileSync),
passphrase: config.get('server.ssl.keyPassphrase'),
ciphers: tlsCiphers,
ciphers: config.get('server.ssl.cipherSuites').join(':'),
// We use the server's cipher order rather than the client's to prevent the BEAST attack
honorCipherOrder: true
honorCipherOrder: true,
requestCert,
rejectUnauthorized,
secureOptions: secureOptions(config.get('server.ssl.supportedProtocols'))
})
});

View file

@ -1,26 +0,0 @@
// The default ciphers in node 0.12.x include insecure ciphers, so until
// we enforce a more recent version of node, we craft our own list
// @see https://github.com/nodejs/node/blob/master/src/node_constants.h#L8-L28
export default [
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-SHA256',
'DHE-RSA-AES128-SHA256',
'ECDHE-RSA-AES256-SHA384',
'DHE-RSA-AES256-SHA384',
'ECDHE-RSA-AES256-SHA256',
'DHE-RSA-AES256-SHA256',
'HIGH',
'!aNULL',
'!eNULL',
'!EXPORT',
'!DES',
'!RC4',
'!MD5',
'!PSK',
'!SRP',
'!CAMELLIA'
].join(':');

View file

@ -19,6 +19,7 @@ module.exports = class KbnServer {
require('./config/setup'), // sets this.config, reads this.settings
require('./http'), // sets this.server
require('./logging'),
require('./config/deprecation_warnings'),
require('./warnings'),
require('./status'),

View file

@ -3,6 +3,7 @@ import Joi from 'joi';
import Bluebird, { attempt, fromNode } from 'bluebird';
import { basename, resolve } from 'path';
import { inherits } from 'util';
import { Deprecations } from '../../deprecation';
const extendInitFns = Symbol('extend plugin initialization');
@ -68,6 +69,7 @@ module.exports = class Plugin {
this.externalInit = opts.init || _.noop;
this.configPrefix = opts.configPrefix || this.id;
this.getExternalConfigSchema = opts.config || _.noop;
this.getExternalDeprecations = opts.deprecations || _.noop;
this.preInit = _.once(this.preInit);
this.init = _.once(this.init);
this[extendInitFns] = [];
@ -99,6 +101,11 @@ module.exports = class Plugin {
return schema || defaultConfigSchema;
}
getDeprecations() {
const rules = this.getExternalDeprecations(Deprecations);
return rules || [];
}
async preInit() {
return await this.externalPreInit(this.kbnServer.server);
}

View file

@ -4,14 +4,24 @@ import { inspect } from 'util';
import { get, indexBy } from 'lodash';
import toPath from 'lodash/internal/toPath';
import Collection from '../../utils/collection';
import { transformDeprecations } from '../config/transform_deprecations';
import { createTransform } from '../../deprecation';
const byIdCache = Symbol('byIdCache');
const pluginApis = Symbol('pluginApis');
async function addPluginConfig(pluginCollection, plugin) {
const { config, server, settings } = pluginCollection.kbnServer;
const transformedSettings = transformDeprecations(settings);
const pluginSettings = get(transformedSettings, plugin.configPrefix);
const deprecations = plugin.getDeprecations();
const transformedPluginSettings = createTransform(deprecations)(pluginSettings, (message) => {
server.log(['warning', plugin.configPrefix, 'config', 'deprecation'], message);
});
const configSchema = await plugin.getConfigSchema();
const { config } = pluginCollection.kbnServer;
config.extendSchema(plugin.configPrefix, configSchema);
config.extendSchema(configSchema, transformedPluginSettings, plugin.configPrefix);
}
function removePluginConfig(pluginCollection, plugin) {

View file

@ -1,4 +1,6 @@
export Binder from './binder';
export BinderFor from './binder_for';
export deepCloneWithBuffers from './deep_clone_with_buffers';
export fromRoot from './from_root';
export pkg from './package_json';
export unset from './unset';