Skip to content

Commit

Permalink
Support PKCS#12 encoded certificates (#17261)
Browse files Browse the repository at this point in the history
Support for PKCS#12 encoded certificates
  • Loading branch information
legrego authored Apr 4, 2018
1 parent bd9509e commit de91bd0
Show file tree
Hide file tree
Showing 16 changed files with 219 additions and 65 deletions.
9 changes: 9 additions & 0 deletions docs/setup/production.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ server.ssl.key: /path/to/your/server.key
server.ssl.certificate: /path/to/your/server.crt
----

Alternatively, you can specify a PKCS#12 encoded certificate with the `server.ssl.keystore.path` property in `kibana.yml`:

[source,text]
----
# SSL for outgoing requests from the Kibana Server (PKCS#12 formatted)
server.ssl.enabled: true
server.ssl.keystore.path: /path/to/your/server.p12
----

If you are using X-Pack Security or a proxy that provides an HTTPS endpoint for Elasticsearch,
you can configure Kibana to access Elasticsearch via HTTPS so communications between
the Kibana server and Elasticsearch are encrypted.
Expand Down
9 changes: 8 additions & 1 deletion docs/setup/settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ To send *no* client-side headers, set this value to [] (an empty list).
`elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait for responses from the back end or
Elasticsearch. This value must be a positive integer.
`elasticsearch.shardTimeout:`:: *Default: 30000* Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable.
`elasticsearch.ssl.keystore.path`:: Optional setting that provides the path to the PKCS#12-format SSL Certificate and Key file. This file is used to verify the identity of Kibana
to Elasticsearch. Either this, or `elasticsearch.ssl.certificate`/`elasticsearch.ssl.key` pair is required when `xpack.ssl.verification_mode` in Elasticsearch is set to either
`certificate` or `full`. Specifying both `elasticsearch.ssl.keystore.path` and `elasticsearch.ssl.certificate` is not allowed.
`elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Optional settings that provide the paths to the PEM-format SSL
certificate and key files. These files are used to verify the identity of Kibana to Elasticsearch and are required when `xpack.ssl.verification_mode` in Elasticsearch is set to either `certificate` or `full`.
certificate and key files. These files are used to verify the identity of Kibana to Elasticsearch.
Either this, or `elasticsearch.ssl.keystore.path` is required when `xpack.ssl.verification_mode` in Elasticsearch is set to either `certificate` or `full`.
Specifying both `elasticsearch.ssl.certificate` and `elasticsearch.ssl.keystore.path` is not allowed.
`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.keyPassphrase:`:: The passphrase that will be used to decrypt the private key. This value is optional as the key may not be encrypted.
Expand Down Expand Up @@ -97,6 +102,8 @@ By turning this off, only the layers that are configured here will be included.
`server.port:`:: *Default: 5601* Kibana is served by a back end server. This setting specifies the port to use.
`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.keystore.path`:: Path to the PKCS#12 encoded SSL certificate and key. This is an alternative to setting `server.ssl.certificate` and `server.ssl.key`.
`server.ssl.keystore.password`:: The password that will be used to decrypt the private key within the keystore. 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.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)
`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.
Expand Down
18 changes: 18 additions & 0 deletions src/cli/cluster/__tests__/base_path_proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import expect from 'expect.js';
import { set } from 'lodash';
import BasePathProxy from '../base_path_proxy';

describe('CLI Cluster Manager', function () {
describe('base_path_proxy constructor', function () {
it('should throw an error when both server.ssl.keystore.path and server.ssl.certificate are specified', function () {
const settings = {};
set(settings, 'server.ssl.keystore.path', '/cert.p12');
set(settings, 'server.ssl.certificate', './cert.crt');
set(settings, 'server.ssl.key', './cert.key');

expect(() => new BasePathProxy(null, settings)).to.throwError(
`Invalid Configuration: please specify either "server.ssl.keystore.path" or "server.ssl.certificate", not both.`
);
});
});
});
27 changes: 21 additions & 6 deletions src/cli/cluster/base_path_proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,28 @@ export default class BasePathProxy {

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),
const agentOptions = {
ca: map(config.get('server.ssl.certificateAuthorities'), (certAuthority) => readFileSync(certAuthority)),
rejectUnauthorized: false
});
};

const keystoreConfig = config.get('server.ssl.keystore.path');
const pemConfig = config.get('server.ssl.certificate');

if (keystoreConfig && pemConfig) {
throw new Error(`Invalid Configuration: please specify either "server.ssl.keystore.path" or "server.ssl.certificate", not both.`);
}

if (keystoreConfig) {
agentOptions.pfx = readFileSync(keystoreConfig);
agentOptions.passphrase = config.get('server.ssl.keystore.password');
} else {
agentOptions.key = readFileSync(config.get('server.ssl.key'));
agentOptions.cert = readFileSync(pemConfig);
agentOptions.passphrase = config.get('server.ssl.keyPassphrase');
}

this.proxyAgent = new HttpsAgent(agentOptions);
}

if (!this.basePath) {
Expand Down
2 changes: 1 addition & 1 deletion src/cli/serve/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function readServerSettings(opts, extraCliOptions) {
set('server.ssl.enabled', true);
}

if (opts.ssl && !has('server.ssl.certificate') && !has('server.ssl.key')) {
if (opts.ssl && !has('server.ssl.keystore.path') && !has('server.ssl.certificate') && !has('server.ssl.key')) {
set('server.ssl.certificate', DEV_SSL_CERT_PATH);
set('server.ssl.key', DEV_SSL_KEY_PATH);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ const createAgent = (server) => {
}

// Add client certificate and key if required by elasticsearch
if (config.get('elasticsearch.ssl.certificate') && config.get('elasticsearch.ssl.key')) {
if (config.get('elasticsearch.ssl.keystore.path')) {
agentOptions.pfx = readFile(config.get('elasticsearch.ssl.keystore.path'));
agentOptions.passphrase = config.get('elasticsearch.ssl.keystore.password');
} else 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');
Expand Down
52 changes: 29 additions & 23 deletions src/core_plugins/elasticsearch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,39 @@ export default function (kibana) {
return new kibana.Plugin({
require: ['kibana'],
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()
const sslSchema = Joi.object({
verificationMode: Joi.string().valid('none', 'certificate', 'full').default('full'),
certificateAuthorities: Joi.array().single().items(Joi.string()),
certificate: Joi.string(),
key: Joi.when('certificate', {
is: Joi.exist(),
then: Joi.string().required(),
otherwise: Joi.string().forbidden()
}),
keystore: Joi.object({
path: Joi.string(),
password: Joi.string()
}).default(),
keyPassphrase: Joi.string()
}).default();

return object({
enabled: boolean().default(true),
url: string().uri({ scheme: ['http', 'https'] }).default('http://localhost:9200'),
preserveHost: boolean().default(true),
username: string(),
password: string(),
shardTimeout: number().default(30000),
requestTimeout: number().default(30000),
requestHeadersWhitelist: array().items().single().default(DEFAULT_REQUEST_HEADERS),
customHeaders: object().default({}),
pingTimeout: number().default(ref('requestTimeout')),
startupTimeout: number().default(5000),
logQueries: boolean().default(false),
return Joi.object({
enabled: Joi.boolean().default(true),
url: Joi.string().uri({ scheme: ['http', 'https'] }).default('http://localhost:9200'),
preserveHost: Joi.boolean().default(true),
username: Joi.string(),
password: Joi.string(),
shardTimeout: Joi.number().default(30000),
requestTimeout: Joi.number().default(30000),
requestHeadersWhitelist: Joi.array().items().single().default(DEFAULT_REQUEST_HEADERS),
customHeaders: Joi.object().default({}),
pingTimeout: Joi.number().default(Joi.ref('requestTimeout')),
startupTimeout: Joi.number().default(5000),
logQueries: Joi.boolean().default(false),
ssl: sslSchema,
apiVersion: Joi.string().default('master'),
healthCheck: object({
delay: number().default(2500)
healthCheck: Joi.object({
delay: Joi.number().default(2500)
}).default(),
}).default();
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test pfx
22 changes: 21 additions & 1 deletion src/core_plugins/elasticsearch/lib/__tests__/parse_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ describe('plugins/elasticsearch', function () {
serverConfig = {
url: 'https://localhost:9200',
ssl: {
verificationMode: 'full'
verificationMode: 'full',
keystore: {}
}
};
});
Expand Down Expand Up @@ -86,6 +87,25 @@ describe('plugins/elasticsearch', function () {
const config = parseConfig(serverConfig);
expect(config.ssl.passphrase).to.be('secret');
});

it(`sets pfx when a PKCS#12 certificate bundle is specified`, function () {
serverConfig.ssl.keystore.path = __dirname + '/fixtures/cert.pfx';
serverConfig.ssl.keystore.password = 'secret';

const config = parseConfig(serverConfig);
expect(Buffer.isBuffer(config.ssl.pfx)).to.be(true);
expect(config.ssl.pfx.toString('utf-8')).to.be('test pfx\n');
expect(config.ssl.passphrase).to.be('secret');
});

it('throws an error when both pfx and certificate are specified', function () {
serverConfig.ssl.certificate = __dirname + '/fixtures/cert.crt';
serverConfig.ssl.keystore.path = __dirname + '/fixtures/cert.pfx';

expect(() => parseConfig(serverConfig)).to.throwError(
`Invalid Configuration: please specify either "elasticsearch.ssl.keystore.path" or "elasticsearch.ssl.certificate", not both.`
);
});
});
});
});
17 changes: 15 additions & 2 deletions src/core_plugins/elasticsearch/lib/parse_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { readFileSync } from 'fs';
import Bluebird from 'bluebird';

const readFile = (file) => readFileSync(file, 'utf8');
const readBinaryFile = (file) => readFileSync(file);

export function parseConfig(serverConfig = {}) {
const config = {
Expand Down Expand Up @@ -56,8 +57,20 @@ export function parseConfig(serverConfig = {}) {
}

// 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);
const keystoreConfig = get(serverConfig, 'ssl.keystore.path');
const pemConfig = get(serverConfig, 'ssl.certificate');

if (keystoreConfig && pemConfig) {
throw new Error(
`Invalid Configuration: please specify either "elasticsearch.ssl.keystore.path" or "elasticsearch.ssl.certificate", not both.`
);
}

if (keystoreConfig) {
config.ssl.pfx = readBinaryFile(keystoreConfig);
config.ssl.passphrase = get(serverConfig, 'ssl.keystore.password');
} else if (pemConfig && get(serverConfig, 'ssl.key')) {
config.ssl.cert = readFile(pemConfig);
config.ssl.key = readFile(serverConfig.ssl.key);
config.ssl.passphrase = serverConfig.ssl.keyPassphrase;
}
Expand Down
40 changes: 32 additions & 8 deletions src/server/config/__tests__/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,34 +120,58 @@ describe('Config schema', function () {
const { error } = validate(config);
expect(error).to.be(null);
});
});

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.key', '/path.key');
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.certificate');
expect(error.details[0]).to.have.property('path', 'server.ssl.key');
});
});

describe('key', function () {
describe('keystore.path', 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 () {
it('is allowed when ssl is enabled, and a certificate is not specified', function () {
const config = {};
set(config, 'server.ssl.enabled', true);
set(config, 'server.ssl.certificate', '/path.cert');
set(config, 'server.ssl.keystore.path', '/path.p12');
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');
expect(error).to.be(null);
});
});

describe('keystore.password', 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 allowed when ssl is enabled, and a certificate is not specified', function () {
const config = {};
set(config, 'server.ssl.enabled', true);
set(config, 'server.ssl.keystore.password', 'secret');
const { error } = validate(config);
expect(error).to.be(null);
});
});

Expand Down
15 changes: 15 additions & 0 deletions src/server/config/__tests__/transform_deprecations.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ describe('server/config', function () {
expect(result.server.ssl.enabled).to.be(true);
});

it('sets enabled to true when keystore.path is set', function () {
const settings = {
server: {
ssl: {
keystore: {
path: '/server.pfx'
}
}
}
};

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: {
Expand Down
1 change: 1 addition & 0 deletions src/server/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export class Config {
const child = schema._inner.children[i];
// If the child is an object recurse through it's children and return
// true if there's a match

if (child.schema._type === 'object') {
if (has(key, child.schema, path.concat([child.key]))) return true;
// if the child matches, return true
Expand Down
36 changes: 20 additions & 16 deletions src/server/config/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ import os from 'os';
import { fromRoot } from '../../utils';
import { getData } from '../path';

const sslSchema = Joi.object({
enabled: Joi.boolean().optional().default(false),
redirectHttpFromPort: Joi.number().default(),
keystore: Joi.object({
path: Joi.string(),
password: Joi.string()
}).default(),
certificate: Joi.string(),
key: Joi.when('certificate', {
is: Joi.exist(),
then: Joi.string().required(),
otherwise: Joi.string().forbidden()
}),
keyPassphrase: Joi.string(),
certificateAuthorities: Joi.array().single().items(Joi.string()).default([]),
supportedProtocols: Joi.array().items(Joi.string().valid('TLSv1', 'TLSv1.1', 'TLSv1.2')).default([]),
cipherSuites: Joi.array().items(Joi.string()).default(cryptoConstants.defaultCoreCipherList.split(':'))
});

export default () => Joi.object({
pkg: Joi.object({
version: Joi.string().default(Joi.ref('$version')),
Expand Down Expand Up @@ -59,22 +78,7 @@ export default () => Joi.object({
otherwise: Joi.default(false),
}),
customResponseHeaders: Joi.object().unknown(true).default({}),
ssl: Joi.object({
enabled: Joi.boolean().default(false),
redirectHttpFromPort: Joi.number(),
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()).default([]),
supportedProtocols: Joi.array().items(Joi.string().valid('TLSv1', 'TLSv1.1', 'TLSv1.2')),
cipherSuites: Joi.array().items(Joi.string()).default(cryptoConstants.defaultCoreCipherList.split(':'))
}).default(),
ssl: sslSchema.default(),
cors: Joi.when('$dev', {
is: true,
then: Joi.object().default({
Expand Down
Loading

0 comments on commit de91bd0

Please sign in to comment.